Compare commits
No commits in common. "main" and "builds" have entirely different histories.
121 changed files with 1 additions and 6897 deletions
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
|
|
@ -1,59 +0,0 @@
|
||||||
name: Build
|
|
||||||
|
|
||||||
# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency
|
|
||||||
concurrency:
|
|
||||||
group: "build"
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths-ignore:
|
|
||||||
- '*.md'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@master
|
|
||||||
with:
|
|
||||||
path: "src"
|
|
||||||
|
|
||||||
- name: Checkout builds
|
|
||||||
uses: actions/checkout@master
|
|
||||||
with:
|
|
||||||
ref: "builds"
|
|
||||||
path: "builds"
|
|
||||||
|
|
||||||
- name: Checkout Aliucord
|
|
||||||
uses: actions/checkout@master
|
|
||||||
with:
|
|
||||||
repository: "Aliucord/Aliucord"
|
|
||||||
path: "repo"
|
|
||||||
|
|
||||||
- name: Setup JDK 21
|
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: 21
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
- name: Build Plugins
|
|
||||||
run: |
|
|
||||||
cd $GITHUB_WORKSPACE/src
|
|
||||||
chmod +x gradlew
|
|
||||||
./gradlew make generateUpdaterJson
|
|
||||||
cp {canary,plugins}/*/build/outputs/*.zip $GITHUB_WORKSPACE/builds
|
|
||||||
cp build/outputs/updater.json $GITHUB_WORKSPACE/builds
|
|
||||||
|
|
||||||
- name: Push builds
|
|
||||||
run: |
|
|
||||||
cd $GITHUB_WORKSPACE/builds
|
|
||||||
git config --local user.email "actions@github.com"
|
|
||||||
git config --local user.name "GitHub Actions"
|
|
||||||
git add .
|
|
||||||
git commit -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
|
|
||||||
git push
|
|
||||||
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -1,12 +0,0 @@
|
||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
**/build
|
|
||||||
/captures
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
|
||||||
local.properties
|
|
||||||
/libs
|
|
||||||
BIN
AlignThreads.zip
Normal file
BIN
AlignThreads.zip
Normal file
Binary file not shown.
BIN
Bubbles.zip
Normal file
BIN
Bubbles.zip
Normal file
Binary file not shown.
BIN
Clump.zip
Normal file
BIN
Clump.zip
Normal file
Binary file not shown.
BIN
ComponentsV2Beta.zip
Normal file
BIN
ComponentsV2Beta.zip
Normal file
Binary file not shown.
BIN
Glance.zip
Normal file
BIN
Glance.zip
Normal file
Binary file not shown.
19
LICENCE
19
LICENCE
|
|
@ -1,19 +0,0 @@
|
||||||
Copyright (c) 2025 Cilly Leang
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
43
README.md
43
README.md
|
|
@ -1,43 +0,0 @@
|
||||||
# Awoocord Plugins
|
|
||||||
|
|
||||||
## [Bubbles](plugins/Crocosmia) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Bubbles.zip)
|
|
||||||
|
|
||||||
Wrap messages in bubbles inspired by Material 3 Expressive
|
|
||||||
|
|
||||||
## [Clump](plugins/Bocchi) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Clump.zip)
|
|
||||||
|
|
||||||
Group messages more leniently (e.g. mentions, attachments, etc..), reducing clutter and wasted space.
|
|
||||||
|
|
||||||
## [Glance](plugins/Myosotis) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Glance.zip)
|
|
||||||
|
|
||||||
Backports DM previews similar to latest RN. Shows you a line of the last message sent in a DM.
|
|
||||||
|
|
||||||
## [RoleBlocks](plugins/Zinnia) | [Download](https://github.com/cillynder/Awoocord/raw/builds/RoleBlocks.zip)
|
|
||||||
|
|
||||||
Apply the role colour as a background of usernames, improving contrast with some role colours
|
|
||||||
|
|
||||||
## [Scout](plugins/Scout) | [Download](https://github.com/cillynder/Awoocord/raw/builds/Scout.zip)
|
|
||||||
|
|
||||||
Vastly improves the search experience on Aliucord.
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Sort by oldest messages first
|
|
||||||
- Sort by oldest first
|
|
||||||
- Filter by date (before, during, after)
|
|
||||||
- Exclude certain messages (opposite of `in:`) (not even desktop has this!)
|
|
||||||
- Search by user ID
|
|
||||||
- Search in threads
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
- Removes the large padding from the top, most noticable if your device has a large status bar
|
|
||||||
- Removes the unnecessary #0000 discriminator
|
|
||||||
|
|
||||||
# WIP Backports
|
|
||||||
|
|
||||||
## [SlashCommandsFix](canary/SlashCommandsFix) | [Download](https://github.com/cillynder/Awoocord/raw/builds/SlashCommandsFixBeta.zip)
|
|
||||||
|
|
||||||
Fixes slash commands not showing up.
|
|
||||||
|
|
||||||
## [ComponentsV2](canary/ComponentsV2) | [Download](https://github.com/cillynder/Awoocord/raw/builds/ComponentsV2Beta.zip)
|
|
||||||
|
|
||||||
Fix missing/empty bot messages using the new embed system. Such messages will be marked "CV2" as part of its tag.
|
|
||||||
BIN
RoleBlocks.zip
Normal file
BIN
RoleBlocks.zip
Normal file
Binary file not shown.
BIN
Scout.zip
Normal file
BIN
Scout.zip
Normal file
Binary file not shown.
BIN
SlashCommandsFixBeta.zip
Normal file
BIN
SlashCommandsFixBeta.zip
Normal file
Binary file not shown.
|
|
@ -1,78 +0,0 @@
|
||||||
@file:Suppress("UnstableApiUsage")
|
|
||||||
|
|
||||||
import com.aliucord.gradle.AliucordExtension
|
|
||||||
import com.android.build.gradle.LibraryExtension
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidExtension
|
|
||||||
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
|
||||||
alias(libs.plugins.android.library) apply false
|
|
||||||
alias(libs.plugins.aliucord.plugin) apply true
|
|
||||||
alias(libs.plugins.ktlint) apply false
|
|
||||||
alias(libs.plugins.shadow) apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
subprojects {
|
|
||||||
val libs = rootProject.libs
|
|
||||||
|
|
||||||
apply {
|
|
||||||
plugin(libs.plugins.android.library.get().pluginId)
|
|
||||||
plugin(libs.plugins.aliucord.plugin.get().pluginId)
|
|
||||||
plugin(libs.plugins.kotlin.android.get().pluginId)
|
|
||||||
plugin(libs.plugins.ktlint.get().pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
configure<LibraryExtension> {
|
|
||||||
compileSdk = 36
|
|
||||||
namespace = "moe.lava.awoocord"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
aidl = false
|
|
||||||
buildConfig = true
|
|
||||||
renderScript = false
|
|
||||||
shaders = false
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configure<AliucordExtension> {
|
|
||||||
author("cilly", 368398754077868032L, hyperlink = false)
|
|
||||||
github("https://github.com/cillynder/Awoocord")
|
|
||||||
}
|
|
||||||
|
|
||||||
configure<KtlintExtension> {
|
|
||||||
version.set(libs.versions.ktlint.asProvider())
|
|
||||||
|
|
||||||
coloredOutput.set(true)
|
|
||||||
outputColorName.set("RED")
|
|
||||||
ignoreFailures.set(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
configure<KotlinAndroidExtension> {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget = JvmTarget.JVM_21
|
|
||||||
optIn.add("kotlin.RequiresOptIn")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
dependencies {
|
|
||||||
val compileOnly by configurations
|
|
||||||
val implementation by configurations
|
|
||||||
|
|
||||||
compileOnly(libs.discord)
|
|
||||||
compileOnly(libs.aliucord)
|
|
||||||
compileOnly(libs.aliuhook)
|
|
||||||
compileOnly(libs.kotlin.stdlib)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
|
||||||
|
|
||||||
version = "8.8.0"
|
|
||||||
description = "Beta backport of ComponentsV2"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "moe.lava.corenary.componentsv2"
|
|
||||||
}
|
|
||||||
|
|
||||||
aliucord {
|
|
||||||
// Changelog of your plugin
|
|
||||||
changelog.set("""
|
|
||||||
TODO {fixed}
|
|
||||||
======================
|
|
||||||
* File component
|
|
||||||
* SelectV2: searching
|
|
||||||
* SelectV2: showing selected items in chat list
|
|
||||||
|
|
||||||
Changelog {added marginTop}
|
|
||||||
======================
|
|
||||||
# 8.8.0
|
|
||||||
* Fix a possible weird crash
|
|
||||||
|
|
||||||
# 8.7.0
|
|
||||||
* Prevent ViewRaw crash
|
|
||||||
* Add a CV2 tag to distinguish new embeds (will not be in core)
|
|
||||||
|
|
||||||
# 7.15.1
|
|
||||||
* Fix broken reply preview >w<
|
|
||||||
|
|
||||||
# 7.15.0
|
|
||||||
* Initial release >w<
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
deploy.set(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
apply {
|
|
||||||
plugin(libs.plugins.shadow.get().pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val shadowDir = File(buildDir, "intermediates/shadowed")
|
|
||||||
|
|
||||||
tasks.register<ShadowJar>("relocateJar") {
|
|
||||||
val task = tasks.findByName("compileDebugKotlin")!!
|
|
||||||
from(task.outputs)
|
|
||||||
// relocate("com.discord.api.botuikit", "moe.lava.awoocanary.componentsv2.botuikit") {
|
|
||||||
// exclude("com.discord.api.botuikit.ComponentType")
|
|
||||||
// }
|
|
||||||
relocate("com.aliucord.coreplugins.componentsv2", "moe.lava.corenary.componentsv2")
|
|
||||||
relocate("com.aliucord.coreplugins.ComponentsV2", "moe.lava.corenary.ComponentsV2")
|
|
||||||
archiveClassifier.set("shadowed")
|
|
||||||
destinationDirectory.set(File(buildDir, "intermediates"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Sync>("copyShadowed") {
|
|
||||||
val reloc = tasks.findByName("relocateJar")!! as ShadowJar
|
|
||||||
dependsOn(reloc)
|
|
||||||
from(zipTree(reloc.archiveFile))
|
|
||||||
into(shadowDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
project.afterEvaluate {
|
|
||||||
tasks.compileDex {
|
|
||||||
val copyShadowed = tasks.findByName("copyShadowed")!! as Sync
|
|
||||||
dependsOn(copyShadowed)
|
|
||||||
input.setFrom(shadowDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package com.aliucord.coreplugins
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.aliucord.Constants
|
|
||||||
import com.aliucord.Utils
|
|
||||||
import com.aliucord.api.PatcherAPI
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.patcher.*
|
|
||||||
import com.aliucord.utils.GsonUtils
|
|
||||||
import com.aliucord.utils.GsonUtils.toJson
|
|
||||||
import com.aliucord.utils.ReflectUtils
|
|
||||||
import com.discord.api.botuikit.ComponentType
|
|
||||||
import com.discord.api.botuikit.gson.ComponentRuntimeTypeAdapter
|
|
||||||
import com.discord.api.botuikit.gson.ComponentTypeTypeAdapter
|
|
||||||
import com.discord.api.message.attachment.MessageAttachment
|
|
||||||
import com.discord.models.domain.Model
|
|
||||||
import com.discord.models.message.Message
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import java.io.File
|
|
||||||
import b.a.b.a as TypeAdapterRegistrar
|
|
||||||
import b.i.d.c as FieldNamingPolicy
|
|
||||||
import b.i.d.e as GsonBuilder
|
|
||||||
|
|
||||||
fun ComponentsV2.compat(patcher: PatcherAPI) {
|
|
||||||
// check for old cursed plugin, probably not needed anymore
|
|
||||||
val oldFile = File("${Constants.PLUGINS_PATH}/ComponentsV2-Beta.zip")
|
|
||||||
if (oldFile.exists()) {
|
|
||||||
logger.info("old plugin found, deleting and prompting restart")
|
|
||||||
oldFile.delete()
|
|
||||||
Utils.promptRestart()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// I'm sorry
|
|
||||||
// ViewRaw crashes without this
|
|
||||||
val cuteGson = GsonBuilder().run {
|
|
||||||
c = FieldNamingPolicy.m // LOWER_CASE_WITH_UNDERSCORES
|
|
||||||
TypeAdapterRegistrar.a(this)
|
|
||||||
e.add(Model.TypeAdapterFactory())
|
|
||||||
a().apply {
|
|
||||||
ReflectUtils.setField(this, "k", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patcher.patch(GsonUtils::class.java.getDeclaredMethod("toJsonPretty", Object::class.java))
|
|
||||||
{ (param, obj: Any) ->
|
|
||||||
if (obj is Message && obj.isComponentV2)
|
|
||||||
param.result = cuteGson.toJson(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add cv2 tag
|
|
||||||
patcher.after<WidgetChatListAdapterItemMessage>("configureItemTag", Message::class.java, Boolean::class.javaPrimitiveType!!)
|
|
||||||
{ (_, msg: Message) ->
|
|
||||||
val textView = ReflectUtils.getField(this, "itemTag") as TextView?
|
|
||||||
?: return@after
|
|
||||||
|
|
||||||
if (!msg.isComponentV2)
|
|
||||||
return@after
|
|
||||||
|
|
||||||
if (textView.text.isEmpty()) {
|
|
||||||
// this code path shouldn't really ever run (only bots can send cv2, and bots have the tag already)
|
|
||||||
// but idk maybe someone self-bots or something
|
|
||||||
textView.visibility = View.VISIBLE
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
textView.text = "CV2"
|
|
||||||
textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
|
||||||
} else {
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
textView.text = textView.text.toString() + " | CV2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ComponentV2Type.make()
|
|
||||||
patchGson(patcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ComponentsV2.stopCompat() {
|
|
||||||
unpatchGson()
|
|
||||||
ComponentV2Type.unmake(logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchGson(patcher: PatcherAPI) {
|
|
||||||
val factory = ComponentRuntimeTypeAdapter.INSTANCE.a()
|
|
||||||
val typeToClass = factory.l
|
|
||||||
val classToType = factory.m
|
|
||||||
ComponentV2Type.newValues?.forEach {
|
|
||||||
typeToClass[it.type.toString()] = it.clazz
|
|
||||||
classToType[it.clazz] = it.type.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.instead<ComponentTypeTypeAdapter>("read", JsonReader::class.java)
|
|
||||||
{ (_, jsonReader: JsonReader) ->
|
|
||||||
val type: Int = b.c.a.a0.d.n1(jsonReader)
|
|
||||||
ComponentType.values().find { it.type == type } ?: ComponentType.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun unpatchGson() {
|
|
||||||
val factory = ComponentRuntimeTypeAdapter.INSTANCE.a()
|
|
||||||
val typeToClass = factory.l
|
|
||||||
val classToType = factory.m
|
|
||||||
ComponentV2Type.newValues?.forEach {
|
|
||||||
typeToClass.remove(it.type.toString())
|
|
||||||
classToType.remove(it.clazz)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object CV2Compat {
|
|
||||||
/** Creates a new [MessageAttachment] */
|
|
||||||
fun createAttachment(
|
|
||||||
filename: String,
|
|
||||||
filesize: Long,
|
|
||||||
proxyUrl: String,
|
|
||||||
url: String,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
): MessageAttachment {
|
|
||||||
val inst = ReflectUtils.allocateInstance(clazz)
|
|
||||||
filenameField.set(inst, filename)
|
|
||||||
filesizeField.set(inst, filesize)
|
|
||||||
proxyUrlField.set(inst, proxyUrl)
|
|
||||||
urlField.set(inst, url)
|
|
||||||
widthField.set(inst, width)
|
|
||||||
heightField.set(inst, height)
|
|
||||||
return inst
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val clazz = MessageAttachment::class.java
|
|
||||||
private val filenameField = clazz.getDeclaredField("filename").apply { isAccessible = true }
|
|
||||||
private val filesizeField = clazz.getDeclaredField("size").apply { isAccessible = true }
|
|
||||||
private val proxyUrlField = clazz.getDeclaredField("proxyUrl").apply { isAccessible = true }
|
|
||||||
private val urlField = clazz.getDeclaredField("url").apply { isAccessible = true }
|
|
||||||
private val widthField = clazz.getDeclaredField("width").apply { isAccessible = true }
|
|
||||||
private val heightField = clazz.getDeclaredField("height").apply { isAccessible = true }
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
package com.aliucord.coreplugins
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.aliucord.Utils
|
|
||||||
import com.aliucord.annotations.AliucordPlugin
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.*
|
|
||||||
import com.aliucord.coreplugins.componentsv2.patchMessageItems
|
|
||||||
import com.aliucord.coreplugins.componentsv2.views.*
|
|
||||||
import com.aliucord.entities.Plugin
|
|
||||||
import com.aliucord.patcher.*
|
|
||||||
import com.discord.api.botuikit.*
|
|
||||||
import com.discord.models.botuikit.*
|
|
||||||
import com.discord.models.message.Message
|
|
||||||
import com.discord.stores.StoreApplicationInteractions.InteractionSendState
|
|
||||||
import com.discord.utilities.view.extensions.ViewExtensions
|
|
||||||
import com.discord.widgets.botuikit.*
|
|
||||||
import com.discord.widgets.botuikit.ComponentChatListState.ComponentStoreState
|
|
||||||
import com.discord.widgets.botuikit.views.*
|
|
||||||
import com.discord.widgets.botuikit.views.select.SelectComponentView
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapter
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
|
||||||
import com.discord.widgets.chat.list.entries.BotUiComponentEntry
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
import de.robv.android.xposed.XposedBridge
|
|
||||||
|
|
||||||
val Message.isComponentV2 get() = ((flags ?: 0) shr 15) and 1 == 1L
|
|
||||||
|
|
||||||
@AliucordPlugin(requiresRestart = true)
|
|
||||||
@Suppress("unused")
|
|
||||||
class ComponentsV2 : Plugin() {
|
|
||||||
override fun start(context: Context) {
|
|
||||||
compat(patcher)
|
|
||||||
XposedBridge.makeClassInheritable(BotUiComponentEntry::class.java)
|
|
||||||
// https://github.com/LSPosed/LSPlant/issues/41
|
|
||||||
patchMessageItems(patcher)
|
|
||||||
|
|
||||||
patcher.instead<ComponentStateMapper>(
|
|
||||||
"toMessageLayoutComponent",
|
|
||||||
LayoutComponent::class.java,
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
List::class.java,
|
|
||||||
ComponentExperiments::class.java
|
|
||||||
) { (_, layout: LayoutComponent, index: Int, components: List<MessageComponent>) ->
|
|
||||||
when (layout) {
|
|
||||||
is ActionRowComponent ->
|
|
||||||
ActionRowMessageComponent(layout.type, index, components)
|
|
||||||
is SectionComponent ->
|
|
||||||
SectionMessageComponent.mergeToMessageComponent(layout, index, components)
|
|
||||||
is TextDisplayComponent ->
|
|
||||||
TextDisplayMessageComponent.mergeToMessageComponent(layout, index)
|
|
||||||
is ThumbnailComponent ->
|
|
||||||
ThumbnailMessageComponent.mergeToMessageComponent(layout, index)
|
|
||||||
is MediaGalleryComponent ->
|
|
||||||
MediaGalleryMessageComponent.mergeToMessageComponent(layout, index)
|
|
||||||
is FileComponent ->
|
|
||||||
ActionRowMessageComponent(layout.type, index, components)
|
|
||||||
is SeparatorComponent ->
|
|
||||||
SeparatorMessageComponent.mergeToMessageComponent(layout, index)
|
|
||||||
is ContainerComponent ->
|
|
||||||
ContainerMessageComponent.mergeToMessageComponent(layout, index, components)
|
|
||||||
else ->
|
|
||||||
throw IllegalArgumentException("Unknown layout component ${layout::class.java.name} (${layout.type.type}:${layout.type.name})")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.instead<ComponentProvider>("configureView", ComponentActionListener::class.java, MessageComponent::class.java, ComponentView::class.java)
|
|
||||||
{ (_, listener: ComponentActionListener, component: MessageComponent, view: ComponentView<MessageComponent>?) ->
|
|
||||||
view?.configure(component, this, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.instead<ComponentInflater>("inflateComponent", ComponentType::class.java, ViewGroup::class.java)
|
|
||||||
{ (_, type: ComponentType, viewGroup: ViewGroup) ->
|
|
||||||
when (type) {
|
|
||||||
ComponentType.ACTION_ROW ->
|
|
||||||
ActionRowComponentView.Companion!!.inflateComponent(this.context, viewGroup)
|
|
||||||
ComponentType.BUTTON ->
|
|
||||||
ButtonComponentView.Companion!!.inflateComponent(this.context, viewGroup)
|
|
||||||
ComponentType.SELECT ->
|
|
||||||
SelectComponentView.Companion!!.inflateComponent(this.context, viewGroup)
|
|
||||||
ComponentV2Type.USER_SELECT,
|
|
||||||
ComponentV2Type.ROLE_SELECT,
|
|
||||||
ComponentV2Type.MENTIONABLE_SELECT,
|
|
||||||
ComponentV2Type.CHANNEL_SELECT ->
|
|
||||||
SelectV2ComponentView(this.context, type)
|
|
||||||
ComponentV2Type.SECTION ->
|
|
||||||
SectionComponentView(this.context)
|
|
||||||
ComponentV2Type.TEXT_DISPLAY ->
|
|
||||||
TextDisplayComponentView(this.context)
|
|
||||||
ComponentV2Type.THUMBNAIL ->
|
|
||||||
ThumbnailComponentView(this.context)
|
|
||||||
ComponentV2Type.MEDIA_GALLERY ->
|
|
||||||
MediaGalleryComponentView(this.context)
|
|
||||||
ComponentV2Type.FILE ->
|
|
||||||
null
|
|
||||||
ComponentV2Type.SEPARATOR ->
|
|
||||||
SeparatorComponentView(this.context)
|
|
||||||
ComponentV2Type.CONTAINER ->
|
|
||||||
ContainerComponentView(this.context)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.after<WidgetChatListAdapterItemBotComponentRow>(WidgetChatListAdapter::class.java)
|
|
||||||
{
|
|
||||||
val rootLayout = itemView.findViewById<LinearLayout>(Utils.getResId("chat_list_adapter_item_component_root", "id"))
|
|
||||||
rootLayout.layoutParams = (rootLayout.layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
marginEnd = adapter.context.resources.getDimension(R.d.chat_cell_horizontal_spacing_padding).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewExtensions.setOnLongClickListenerConsumeClick(itemView) {
|
|
||||||
adapter.eventHandler.onMessageLongClicked(entry.message, "", false)
|
|
||||||
}
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
adapter.eventHandler.onMessageClicked(entry.message, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.instead<ComponentStateMapper>(
|
|
||||||
"createActionMessageComponent",
|
|
||||||
ActionComponent::class.java,
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
ComponentStoreState::class.java,
|
|
||||||
ComponentExperiments::class.java,
|
|
||||||
) { (
|
|
||||||
_,
|
|
||||||
actionComponent: ActionComponent,
|
|
||||||
index: Int,
|
|
||||||
componentStoreState: ComponentStoreState,
|
|
||||||
) ->
|
|
||||||
val interactionState: Map<Int, InteractionSendState>? = componentStoreState.interactionState;
|
|
||||||
val num = interactionState?.entries?.find { it.value is InteractionSendState.Loading }?.key
|
|
||||||
|
|
||||||
val state = interactionState?.get(index)
|
|
||||||
val comState: ActionInteractionComponentState = when {
|
|
||||||
state is InteractionSendState.Failed -> ActionInteractionComponentState.Failed(state.errorMessage)
|
|
||||||
num == null -> ActionInteractionComponentState.Enabled.INSTANCE
|
|
||||||
num == index -> ActionInteractionComponentState.Loading.INSTANCE
|
|
||||||
else -> ActionInteractionComponentState.Disabled.INSTANCE
|
|
||||||
}
|
|
||||||
|
|
||||||
when (actionComponent) {
|
|
||||||
is ButtonComponent ->
|
|
||||||
ButtonMessageComponentKt.mergeToMessageComponent(actionComponent, index, comState, componentStoreState)
|
|
||||||
is SelectComponent ->
|
|
||||||
SelectMessageComponentKt.mergeToMessageComponent(actionComponent, index, comState, componentStoreState)
|
|
||||||
is SelectV2Component ->
|
|
||||||
SelectV2MessageComponent.mergeToMessageComponent(actionComponent, index, comState, componentStoreState)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.after<Message>("shouldShowReplyPreviewAsAttachment") { param ->
|
|
||||||
if (this.isComponentV2) param.result = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop(context: Context) {
|
|
||||||
patcher.unpatchAll()
|
|
||||||
stopCompat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2
|
|
||||||
|
|
||||||
import com.discord.api.channel.Channel
|
|
||||||
import com.discord.api.role.GuildRole
|
|
||||||
import com.discord.models.botuikit.MessageComponent
|
|
||||||
import com.discord.models.member.GuildMember
|
|
||||||
import com.discord.models.message.Message
|
|
||||||
import com.discord.stores.StoreMessageState
|
|
||||||
import com.discord.widgets.chat.list.entries.BotUiComponentEntry
|
|
||||||
|
|
||||||
@Suppress("EqualsOrHashCode")
|
|
||||||
class BotUiComponentV2Entry(
|
|
||||||
message: Message, appId: Long, guildId: Long?, components: MutableList<out MessageComponent>,
|
|
||||||
private val v2Fields: V2Fields
|
|
||||||
) : BotUiComponentEntry(message, appId, guildId, components) {
|
|
||||||
data class V2Fields(
|
|
||||||
val state: StoreMessageState.State?,
|
|
||||||
val meId: Long,
|
|
||||||
val channel: Channel,
|
|
||||||
val guildMembers: Map<Long, GuildMember>,
|
|
||||||
val guildRoles: Map<Long, GuildRole>,
|
|
||||||
// val channelNames: Map<Long, String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromV1(entry: BotUiComponentEntry, fields: V2Fields) =
|
|
||||||
entry.run { BotUiComponentV2Entry(message, applicationId, guildId, messageComponents, fields) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val state get() = v2Fields.state
|
|
||||||
val meId get() = v2Fields.meId
|
|
||||||
val channel get() = v2Fields.channel
|
|
||||||
val guildMembers get() = v2Fields.guildMembers
|
|
||||||
val guildRoles get() = v2Fields.guildRoles
|
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
|
||||||
super.equals(other) && if (other is BotUiComponentV2Entry) this.v2Fields == other.v2Fields else true
|
|
||||||
|
|
||||||
override fun toString() =
|
|
||||||
"AliuV2" + super.toString() + "& " + v2Fields.toString()
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2
|
|
||||||
|
|
||||||
import com.aliucord.Logger
|
|
||||||
import com.discord.api.botuikit.*
|
|
||||||
|
|
||||||
// Values added by smali patch
|
|
||||||
object ComponentV2Type {
|
|
||||||
lateinit var USER_SELECT: ComponentType
|
|
||||||
lateinit var ROLE_SELECT: ComponentType
|
|
||||||
lateinit var MENTIONABLE_SELECT: ComponentType
|
|
||||||
lateinit var CHANNEL_SELECT: ComponentType
|
|
||||||
lateinit var SECTION: ComponentType
|
|
||||||
lateinit var TEXT_DISPLAY: ComponentType
|
|
||||||
lateinit var THUMBNAIL: ComponentType
|
|
||||||
lateinit var MEDIA_GALLERY: ComponentType
|
|
||||||
lateinit var FILE: ComponentType
|
|
||||||
lateinit var SEPARATOR: ComponentType
|
|
||||||
lateinit var CONTAINER: ComponentType
|
|
||||||
|
|
||||||
var newValues: Array<ComponentType>? = null
|
|
||||||
private var oldValues: Array<ComponentType>? = null
|
|
||||||
@Suppress("UNCHECKED_CAST", "UNUSED_CHANGED_VALUE")
|
|
||||||
fun make() {
|
|
||||||
if (oldValues != null)
|
|
||||||
return
|
|
||||||
oldValues = ComponentType.values()
|
|
||||||
|
|
||||||
val cls = ComponentType::class.java
|
|
||||||
val constructor = cls.declaredConstructors[0]
|
|
||||||
constructor.isAccessible = true
|
|
||||||
|
|
||||||
val field = cls.getDeclaredField("\$VALUES")
|
|
||||||
field.isAccessible = true
|
|
||||||
val values = ComponentType.values()
|
|
||||||
var nextIdx = values.size
|
|
||||||
|
|
||||||
USER_SELECT = constructor.newInstance("USER_SELECT", nextIdx++, 5, UserSelectComponent::class.java) as ComponentType
|
|
||||||
ROLE_SELECT = constructor.newInstance("ROLE_SELECT", nextIdx++, 6, RoleSelectComponent::class.java) as ComponentType
|
|
||||||
MENTIONABLE_SELECT = constructor.newInstance("MENTIONABLE_SELECT", nextIdx++, 7, MentionableSelectComponent::class.java) as ComponentType
|
|
||||||
CHANNEL_SELECT = constructor.newInstance("CHANNEL_SELECT", nextIdx++, 8, ChannelSelectComponent::class.java) as ComponentType
|
|
||||||
SECTION = constructor.newInstance("SECTION", nextIdx++, 9, SectionComponent::class.java) as ComponentType
|
|
||||||
TEXT_DISPLAY = constructor.newInstance("TEXT_DISPLAY", nextIdx++, 10, TextDisplayComponent::class.java) as ComponentType
|
|
||||||
THUMBNAIL = constructor.newInstance("THUMBNAIL", nextIdx++, 11, ThumbnailComponent::class.java) as ComponentType
|
|
||||||
MEDIA_GALLERY = constructor.newInstance("MEDIA_GALLERY", nextIdx++, 12, MediaGalleryComponent::class.java) as ComponentType
|
|
||||||
FILE = constructor.newInstance("FILE", nextIdx++, 13, FileComponent::class.java) as ComponentType
|
|
||||||
SEPARATOR = constructor.newInstance("SEPARATOR", nextIdx++, 14, SeparatorComponent::class.java) as ComponentType
|
|
||||||
CONTAINER = constructor.newInstance("CONTAINER", nextIdx++, 17, ContainerComponent::class.java) as ComponentType
|
|
||||||
|
|
||||||
newValues = arrayOf(USER_SELECT, ROLE_SELECT, MENTIONABLE_SELECT, CHANNEL_SELECT, SECTION, TEXT_DISPLAY, THUMBNAIL, MEDIA_GALLERY, FILE, SEPARATOR, CONTAINER)
|
|
||||||
field.set(null, values + newValues!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unmake(logger: Logger) {
|
|
||||||
if (oldValues == null)
|
|
||||||
return logger.error("No unpatched component types?", null)
|
|
||||||
|
|
||||||
val cls = ComponentType::class.java
|
|
||||||
val field = cls.getDeclaredField("\$VALUES")
|
|
||||||
field.isAccessible = true
|
|
||||||
field.set(null, oldValues)
|
|
||||||
oldValues = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2
|
|
||||||
|
|
||||||
import com.aliucord.api.PatcherAPI
|
|
||||||
import com.aliucord.coreplugins.isComponentV2
|
|
||||||
import com.aliucord.patcher.*
|
|
||||||
import com.discord.api.channel.Channel
|
|
||||||
import com.discord.api.role.GuildRole
|
|
||||||
import com.discord.models.member.GuildMember
|
|
||||||
import com.discord.models.message.Message
|
|
||||||
import com.discord.stores.StoreMessageReplies.MessageState
|
|
||||||
import com.discord.stores.StoreMessageState
|
|
||||||
import com.discord.stores.StoreThreadMessages
|
|
||||||
import com.discord.widgets.chat.list.entries.BotUiComponentEntry
|
|
||||||
import com.discord.widgets.chat.list.entries.ChatListEntry
|
|
||||||
import com.discord.widgets.chat.list.model.WidgetChatListModelMessages
|
|
||||||
|
|
||||||
fun patchMessageItems(patcher: PatcherAPI) {
|
|
||||||
@Suppress("UNUSED_DESTRUCTURED_PARAMETER_ENTRY", "LocalVariableName", "UnusedVariable")
|
|
||||||
patcher.patch(WidgetChatListModelMessages.Companion::class.java.declaredMethods.find { it.name == "getMessageItems" }!!)
|
|
||||||
{(
|
|
||||||
param,
|
|
||||||
channel: Channel,
|
|
||||||
guildMembers: Map<Long, GuildMember>,
|
|
||||||
guildRoles: Map<Long, GuildRole>,
|
|
||||||
_blockedRelationships: Map<Long, Int>?,
|
|
||||||
_referencedChannel: Channel?,
|
|
||||||
_threadStoreState: StoreThreadMessages.ThreadState?,
|
|
||||||
_message: Message,
|
|
||||||
state: StoreMessageState.State?,
|
|
||||||
_repliedMessages: Map<Long, MessageState>?,
|
|
||||||
_isBlockedExpanded: Boolean,
|
|
||||||
_isMinimal: Boolean,
|
|
||||||
) ->
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val result = (param.result as MutableList<ChatListEntry>)
|
|
||||||
val meId = param.args[15] as Long
|
|
||||||
result.forEachIndexed { index, entry ->
|
|
||||||
if (entry is BotUiComponentEntry && entry.message.isComponentV2) {
|
|
||||||
val fields = BotUiComponentV2Entry.V2Fields(state, meId, channel, guildMembers, guildRoles)
|
|
||||||
result[index] = BotUiComponentV2Entry.fromV1(entry, fields)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.models
|
|
||||||
|
|
||||||
import com.discord.api.botuikit.ComponentType
|
|
||||||
import com.discord.api.botuikit.ContainerComponent
|
|
||||||
import com.discord.models.botuikit.MessageComponent
|
|
||||||
|
|
||||||
data class ContainerMessageComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
private val index: Int,
|
|
||||||
|
|
||||||
override val id: Int,
|
|
||||||
val components: List<MessageComponent>,
|
|
||||||
val accentColor: Int?,
|
|
||||||
override val spoiler: Boolean,
|
|
||||||
) : SpoilableMessageComponent {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun getIndex() = index
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mergeToMessageComponent(
|
|
||||||
component: ContainerComponent,
|
|
||||||
index: Int,
|
|
||||||
components: List<MessageComponent>,
|
|
||||||
): ContainerMessageComponent {
|
|
||||||
components.forEach {
|
|
||||||
if (it is MediaGalleryMessageComponent)
|
|
||||||
it.markedContained = true
|
|
||||||
}
|
|
||||||
return component.run {
|
|
||||||
ContainerMessageComponent(
|
|
||||||
type,
|
|
||||||
index,
|
|
||||||
id,
|
|
||||||
components,
|
|
||||||
accentColor,
|
|
||||||
spoiler,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.models
|
|
||||||
|
|
||||||
import com.discord.api.botuikit.*
|
|
||||||
import com.discord.models.botuikit.MessageComponent
|
|
||||||
|
|
||||||
data class MediaGalleryMessageComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
private val index: Int,
|
|
||||||
|
|
||||||
val id: Int,
|
|
||||||
val items: List<MediaGalleryItem>,
|
|
||||||
// Set by ContainerComponentView to tell MediaGalleryComponentView it is contained
|
|
||||||
var markedContained: Boolean = false,
|
|
||||||
) : MessageComponent {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun getIndex() = index
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mergeToMessageComponent(
|
|
||||||
component: MediaGalleryComponent,
|
|
||||||
index: Int
|
|
||||||
): MediaGalleryMessageComponent {
|
|
||||||
return component.run {
|
|
||||||
MediaGalleryMessageComponent(
|
|
||||||
type,
|
|
||||||
index,
|
|
||||||
id,
|
|
||||||
items,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.models
|
|
||||||
|
|
||||||
import com.discord.api.botuikit.ComponentType
|
|
||||||
import com.discord.api.botuikit.SectionComponent
|
|
||||||
import com.discord.models.botuikit.MessageComponent
|
|
||||||
|
|
||||||
data class SectionMessageComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
private val index: Int,
|
|
||||||
|
|
||||||
val id: Int,
|
|
||||||
val components: List<MessageComponent>,
|
|
||||||
val accessory: MessageComponent?,
|
|
||||||
) : MessageComponent {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun getIndex() = index
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mergeToMessageComponent(
|
|
||||||
component: SectionComponent,
|
|
||||||
index: Int,
|
|
||||||
components: List<MessageComponent>,
|
|
||||||
): SectionMessageComponent {
|
|
||||||
return component.run {
|
|
||||||
val realComponents = components.toMutableList()
|
|
||||||
val accessory = realComponents.removeAt(realComponents.lastIndex)
|
|
||||||
SectionMessageComponent(
|
|
||||||
type,
|
|
||||||
index,
|
|
||||||
id,
|
|
||||||
realComponents,
|
|
||||||
accessory,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.models
|
|
||||||
|
|
||||||
import com.discord.api.botuikit.*
|
|
||||||
import com.discord.models.botuikit.ActionInteractionComponentState
|
|
||||||
import com.discord.models.botuikit.ActionMessageComponent
|
|
||||||
import com.discord.widgets.botuikit.ComponentChatListState
|
|
||||||
|
|
||||||
data class SelectV2MessageComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
private val index: Int,
|
|
||||||
private val stateInteraction: ActionInteractionComponentState,
|
|
||||||
|
|
||||||
val id: Int,
|
|
||||||
val customId: String,
|
|
||||||
val placeholder: String,
|
|
||||||
val minValues: Int,
|
|
||||||
val maxValues: Int,
|
|
||||||
val defaultValues: List<SelectV2DefaultValue>,
|
|
||||||
val emojiAnimationsEnabled: Boolean,
|
|
||||||
) : ActionMessageComponent() {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun getIndex() = index
|
|
||||||
override fun getStateInteraction() = stateInteraction
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mergeToMessageComponent(
|
|
||||||
selectComponent: SelectV2Component,
|
|
||||||
index: Int,
|
|
||||||
stateInteraction: ActionInteractionComponentState,
|
|
||||||
componentStoreState: ComponentChatListState.ComponentStoreState
|
|
||||||
): SelectV2MessageComponent {
|
|
||||||
return selectComponent.run {
|
|
||||||
SelectV2MessageComponent(
|
|
||||||
type,
|
|
||||||
index,
|
|
||||||
stateInteraction,
|
|
||||||
id,
|
|
||||||
customId,
|
|
||||||
placeholder,
|
|
||||||
minValues,
|
|
||||||
maxValues,
|
|
||||||
defaultValues ?: listOf(),
|
|
||||||
componentStoreState.animateEmojis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.models
|
|
||||||
|
|
||||||
import com.discord.api.botuikit.ComponentType
|
|
||||||
import com.discord.api.botuikit.SeparatorComponent
|
|
||||||
import com.discord.models.botuikit.MessageComponent
|
|
||||||
|
|
||||||
data class SeparatorMessageComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
private val index: Int,
|
|
||||||
|
|
||||||
val divider: Boolean,
|
|
||||||
val spacing: Int, // 1 = small padding, 2 = large padding
|
|
||||||
) : MessageComponent {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun getIndex() = index
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mergeToMessageComponent(
|
|
||||||
component: SeparatorComponent,
|
|
||||||
index: Int
|
|
||||||
): SeparatorMessageComponent {
|
|
||||||
return component.run {
|
|
||||||
SeparatorMessageComponent(
|
|
||||||
type,
|
|
||||||
index,
|
|
||||||
divider,
|
|
||||||
spacing,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.models
|
|
||||||
|
|
||||||
import com.discord.models.botuikit.MessageComponent
|
|
||||||
|
|
||||||
interface SpoilableMessageComponent : MessageComponent {
|
|
||||||
val id: Int
|
|
||||||
val spoiler: Boolean
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.models
|
|
||||||
|
|
||||||
import com.discord.api.botuikit.ComponentType
|
|
||||||
import com.discord.api.botuikit.TextDisplayComponent
|
|
||||||
import com.discord.models.botuikit.MessageComponent
|
|
||||||
|
|
||||||
data class TextDisplayMessageComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
private val index: Int,
|
|
||||||
|
|
||||||
val id: Int,
|
|
||||||
val content: String,
|
|
||||||
) : MessageComponent {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun getIndex() = index
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mergeToMessageComponent(
|
|
||||||
component: TextDisplayComponent,
|
|
||||||
index: Int
|
|
||||||
): TextDisplayMessageComponent {
|
|
||||||
return component.run {
|
|
||||||
TextDisplayMessageComponent(
|
|
||||||
type,
|
|
||||||
index,
|
|
||||||
id,
|
|
||||||
content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.models
|
|
||||||
|
|
||||||
import com.discord.api.botuikit.*
|
|
||||||
|
|
||||||
data class ThumbnailMessageComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
private val index: Int,
|
|
||||||
|
|
||||||
override val id: Int,
|
|
||||||
val media: UnfurledMediaItem,
|
|
||||||
val description: String?,
|
|
||||||
override val spoiler: Boolean,
|
|
||||||
) : SpoilableMessageComponent {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun getIndex() = index
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mergeToMessageComponent(
|
|
||||||
component: ThumbnailComponent,
|
|
||||||
index: Int
|
|
||||||
): ThumbnailMessageComponent {
|
|
||||||
return component.run {
|
|
||||||
ThumbnailMessageComponent(
|
|
||||||
type,
|
|
||||||
index,
|
|
||||||
id,
|
|
||||||
media,
|
|
||||||
description,
|
|
||||||
spoiler,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
|
||||||
import com.aliucord.Utils
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.SelectV2MessageComponent
|
|
||||||
import com.discord.app.AppBottomSheet
|
|
||||||
import com.discord.utilities.view.extensions.ViewExtensions
|
|
||||||
import com.discord.utilities.view.recycler.MaxHeightRecyclerView
|
|
||||||
import com.discord.widgets.botuikit.views.select.`SelectComponentBottomSheet$binding$2`
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
import b.a.k.b as FormatUtils
|
|
||||||
|
|
||||||
internal class SelectSheet : AppBottomSheet {
|
|
||||||
val entry: BotUiComponentV2Entry?
|
|
||||||
val component: SelectV2MessageComponent?
|
|
||||||
|
|
||||||
private lateinit var header: ConstraintLayout
|
|
||||||
private lateinit var placeholder: TextView
|
|
||||||
private lateinit var recycler: MaxHeightRecyclerView
|
|
||||||
private lateinit var select: TextView
|
|
||||||
private lateinit var subtitle: TextView
|
|
||||||
|
|
||||||
private lateinit var adapter: SelectSheetAdapter
|
|
||||||
|
|
||||||
constructor(entry: BotUiComponentV2Entry, component: SelectV2MessageComponent) {
|
|
||||||
this.entry = entry
|
|
||||||
this.component = component
|
|
||||||
}
|
|
||||||
constructor() {
|
|
||||||
this.entry = null
|
|
||||||
this.component = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getContentViewResId() = Utils.getResId("widget_select_component_bottom_sheet", "layout")
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, bundle: Bundle?) {
|
|
||||||
super.onViewCreated(view, bundle)
|
|
||||||
|
|
||||||
val viewModel = ViewModelProvider(this).get(SelectSheetViewModel::class.java)
|
|
||||||
|
|
||||||
`SelectComponentBottomSheet$binding$2`.INSTANCE.invoke(view).run {
|
|
||||||
header = a
|
|
||||||
placeholder = b
|
|
||||||
recycler = c
|
|
||||||
select = d
|
|
||||||
subtitle = e
|
|
||||||
}
|
|
||||||
adapter = SelectSheetAdapter(recycler, viewModel)
|
|
||||||
recycler.adapter = adapter
|
|
||||||
(recycler.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
|
||||||
|
|
||||||
select.setOnClickListener { viewModel.submit() }
|
|
||||||
viewModel.onUpdate = ::configureUI
|
|
||||||
viewModel.onRequestDismiss = ::dismiss
|
|
||||||
if (entry != null && component != null)
|
|
||||||
viewModel.configure(entry, component)
|
|
||||||
else
|
|
||||||
viewModel.state?.let { configureUI(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureUI(state: SelectSheetViewModel.ViewState) {
|
|
||||||
placeholder.text = state.placeholder
|
|
||||||
subtitle.visibility = if (state.isMultiSelect) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
if (state.isMultiSelect) {
|
|
||||||
subtitle.text =
|
|
||||||
FormatUtils.k(
|
|
||||||
this,
|
|
||||||
R.h.message_select_component_select_requirement,
|
|
||||||
arrayOf(state.minSelections),
|
|
||||||
null,
|
|
||||||
4
|
|
||||||
)
|
|
||||||
}
|
|
||||||
select.visibility = if (state.isMultiSelect) View.VISIBLE else View.INVISIBLE
|
|
||||||
select.isClickable = state.isValidSelection
|
|
||||||
ViewExtensions.setEnabledAlpha(select, state.isValidSelection, 0.3f)
|
|
||||||
adapter.setData(state.items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.discord.utilities.mg_recycler.MGRecyclerAdapterSimple
|
|
||||||
import com.discord.utilities.mg_recycler.MGRecyclerViewHolder
|
|
||||||
|
|
||||||
internal class SelectSheetAdapter(recycler: RecyclerView, val viewModel: SelectSheetViewModel)
|
|
||||||
: MGRecyclerAdapterSimple<SelectSheetItem>(recycler) {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MGRecyclerViewHolder<*, SelectSheetItem> {
|
|
||||||
return SelectSheetItemViewHolder(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
|
||||||
|
|
||||||
import com.aliucord.wrappers.ChannelWrapper.Companion.id
|
|
||||||
import com.discord.api.channel.Channel
|
|
||||||
import com.discord.api.role.GuildRole
|
|
||||||
import com.discord.models.member.GuildMember
|
|
||||||
import com.discord.models.user.User
|
|
||||||
import com.discord.utilities.mg_recycler.MGRecyclerDataPayload
|
|
||||||
|
|
||||||
sealed class SelectSheetItem(
|
|
||||||
private val type: Int,
|
|
||||||
val id: Long,
|
|
||||||
) : MGRecyclerDataPayload {
|
|
||||||
override fun getKey() = id.toString()
|
|
||||||
override fun getType() = type
|
|
||||||
|
|
||||||
abstract val checked: Boolean
|
|
||||||
abstract val disabled: Boolean
|
|
||||||
abstract fun copy(checked: Boolean = this.checked, disabled: Boolean = this.disabled) : SelectSheetItem
|
|
||||||
|
|
||||||
internal data class UserSelectItem(
|
|
||||||
val user: User,
|
|
||||||
val member: GuildMember,
|
|
||||||
override val checked: Boolean,
|
|
||||||
override val disabled: Boolean = false,
|
|
||||||
) : SelectSheetItem(1, user.id) {
|
|
||||||
override fun copy(checked: Boolean, disabled: Boolean): SelectSheetItem = copy(checked = checked, disabled = disabled, user = user)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class RoleSelectItem(
|
|
||||||
val role: GuildRole,
|
|
||||||
override val checked: Boolean,
|
|
||||||
override val disabled: Boolean = false,
|
|
||||||
) : SelectSheetItem(2, role.id) {
|
|
||||||
override fun copy(checked: Boolean, disabled: Boolean): SelectSheetItem = copy(checked = checked, disabled = disabled, role = role)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class ChannelSelectItem(
|
|
||||||
val channel: Channel,
|
|
||||||
override val checked: Boolean,
|
|
||||||
override val disabled: Boolean = false,
|
|
||||||
) : SelectSheetItem(3, channel.id) {
|
|
||||||
override fun copy(checked: Boolean, disabled: Boolean): SelectSheetItem = copy(checked = checked, disabled = disabled, channel = channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.GONE
|
|
||||||
import android.view.View.VISIBLE
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.aliucord.Utils
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.wrappers.ChannelWrapper.Companion.name
|
|
||||||
import com.aliucord.wrappers.ChannelWrapper.Companion.type
|
|
||||||
import com.aliucord.wrappers.GuildRoleWrapper.Companion.name
|
|
||||||
import com.discord.api.channel.Channel
|
|
||||||
import com.discord.models.member.GuildMember
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.discord.utilities.drawable.DrawableCompat
|
|
||||||
import com.discord.utilities.guilds.RoleUtils
|
|
||||||
import com.discord.utilities.icon.IconUtils
|
|
||||||
import com.discord.utilities.images.MGImages
|
|
||||||
import com.discord.utilities.mg_recycler.MGRecyclerViewHolder
|
|
||||||
import com.discord.utilities.user.UserUtils
|
|
||||||
import com.discord.utilities.view.extensions.ViewExtensions
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView
|
|
||||||
import com.google.android.material.checkbox.MaterialCheckBox
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
internal class SelectSheetItemViewHolder(adapter: SelectSheetAdapter)
|
|
||||||
: MGRecyclerViewHolder<SelectSheetAdapter, SelectSheetItem>(Utils.getResId("widget_select_component_bottom_sheet_item", "layout"), adapter) {
|
|
||||||
|
|
||||||
private val description = itemView.findViewById<MaterialTextView>(Utils.getResId("select_component_sheet_item_description", "id"))!!
|
|
||||||
private val divider = itemView.findViewById<View>(Utils.getResId("select_component_sheet_item_divider", "id"))!!
|
|
||||||
private val dividerWithIcon = itemView.findViewById<View>(Utils.getResId("select_component_sheet_item_divider_icon", "id"))!!
|
|
||||||
private val icon = itemView.findViewById<SimpleDraweeView>(Utils.getResId("select_component_sheet_item_icon", "id"))!!
|
|
||||||
private val checkbox = itemView.findViewById<MaterialCheckBox>(Utils.getResId("select_component_sheet_item_selected", "id"))!!
|
|
||||||
private val title = itemView.findViewById<MaterialTextView>(Utils.getResId("select_component_sheet_item_title", "id"))!!
|
|
||||||
|
|
||||||
init {
|
|
||||||
(itemView as ConstraintLayout).minHeight = 62.dp
|
|
||||||
divider.visibility = View.GONE
|
|
||||||
dividerWithIcon.visibility = View.VISIBLE
|
|
||||||
dividerWithIcon.layoutParams = (dividerWithIcon.layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
marginStart = 56.dp
|
|
||||||
}
|
|
||||||
description.setPadding(0, 0, 0, 12.dp)
|
|
||||||
icon.visibility = View.VISIBLE
|
|
||||||
icon.layoutParams = (icon.layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
width = 24.dp
|
|
||||||
height = 24.dp
|
|
||||||
}
|
|
||||||
MGImages.setRoundingParams(
|
|
||||||
icon,
|
|
||||||
Float.MAX_VALUE,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigure(viewType: Int, item: SelectSheetItem) {
|
|
||||||
super.onConfigure(viewType, item)
|
|
||||||
|
|
||||||
description.visibility = View.GONE
|
|
||||||
checkbox.visibility = if (adapter.viewModel.state?.isMultiSelect == true) VISIBLE else GONE
|
|
||||||
checkbox.isChecked = item.checked
|
|
||||||
title.setPadding(0, 12.dp, 0, 12.dp)
|
|
||||||
|
|
||||||
when (item) {
|
|
||||||
is SelectSheetItem.ChannelSelectItem -> configureChannel(item)
|
|
||||||
is SelectSheetItem.RoleSelectItem -> configureRole(item)
|
|
||||||
is SelectSheetItem.UserSelectItem -> configureUser(item)
|
|
||||||
}
|
|
||||||
itemView.setOnClickListener { adapter.viewModel.onItemSelect(item) }
|
|
||||||
ViewExtensions.setEnabledAlpha(itemView, !item.disabled, 0.3f);
|
|
||||||
itemView.isEnabled = !item.disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureChannel(item: SelectSheetItem.ChannelSelectItem) {
|
|
||||||
title.text = "#${item.channel.name}"
|
|
||||||
val res = when (item.channel.type) {
|
|
||||||
Channel.GUILD_ANNOUNCEMENT -> R.e.ic_channel_announcements
|
|
||||||
Channel.GUILD_VOICE -> R.e.ic_channel_voice
|
|
||||||
Channel.CATEGORY -> DrawableCompat.getThemedDrawableRes(adapter.context, R.b.ic_category)
|
|
||||||
else -> DrawableCompat.getThemedDrawableRes(adapter.context, R.b.ic_channel_text)
|
|
||||||
}
|
|
||||||
icon.setImageResource(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureRole(item: SelectSheetItem.RoleSelectItem) {
|
|
||||||
title.text = item.role.name
|
|
||||||
|
|
||||||
val opaqueColor: Int = RoleUtils.getOpaqueColor(item.role, ColorCompat.getColor(adapter.context, R.c.status_grey_500))
|
|
||||||
icon.setImageResource(R.e.ic_role_24dp)
|
|
||||||
icon.setColorFilter(opaqueColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureUser(item: SelectSheetItem.UserSelectItem) {
|
|
||||||
IconUtils.`setIcon$default`(
|
|
||||||
icon,
|
|
||||||
item.user,
|
|
||||||
R.d.avatar_size_standard,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
item.member,
|
|
||||||
24,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
title.text = GuildMember.Companion!!.getNickOrUsername(item.member, item.user)
|
|
||||||
val descText = item.user.username + if (item.user.discriminator != 0)
|
|
||||||
UserUtils.INSTANCE.getDiscriminatorWithPadding(item.user)
|
|
||||||
else
|
|
||||||
""
|
|
||||||
if (title.text != descText) {
|
|
||||||
title.setPadding(0, 12.dp, 0, 0)
|
|
||||||
description.visibility = View.VISIBLE
|
|
||||||
description.text = descText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.selectsheet
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.SelectV2MessageComponent
|
|
||||||
import com.aliucord.wrappers.ChannelWrapper.Companion.id
|
|
||||||
import com.discord.api.botuikit.ComponentType
|
|
||||||
import com.discord.restapi.RestAPIParams.ComponentInteractionData.SelectComponentInteractionData
|
|
||||||
import com.discord.stores.StoreStream
|
|
||||||
|
|
||||||
const val ENTRY_LIMIT = 15
|
|
||||||
|
|
||||||
internal class SelectSheetViewModel() : ViewModel() {
|
|
||||||
data class ViewState(
|
|
||||||
val placeholder: String,
|
|
||||||
val items: List<SelectSheetItem>,
|
|
||||||
val isMultiSelect: Boolean,
|
|
||||||
val minSelections: Int,
|
|
||||||
val maxSelections: Int,
|
|
||||||
val isValidSelection: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class SubmissionData(
|
|
||||||
val applicationId: Long,
|
|
||||||
val guildId: Long?,
|
|
||||||
val channelId: Long,
|
|
||||||
val messageId: Long,
|
|
||||||
val messageFlags: Long,
|
|
||||||
val index: Int,
|
|
||||||
val customId: String,
|
|
||||||
val type: ComponentType,
|
|
||||||
)
|
|
||||||
|
|
||||||
var onUpdate: ((ViewState) -> Unit)? = null
|
|
||||||
var onRequestDismiss: (() -> Unit)? = null
|
|
||||||
|
|
||||||
var state: ViewState? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
value?.let { onUpdate?.invoke(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var submissionData: SubmissionData? = null
|
|
||||||
|
|
||||||
fun configure(entry: BotUiComponentV2Entry, component: SelectV2MessageComponent) {
|
|
||||||
var entryCount = 0
|
|
||||||
val items = mutableListOf<SelectSheetItem>()
|
|
||||||
val users = StoreStream.getUsers().users
|
|
||||||
if (component.type in listOf(ComponentV2Type.USER_SELECT, ComponentV2Type.MENTIONABLE_SELECT)) {
|
|
||||||
for (member in entry.guildMembers.values) {
|
|
||||||
entryCount += 1
|
|
||||||
if (entryCount > ENTRY_LIMIT)
|
|
||||||
break
|
|
||||||
val user = users[member.userId]!!
|
|
||||||
val isDefault = component.defaultValues.any { it.id == member.userId }
|
|
||||||
items.add(SelectSheetItem.UserSelectItem(user, member, isDefault))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (component.type in listOf(ComponentV2Type.ROLE_SELECT, ComponentV2Type.MENTIONABLE_SELECT)) {
|
|
||||||
for (role in entry.guildRoles.values) {
|
|
||||||
entryCount += 1
|
|
||||||
if (entryCount > ENTRY_LIMIT)
|
|
||||||
break
|
|
||||||
val isDefault = component.defaultValues.any { it.id == role.id }
|
|
||||||
items.add(SelectSheetItem.RoleSelectItem(role, isDefault))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: is the guildID check needed? as in, can server side allow this component?
|
|
||||||
if (component.type == ComponentV2Type.CHANNEL_SELECT && entry.guildId != null) {
|
|
||||||
val channels = StoreStream.getChannels().getChannelsForGuild(entry.guildId!!)!!
|
|
||||||
for (channel in channels.values) {
|
|
||||||
entryCount += 1
|
|
||||||
if (entryCount > ENTRY_LIMIT)
|
|
||||||
break
|
|
||||||
val isDefault = component.defaultValues.any { it.id == channel.id }
|
|
||||||
items.add(SelectSheetItem.ChannelSelectItem(channel, isDefault))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val min = component.minValues
|
|
||||||
val max = component.maxValues
|
|
||||||
state = ViewState(
|
|
||||||
component.placeholder,
|
|
||||||
items,
|
|
||||||
isMultiSelect = max > 1,
|
|
||||||
minSelections = min,
|
|
||||||
maxSelections = max,
|
|
||||||
isValidSelection = false,
|
|
||||||
)
|
|
||||||
submissionData = SubmissionData(
|
|
||||||
entry.applicationId,
|
|
||||||
entry.guildId,
|
|
||||||
entry.message.channelId,
|
|
||||||
entry.message.id,
|
|
||||||
entry.message.flags,
|
|
||||||
component.index,
|
|
||||||
component.customId,
|
|
||||||
component.type,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onItemSelect(item: SelectSheetItem) {
|
|
||||||
val state = state ?: return
|
|
||||||
var checkedCount = 0
|
|
||||||
var newItems = state.items.map {
|
|
||||||
val res = if (it == item)
|
|
||||||
item.copy(checked = !item.checked)
|
|
||||||
else
|
|
||||||
it
|
|
||||||
if (res.checked)
|
|
||||||
checkedCount += 1
|
|
||||||
res
|
|
||||||
}
|
|
||||||
val isMaxed = checkedCount == state.maxSelections
|
|
||||||
newItems = newItems.map {
|
|
||||||
it.copy(disabled = isMaxed && !it.checked)
|
|
||||||
}
|
|
||||||
this.state = state.copy(
|
|
||||||
items = newItems,
|
|
||||||
isValidSelection = checkedCount in state.minSelections..state.maxSelections
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!state.isMultiSelect)
|
|
||||||
submit()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun submit() {
|
|
||||||
// val companion = StoreStream.Companion
|
|
||||||
// companion.localActionComponentState.setSelectComponentSelection(this.componentContext.getMessageId(), this.componentIndex, u.toList(set))
|
|
||||||
val state = state ?: return
|
|
||||||
val submissionData = submissionData ?: return
|
|
||||||
|
|
||||||
val selected = state.items.filter { it.checked }.map { it.id.toString() }
|
|
||||||
submissionData.run {
|
|
||||||
StoreStream.getInteractions().sendComponentInteraction(
|
|
||||||
applicationId,
|
|
||||||
guildId,
|
|
||||||
channelId,
|
|
||||||
messageId,
|
|
||||||
index,
|
|
||||||
SelectComponentInteractionData(
|
|
||||||
type,
|
|
||||||
customId,
|
|
||||||
selected,
|
|
||||||
),
|
|
||||||
messageFlags
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onRequestDismiss?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import com.aliucord.Logger
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.ContainerMessageComponent
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.aliucord.widgets.LinearLayout
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.discord.widgets.botuikit.ComponentProvider
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentView
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRowKt
|
|
||||||
import com.google.android.material.card.MaterialCardView
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
|
|
||||||
class ContainerComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<ContainerMessageComponent> {
|
|
||||||
override fun type() = ComponentV2Type.CONTAINER
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val accentDividerId = View.generateViewId()
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var accentDivider: View
|
|
||||||
private lateinit var contentView: LinearLayout
|
|
||||||
private lateinit var spoilerView: SpoilerView
|
|
||||||
|
|
||||||
init {
|
|
||||||
MaterialCardView(ctx).addTo(this) {
|
|
||||||
radius = 8.dp.toFloat()
|
|
||||||
elevation = 0f
|
|
||||||
setCardBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.colorBackgroundSecondary))
|
|
||||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
bottomToBottom = PARENT_ID
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
}
|
|
||||||
ConstraintLayout(ctx).addTo(this) {
|
|
||||||
accentDivider = View(ctx).addTo(this) {
|
|
||||||
id = accentDividerId
|
|
||||||
layoutParams = LayoutParams(3.dp, 0).apply {
|
|
||||||
bottomToBottom = PARENT_ID
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contentView = LinearLayout(ctx).addTo(this) {
|
|
||||||
setPadding(8.dp, 8.dp, 8.dp, 8.dp)
|
|
||||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
|
||||||
startToEnd = accentDividerId
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
constrainedWidth = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
spoilerView = SpoilerView(ctx, 1).addTo(this) {
|
|
||||||
layoutParams = SpoilerView.constraintLayoutParamsAround(PARENT_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configure(component: ContainerMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
|
||||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
|
||||||
val entry = item.entry
|
|
||||||
if (entry !is BotUiComponentV2Entry) {
|
|
||||||
Logger("ComponentsV2").warn("configured container with non-v2 entry")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val configuredViews = component.components.mapIndexed { index, child ->
|
|
||||||
provider.getConfiguredComponentView(listener, child, contentView, index)
|
|
||||||
}.filterNotNull()
|
|
||||||
WidgetChatListAdapterItemBotComponentRowKt.replaceViews(contentView, configuredViews)
|
|
||||||
|
|
||||||
val color = component.accentColor?.let { ColorUtils.setAlphaComponent(it, 255) }
|
|
||||||
?: ColorCompat.getThemedColor(context, R.b.colorBackgroundModifierAccent)
|
|
||||||
accentDivider.setBackgroundColor(color)
|
|
||||||
|
|
||||||
spoilerView.configure(entry, component)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.componentsv2.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
|
||||||
import com.aliucord.Logger
|
|
||||||
import com.aliucord.coreplugins.CV2Compat
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.MediaGalleryMessageComponent
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.aliucord.widgets.LinearLayout
|
|
||||||
import com.aliucord.wrappers.messages.AttachmentWrapper.Companion.height
|
|
||||||
import com.aliucord.wrappers.messages.AttachmentWrapper.Companion.width
|
|
||||||
import com.discord.api.message.attachment.MessageAttachment
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.discord.utilities.display.DisplayUtils
|
|
||||||
import com.discord.utilities.embed.EmbedResourceUtils
|
|
||||||
import com.discord.widgets.botuikit.ComponentProvider
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentView
|
|
||||||
import com.discord.widgets.chat.list.InlineMediaView
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
|
||||||
import com.discord.widgets.media.WidgetMedia
|
|
||||||
import com.google.android.material.card.MaterialCardView
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
|
|
||||||
class MediaGalleryComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<MediaGalleryMessageComponent> {
|
|
||||||
override fun type() = ComponentV2Type.MEDIA_GALLERY
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val mediaViewId = View.generateViewId()
|
|
||||||
private val maxEmbedHeight = EmbedResourceUtils.INSTANCE.maX_IMAGE_VIEW_HEIGHT_PX
|
|
||||||
}
|
|
||||||
|
|
||||||
private val layout = LinearLayout(ctx).addTo(this) {
|
|
||||||
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var mediaViews: List<Pair<MessageAttachment, InlineMediaView>>? = null
|
|
||||||
|
|
||||||
// This isn't pretty, but Discord actually does this in their code (EmbedResourceUtils.computeMaximumImageWidthPx)
|
|
||||||
private fun calculateMaxWidth(contained: Boolean): Int {
|
|
||||||
var maxPossibleWidth = DisplayUtils.getScreenSize(context).width() -
|
|
||||||
resources.getDimensionPixelSize(R.d.uikit_guideline_chat) -
|
|
||||||
resources.getDimensionPixelSize(R.d.chat_cell_horizontal_spacing_total)
|
|
||||||
|
|
||||||
if (contained)
|
|
||||||
maxPossibleWidth -= 15.dp
|
|
||||||
|
|
||||||
return maxPossibleWidth.coerceAtMost(1440)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference: WidgetChatListAdapterItemAttachment.configureUI
|
|
||||||
override fun configure(component: MediaGalleryMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
|
||||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
|
||||||
val entry = item.entry
|
|
||||||
if (entry !is BotUiComponentV2Entry) {
|
|
||||||
Logger("ComponentsV2").warn("configured media gallery with non-v2 entry")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val maxEmbedWidth = calculateMaxWidth(component.markedContained)
|
|
||||||
layout.removeAllViews()
|
|
||||||
val pendingViews = mutableListOf<Pair<MessageAttachment, InlineMediaView>>()
|
|
||||||
component.items.forEachIndexed { index, it ->
|
|
||||||
val media = it.media
|
|
||||||
// TODO: there's probably a utility to extract filename from url
|
|
||||||
val name = media.url.split("/").last().split("?").first()
|
|
||||||
val attachment = CV2Compat.createAttachment(
|
|
||||||
name,
|
|
||||||
0,
|
|
||||||
media.proxyUrl,
|
|
||||||
media.url,
|
|
||||||
media.width,
|
|
||||||
media.height,
|
|
||||||
)
|
|
||||||
|
|
||||||
val (width, height) = EmbedResourceUtils.INSTANCE.calculateScaledSize(
|
|
||||||
attachment.width!!,
|
|
||||||
attachment.height!!,
|
|
||||||
maxEmbedWidth,
|
|
||||||
maxEmbedHeight,
|
|
||||||
resources,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
MaterialCardView(context).addTo(layout) {
|
|
||||||
radius = 8.dp.toFloat()
|
|
||||||
elevation = 0f
|
|
||||||
setCardBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorBackgroundPrimary))
|
|
||||||
layoutParams = android.widget.LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
|
||||||
topMargin = 8.dp
|
|
||||||
}
|
|
||||||
ConstraintLayout(context).addTo(this) {
|
|
||||||
layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
|
||||||
val mediaView = InlineMediaView(context).addTo(this) {
|
|
||||||
radius = 8.dp.toFloat()
|
|
||||||
elevation = 0f
|
|
||||||
setCardBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorBackgroundPrimary))
|
|
||||||
id = mediaViewId
|
|
||||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
}
|
|
||||||
setOnClickListener {
|
|
||||||
WidgetMedia.Companion!!.launch(context, attachment);
|
|
||||||
}
|
|
||||||
updateUIWithAttachment(attachment, width, height, true)
|
|
||||||
}
|
|
||||||
val spoilerView = SpoilerView(context, 1).addTo(this) {
|
|
||||||
translationZ = 10f
|
|
||||||
layoutParams = SpoilerView.constraintLayoutParamsAround(mediaViewId)
|
|
||||||
}
|
|
||||||
pendingViews.add(attachment to mediaView)
|
|
||||||
spoilerView.configure(it.spoiler, entry.state, entry.message.id, Pair(component.id, "media:$index"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediaViews = pendingViews.toList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.componentsv2.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.SectionMessageComponent
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.aliucord.widgets.LinearLayout
|
|
||||||
import com.discord.widgets.botuikit.ComponentProvider
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentView
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRowKt
|
|
||||||
|
|
||||||
class SectionComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<SectionMessageComponent> {
|
|
||||||
override fun type() = ComponentV2Type.SECTION
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val accessoryViewId = View.generateViewId()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mainView = LinearLayout(ctx).addTo(this) {
|
|
||||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
|
||||||
constrainedWidth = true
|
|
||||||
horizontalBias = 0f
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
endToStart = accessoryViewId
|
|
||||||
marginEnd = 16.dp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var accessoryView = FrameLayout(ctx).addTo(this) {
|
|
||||||
id = accessoryViewId
|
|
||||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configure(component: SectionMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
|
||||||
val configuredViews = component.components.mapIndexed { index, child ->
|
|
||||||
provider.getConfiguredComponentView(listener, child, mainView, index)
|
|
||||||
}.filterNotNull()
|
|
||||||
WidgetChatListAdapterItemBotComponentRowKt.replaceViews(mainView, configuredViews)
|
|
||||||
|
|
||||||
val accessoryComponent = provider.getConfiguredComponentView(listener, component.accessory, accessoryView, 0)
|
|
||||||
accessoryComponent?.let {
|
|
||||||
WidgetChatListAdapterItemBotComponentRowKt.replaceViews(accessoryView, listOf(accessoryComponent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.componentsv2.views
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.aliucord.Logger
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.SelectV2MessageComponent
|
|
||||||
import com.aliucord.coreplugins.componentsv2.selectsheet.SelectSheet
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.discord.api.botuikit.ComponentType
|
|
||||||
import com.discord.models.botuikit.SelectMessageComponent
|
|
||||||
import com.discord.views.typing.TypingDots
|
|
||||||
import com.discord.widgets.botuikit.ComponentProvider
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentView
|
|
||||||
import com.discord.widgets.botuikit.views.select.SelectComponentView
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView
|
|
||||||
import com.google.android.flexbox.FlexboxLayout
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
|
||||||
internal class SelectV2ComponentView(context: Context, private val type: ComponentType)
|
|
||||||
: ConstraintLayout(context), ComponentView<SelectV2MessageComponent> {
|
|
||||||
override fun type(): ComponentType = type
|
|
||||||
|
|
||||||
private val componentView: SelectComponentView
|
|
||||||
private val chevron: ImageView
|
|
||||||
private val loadingDots: TypingDots
|
|
||||||
private val selectionIcon: SimpleDraweeView
|
|
||||||
private val selectionText: MaterialTextView
|
|
||||||
private val selectionsRoot: FlexboxLayout
|
|
||||||
|
|
||||||
init {
|
|
||||||
val view = SelectComponentView.Companion!!.inflateComponent(context, this).addTo(this)
|
|
||||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
||||||
maxWidth = view.maxWidth
|
|
||||||
b.a.i.b5.a(view).run {
|
|
||||||
componentView = a
|
|
||||||
chevron = b
|
|
||||||
loadingDots = c
|
|
||||||
selectionIcon = d
|
|
||||||
selectionText = e
|
|
||||||
selectionsRoot = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configure(
|
|
||||||
component: SelectV2MessageComponent,
|
|
||||||
provider: ComponentProvider,
|
|
||||||
listener: ComponentActionListener,
|
|
||||||
) {
|
|
||||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
|
||||||
val entry = item.entry
|
|
||||||
if (entry !is BotUiComponentV2Entry) {
|
|
||||||
Logger("ComponentsV2").warn("configured v2 select with non-v2 entry")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val proxyComponent = component.run {
|
|
||||||
SelectMessageComponent(
|
|
||||||
type,
|
|
||||||
index,
|
|
||||||
stateInteraction,
|
|
||||||
customId,
|
|
||||||
placeholder,
|
|
||||||
minValues,
|
|
||||||
maxValues,
|
|
||||||
listOf(),
|
|
||||||
listOf(),
|
|
||||||
emojiAnimationsEnabled,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentView.configure(proxyComponent, provider, listener)
|
|
||||||
componentView.setOnClickListener {
|
|
||||||
val sh = SelectSheet(entry, component)
|
|
||||||
sh.show(item.adapter.fragmentManager, SelectSheet::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.componentsv2.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.aliucord.Logger
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.SeparatorMessageComponent
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.aliucord.views.Divider
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.discord.widgets.botuikit.ComponentProvider
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentView
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
|
|
||||||
class SeparatorComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<SeparatorMessageComponent> {
|
|
||||||
override fun type() = ComponentV2Type.SEPARATOR
|
|
||||||
|
|
||||||
private val divider = Divider(ctx).addTo(this) {
|
|
||||||
setBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorTextMuted));
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configure(component: SeparatorMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
|
||||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
|
||||||
val entry = item.entry
|
|
||||||
if (entry !is BotUiComponentV2Entry) {
|
|
||||||
Logger("ComponentsV2").warn("configured separator with non-v2 entry")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
divider.visibility = if (component.divider) VISIBLE else INVISIBLE
|
|
||||||
divider.layoutParams = (divider.layoutParams as LayoutParams).apply {
|
|
||||||
val padding = 6.dp * component.spacing
|
|
||||||
setPadding(paddingLeft, padding, paddingRight, padding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
package com.aliucord.coreplugins.componentsv2.views
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import android.widget.*
|
|
||||||
import androidx.cardview.widget.CardView
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.SpoilableMessageComponent
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.discord.stores.StoreMessageState
|
|
||||||
import com.discord.stores.StoreStream
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A view that can be spoilered.
|
|
||||||
*
|
|
||||||
* @param ctx Context
|
|
||||||
* @param type 1 for full (spoiler text and button), 2 for mini (eye icon)
|
|
||||||
*/
|
|
||||||
@SuppressLint("ViewConstructor")
|
|
||||||
internal class SpoilerView(ctx: Context, type: Int) : ConstraintLayout(ctx) {
|
|
||||||
companion object {
|
|
||||||
fun constraintLayoutParamsAround(viewId: Int) =
|
|
||||||
LayoutParams(0, 0).apply {
|
|
||||||
topToTop = viewId
|
|
||||||
bottomToBottom = viewId
|
|
||||||
startToStart = viewId
|
|
||||||
endToEnd = viewId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val spoilerView = ConstraintLayout(ctx).addTo(this) {
|
|
||||||
visibility = GONE
|
|
||||||
setBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.theme_chat_spoiler_bg))
|
|
||||||
layoutParams = LayoutParams(0, 0).apply {
|
|
||||||
bottomToBottom = PARENT_ID
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
}
|
|
||||||
isClickable = true
|
|
||||||
|
|
||||||
when (type) {
|
|
||||||
1 -> {
|
|
||||||
CardView(ctx).addTo(this) {
|
|
||||||
elevation = ctx.resources.getDimension(R.d.app_elevation)
|
|
||||||
setCardBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.colorBackgroundFloating))
|
|
||||||
radius = 16.dp.toFloat()
|
|
||||||
|
|
||||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
bottomToBottom = PARENT_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
TextView(ctx, null, 0, R.i.UiKit_TextView_H2).addTo(this) {
|
|
||||||
setText(R.h.spoiler)
|
|
||||||
isAllCaps = true
|
|
||||||
setPadding(8.dp, 4.dp, 8.dp, 4.dp)
|
|
||||||
setTextColor(ColorCompat.getThemedColor(ctx, R.b.colorTextNormal))
|
|
||||||
layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
|
||||||
marginStart = 4.dp
|
|
||||||
marginEnd = 4.dp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
ImageView(ctx).addTo(this) {
|
|
||||||
setImageResource(R.e.ic_spoiler)
|
|
||||||
layoutParams = LayoutParams(0, 0).apply {
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
bottomToBottom = PARENT_ID
|
|
||||||
dimensionRatio = "1:1"
|
|
||||||
matchConstraintPercentWidth = 0.5f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException("Invalid spoiler view type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun configure(entry: BotUiComponentV2Entry, component: SpoilableMessageComponent, key: String? = null) {
|
|
||||||
configure(component.spoiler, entry.state, entry.message.id, Pair(component.id, key))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun configure(
|
|
||||||
isSpoiler: Boolean,
|
|
||||||
state: StoreMessageState.State?,
|
|
||||||
messageId: Long,
|
|
||||||
key: Pair<Int, String?>,
|
|
||||||
) {
|
|
||||||
val (id, strKey) = key
|
|
||||||
val spoiled = if (strKey != null)
|
|
||||||
state?.visibleSpoilerEmbedMap?.get(id)?.contains(strKey) ?: false
|
|
||||||
else
|
|
||||||
state?.visibleSpoilerEmbedMap?.containsKey(id) ?: false
|
|
||||||
|
|
||||||
spoilerView.setOnClickListener {
|
|
||||||
spoilerView.setOnClickListener(null)
|
|
||||||
spoilerView.animate()
|
|
||||||
.withEndAction {
|
|
||||||
if (strKey != null)
|
|
||||||
StoreStream.getMessageState().revealSpoilerEmbedData(messageId, id, strKey)
|
|
||||||
else
|
|
||||||
StoreStream.getMessageState().revealSpoilerEmbed(messageId, id)
|
|
||||||
}
|
|
||||||
.alpha(0f)
|
|
||||||
}
|
|
||||||
spoilerView.visibility = if (isSpoiler && !spoiled) VISIBLE else GONE
|
|
||||||
spoilerView.alpha = 1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.componentsv2.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.aliucord.Logger
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.TextDisplayMessageComponent
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.discord.stores.StoreStream
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.discord.utilities.message.MessageUtils
|
|
||||||
import com.discord.utilities.textprocessing.*
|
|
||||||
import com.discord.utilities.textprocessing.node.SpoilerNode
|
|
||||||
import com.discord.utilities.view.text.LinkifiedTextView
|
|
||||||
import com.discord.widgets.botuikit.ComponentProvider
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentView
|
|
||||||
import com.discord.widgets.chat.list.adapter.*
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
|
|
||||||
class TextDisplayComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<TextDisplayMessageComponent> {
|
|
||||||
override fun type() = ComponentV2Type.TEXT_DISPLAY
|
|
||||||
|
|
||||||
private val textView = LinkifiedTextView(ContextThemeWrapper(ctx, R.i.UiKit_Chat_Text)).addTo(this) {
|
|
||||||
layoutParams = LayoutParams(0, WRAP_CONTENT).apply {
|
|
||||||
topMargin = 2.dp
|
|
||||||
bottomMargin = 2.dp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configure(component: TextDisplayMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
|
||||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
|
||||||
val entry = item.entry
|
|
||||||
if (entry !is BotUiComponentV2Entry) {
|
|
||||||
Logger("ComponentsV2").warn("configured text display with non-v2 entry")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render(component.id, component.content, item.adapter, entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun render(id: Int, content: String, adapter: WidgetChatListAdapter, entry: BotUiComponentV2Entry) {
|
|
||||||
val data = adapter.data
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val spoilers = entry.state?.visibleSpoilerEmbedMap?.let {
|
|
||||||
WidgetChatListAdapterItemEmbed.Companion.`access$getEmbedFieldVisibleIndices`(
|
|
||||||
WidgetChatListAdapterItemEmbed.Companion,
|
|
||||||
it,
|
|
||||||
id,
|
|
||||||
"comp"
|
|
||||||
)
|
|
||||||
} as List<Int>?
|
|
||||||
val processor = MessagePreprocessor(entry.meId, spoilers, null, false, 50)
|
|
||||||
val nickOrUsernames = MessageUtils.getNickOrUsernames(entry.message, entry.channel, entry.guildMembers, entry.channel.q())
|
|
||||||
val parseChannelMessage = DiscordParser.parseChannelMessage(
|
|
||||||
context,
|
|
||||||
content,
|
|
||||||
MessageRenderContext(
|
|
||||||
context,
|
|
||||||
entry.meId,
|
|
||||||
false,
|
|
||||||
nickOrUsernames,
|
|
||||||
StoreStream.getChannels().channelNames, // TODO, does not change
|
|
||||||
entry.guildRoles,
|
|
||||||
R.b.colorTextLink,
|
|
||||||
`WidgetChatListAdapterItemMessage$getMessageRenderContext$1`.INSTANCE,
|
|
||||||
{ s: String -> adapter.eventHandler.onUrlLongClicked(s) },
|
|
||||||
ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg),
|
|
||||||
ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg_visible),
|
|
||||||
{ node: SpoilerNode<*> -> StoreStream.getMessageState().revealSpoilerEmbedData(entry.message.id, id, "comp:${node.id}") },
|
|
||||||
{ l: Long -> adapter.eventHandler.onUserMentionClicked(l, data.channelId, data.guildId) },
|
|
||||||
`WidgetChatListAdapterItemMessage$getMessageRenderContext$4`(context)
|
|
||||||
),
|
|
||||||
processor,
|
|
||||||
DiscordParser.ParserOptions.DEFAULT,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
textView.setDraweeSpanStringBuilder(parseChannelMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.componentsv2.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.aliucord.Logger
|
|
||||||
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
|
|
||||||
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
|
|
||||||
import com.aliucord.coreplugins.componentsv2.models.ThumbnailMessageComponent
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.discord.utilities.embed.EmbedResourceUtils
|
|
||||||
import com.discord.utilities.images.MGImages
|
|
||||||
import com.discord.widgets.botuikit.ComponentProvider
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentActionListener
|
|
||||||
import com.discord.widgets.botuikit.views.ComponentView
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView
|
|
||||||
import com.google.android.material.card.MaterialCardView
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
|
|
||||||
class ThumbnailComponentView(ctx: Context) : ConstraintLayout(ctx), ComponentView<ThumbnailMessageComponent> {
|
|
||||||
override fun type() = ComponentV2Type.THUMBNAIL
|
|
||||||
|
|
||||||
private val embedThumbnailMaxSize = (ctx.resources.getDimension(R.d.embed_thumbnail_max_size) * 1.5).toInt()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val imageViewId = View.generateViewId()
|
|
||||||
}
|
|
||||||
private lateinit var imageView: SimpleDraweeView
|
|
||||||
private lateinit var spoilerView: SpoilerView
|
|
||||||
|
|
||||||
init {
|
|
||||||
MaterialCardView(ctx).addTo(this) {
|
|
||||||
radius = 8.dp.toFloat()
|
|
||||||
elevation = 0f
|
|
||||||
setCardBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.colorBackgroundPrimary))
|
|
||||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
|
||||||
ConstraintLayout(ctx).addTo(this) {
|
|
||||||
layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
|
||||||
imageView = SimpleDraweeView(ctx, null, 0, R.i.UiKit_ImageView).addTo(this) {
|
|
||||||
id = imageViewId
|
|
||||||
}
|
|
||||||
spoilerView = SpoilerView(ctx, 2).addTo(this) {
|
|
||||||
layoutParams = SpoilerView.constraintLayoutParamsAround(imageViewId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference: WidgetChatListAdapterItemEmbed.configureEmbedThumbnail
|
|
||||||
override fun configure(component: ThumbnailMessageComponent, provider: ComponentProvider, listener: ComponentActionListener) {
|
|
||||||
val item = listener as WidgetChatListAdapterItemBotComponentRow
|
|
||||||
val entry = item.entry
|
|
||||||
if (entry !is BotUiComponentV2Entry) {
|
|
||||||
Logger("ComponentsV2").warn("configured thumbnail with non-v2 entry")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val (width, height) = EmbedResourceUtils.INSTANCE.calculateScaledSize(
|
|
||||||
component.media.width,
|
|
||||||
component.media.height,
|
|
||||||
embedThumbnailMaxSize,
|
|
||||||
embedThumbnailMaxSize,
|
|
||||||
resources,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
imageView.apply {
|
|
||||||
if (layoutParams.width != width || layoutParams.height != height)
|
|
||||||
layoutParams = layoutParams.apply {
|
|
||||||
this.width = width
|
|
||||||
this.height = height
|
|
||||||
}
|
|
||||||
MGImages.`setImage$default`(
|
|
||||||
this,
|
|
||||||
EmbedResourceUtils.INSTANCE.getPreviewUrls(component.media.proxyUrl, width, height, true), // z2: shouldAnimate
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
252,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
spoilerView.configure(entry, component)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package com.aliucord.utils
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import com.discord.views.CheckedSetting
|
|
||||||
|
|
||||||
object ViewUtils {
|
|
||||||
/**
|
|
||||||
* Shorthand extension function to add a View into a ViewGroup, and then
|
|
||||||
* run a scoped function
|
|
||||||
*
|
|
||||||
* @param group ViewGroup to add this View into
|
|
||||||
* @param block A scoped function, with the View as its receiver
|
|
||||||
* @return The View
|
|
||||||
*/
|
|
||||||
fun <T : View> T.addTo(group: ViewGroup, block: (T.() -> Unit)? = null): T = apply { group.addView(this); block?.invoke(this) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shorthand extension function to add a View into a ViewGroup at specified
|
|
||||||
* index, and then run a scoped function
|
|
||||||
*
|
|
||||||
* @param group ViewGroup to add this View into
|
|
||||||
* @param index Index to insert this View at
|
|
||||||
* @param block A scoped function, with the View as its receiver
|
|
||||||
* @return The View
|
|
||||||
*/
|
|
||||||
fun <T : View> T.addTo(group: ViewGroup, index: Int, block: (T.() -> Unit)? = null): T = apply { group.addView(this, index); block?.invoke(this) }
|
|
||||||
|
|
||||||
/** Main layout of the setting */
|
|
||||||
val CheckedSetting.layout get() = l.b() as ConstraintLayout
|
|
||||||
|
|
||||||
/** Main text/label of the setting */
|
|
||||||
val CheckedSetting.label get() = l.a()
|
|
||||||
|
|
||||||
/** Checkbox button at the end of the setting */
|
|
||||||
val CheckedSetting.checkbox get() = l.c()
|
|
||||||
|
|
||||||
/** Subtext of the setting */
|
|
||||||
val CheckedSetting.subtext get() = l.f()
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class ChannelSelectComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
override val id: Int,
|
|
||||||
@b.i.d.p.b("custom_id") override val customId: String,
|
|
||||||
override val placeholder: String,
|
|
||||||
override val defaultValues: List<SelectV2DefaultValue>?,
|
|
||||||
override val minValues: Int,
|
|
||||||
override val maxValues: Int,
|
|
||||||
override val disabled: Boolean,
|
|
||||||
) : SelectV2Component() {
|
|
||||||
override fun getType() = type
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class ContainerComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
val id: Int,
|
|
||||||
val components: List<Component>,
|
|
||||||
@b.i.d.p.b("accent_color") val accentColor: Int?,
|
|
||||||
val spoiler: Boolean,
|
|
||||||
): LayoutComponent() {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun a() = components
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
abstract class ContentComponent : LayoutComponent(), Serializable {
|
|
||||||
final override fun a(): List<Component> = listOf()
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class FileComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
val id: Int,
|
|
||||||
val file: UnfurledMediaItem,
|
|
||||||
val spoiler: Boolean,
|
|
||||||
val name: String,
|
|
||||||
val size: Int,
|
|
||||||
) : ContentComponent() {
|
|
||||||
override fun getType() = type
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class MediaGalleryComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
val id: Int,
|
|
||||||
val items: List<MediaGalleryItem>,
|
|
||||||
) : ContentComponent() {
|
|
||||||
override fun getType() = type
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class MediaGalleryItem(
|
|
||||||
val media: UnfurledMediaItem,
|
|
||||||
val description: String?,
|
|
||||||
val spoiler: Boolean,
|
|
||||||
)
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class MentionableSelectComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
override val id: Int,
|
|
||||||
@b.i.d.p.b("custom_id") override val customId: String,
|
|
||||||
override val placeholder: String,
|
|
||||||
override val defaultValues: List<SelectV2DefaultValue>?,
|
|
||||||
override val minValues: Int,
|
|
||||||
override val maxValues: Int,
|
|
||||||
override val disabled: Boolean,
|
|
||||||
) : SelectV2Component() {
|
|
||||||
override fun getType() = type
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class RoleSelectComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
override val id: Int,
|
|
||||||
@b.i.d.p.b("custom_id") override val customId: String,
|
|
||||||
override val placeholder: String,
|
|
||||||
override val defaultValues: List<SelectV2DefaultValue>?,
|
|
||||||
override val minValues: Int,
|
|
||||||
override val maxValues: Int,
|
|
||||||
override val disabled: Boolean,
|
|
||||||
) : SelectV2Component() {
|
|
||||||
override fun getType() = type
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class SectionComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
val id: Int,
|
|
||||||
val components: List<Component>,
|
|
||||||
val accessory: Component,
|
|
||||||
): LayoutComponent() {
|
|
||||||
override fun getType() = type
|
|
||||||
|
|
||||||
// This property will be accessed by ComponentStateMapper to be processed into MessageComponents,
|
|
||||||
// so we pass in the accessory component to be processed too.
|
|
||||||
// Back in SectionMessageComponent.mergeToMessageComponent, we will separate this back correctly.
|
|
||||||
override fun a() = components + accessory
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
abstract class SelectV2Component() : ActionComponent() {
|
|
||||||
abstract val id: Int
|
|
||||||
abstract val customId: String
|
|
||||||
abstract val placeholder: String
|
|
||||||
abstract val defaultValues: List<SelectV2DefaultValue>?
|
|
||||||
abstract val minValues: Int
|
|
||||||
abstract val maxValues: Int
|
|
||||||
abstract val disabled: Boolean
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class SelectV2DefaultValue(
|
|
||||||
val id: Long,
|
|
||||||
val type: SelectV2DefaultValueType,
|
|
||||||
)
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
enum class SelectV2DefaultValueType {
|
|
||||||
@b.i.d.p.b("user") USER,
|
|
||||||
@b.i.d.p.b("role") ROLE,
|
|
||||||
@b.i.d.p.b("channel") CHANNEL,
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class SeparatorComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
val id: Int,
|
|
||||||
val divider: Boolean,
|
|
||||||
val spacing: Int, // 1 = small padding, 2 = large padding
|
|
||||||
): LayoutComponent() {
|
|
||||||
override fun getType() = type
|
|
||||||
override fun a(): List<Component> = listOf()
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class TextDisplayComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
val id: Int,
|
|
||||||
val content: String,
|
|
||||||
) : ContentComponent() {
|
|
||||||
override fun getType() = type
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class ThumbnailComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
val id: Int,
|
|
||||||
val media: UnfurledMediaItem,
|
|
||||||
val description: String?,
|
|
||||||
val spoiler: Boolean,
|
|
||||||
) : ContentComponent() {
|
|
||||||
override fun getType() = type
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class UnfurledMediaItem(
|
|
||||||
val url: String,
|
|
||||||
@b.i.d.p.b("proxy_url") val proxyUrl: String,
|
|
||||||
val height: Int,
|
|
||||||
val width: Int,
|
|
||||||
@b.i.d.p.b("content_type") val contentType: String?,
|
|
||||||
@b.i.d.p.b("attachment_id") val attachmentId: Long?,
|
|
||||||
)
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package com.discord.api.botuikit
|
|
||||||
|
|
||||||
data class UserSelectComponent(
|
|
||||||
private val type: ComponentType,
|
|
||||||
override val id: Int,
|
|
||||||
@b.i.d.p.b("custom_id") override val customId: String,
|
|
||||||
override val placeholder: String,
|
|
||||||
override val defaultValues: List<SelectV2DefaultValue>?,
|
|
||||||
override val minValues: Int,
|
|
||||||
override val maxValues: Int,
|
|
||||||
override val disabled: Boolean,
|
|
||||||
) : SelectV2Component() {
|
|
||||||
override fun getType() = type
|
|
||||||
}
|
|
||||||
172
canary/LICENSE
172
canary/LICENSE
|
|
@ -1,172 +0,0 @@
|
||||||
Open Software License ("OSL") v. 3.0
|
|
||||||
|
|
||||||
This Open Software License (the "License") applies to any original work of
|
|
||||||
authorship (the "Original Work") whose owner (the "Licensor") has placed the
|
|
||||||
following licensing notice adjacent to the copyright notice for the Original
|
|
||||||
Work:
|
|
||||||
|
|
||||||
Licensed under the Open Software License version 3.0
|
|
||||||
|
|
||||||
1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free,
|
|
||||||
non-exclusive, sublicensable license, for the duration of the copyright, to do
|
|
||||||
the following:
|
|
||||||
|
|
||||||
a) to reproduce the Original Work in copies, either alone or as part of a
|
|
||||||
collective work;
|
|
||||||
|
|
||||||
b) to translate, adapt, alter, transform, modify, or arrange the Original
|
|
||||||
Work, thereby creating derivative works ("Derivative Works") based upon the
|
|
||||||
Original Work;
|
|
||||||
|
|
||||||
c) to distribute or communicate copies of the Original Work and Derivative
|
|
||||||
Works to the public, with the proviso that copies of Original Work or
|
|
||||||
Derivative Works that You distribute or communicate shall be licensed under
|
|
||||||
this Open Software License;
|
|
||||||
|
|
||||||
d) to perform the Original Work publicly; and
|
|
||||||
|
|
||||||
e) to display the Original Work publicly.
|
|
||||||
|
|
||||||
2) Grant of Patent License. Licensor grants You a worldwide, royalty-free,
|
|
||||||
non-exclusive, sublicensable license, under patent claims owned or controlled
|
|
||||||
by the Licensor that are embodied in the Original Work as furnished by the
|
|
||||||
Licensor, for the duration of the patents, to make, use, sell, offer for sale,
|
|
||||||
have made, and import the Original Work and Derivative Works.
|
|
||||||
|
|
||||||
3) Grant of Source Code License. The term "Source Code" means the preferred
|
|
||||||
form of the Original Work for making modifications to it and all available
|
|
||||||
documentation describing how to modify the Original Work. Licensor agrees to
|
|
||||||
provide a machine-readable copy of the Source Code of the Original Work along
|
|
||||||
with each copy of the Original Work that Licensor distributes. Licensor
|
|
||||||
reserves the right to satisfy this obligation by placing a machine-readable
|
|
||||||
copy of the Source Code in an information repository reasonably calculated to
|
|
||||||
permit inexpensive and convenient access by You for as long as Licensor
|
|
||||||
continues to distribute the Original Work.
|
|
||||||
|
|
||||||
4) Exclusions From License Grant. Neither the names of Licensor, nor the names
|
|
||||||
of any contributors to the Original Work, nor any of their trademarks or
|
|
||||||
service marks, may be used to endorse or promote products derived from this
|
|
||||||
Original Work without express prior permission of the Licensor. Except as
|
|
||||||
expressly stated herein, nothing in this License grants any license to
|
|
||||||
Licensor's trademarks, copyrights, patents, trade secrets or any other
|
|
||||||
intellectual property. No patent license is granted to make, use, sell, offer
|
|
||||||
for sale, have made, or import embodiments of any patent claims other than the
|
|
||||||
licensed claims defined in Section 2. No license is granted to the trademarks
|
|
||||||
of Licensor even if such marks are included in the Original Work. Nothing in
|
|
||||||
this License shall be interpreted to prohibit Licensor from licensing under
|
|
||||||
terms different from this License any Original Work that Licensor otherwise
|
|
||||||
would have a right to license.
|
|
||||||
|
|
||||||
5) External Deployment. The term "External Deployment" means the use,
|
|
||||||
distribution, or communication of the Original Work or Derivative Works in any
|
|
||||||
way such that the Original Work or Derivative Works may be used by anyone
|
|
||||||
other than You, whether those works are distributed or communicated to those
|
|
||||||
persons or made available as an application intended for use over a network.
|
|
||||||
As an express condition for the grants of license hereunder, You must treat
|
|
||||||
any External Deployment by You of the Original Work or a Derivative Work as a
|
|
||||||
distribution under section 1(c).
|
|
||||||
|
|
||||||
6) Attribution Rights. You must retain, in the Source Code of any Derivative
|
|
||||||
Works that You create, all copyright, patent, or trademark notices from the
|
|
||||||
Source Code of the Original Work, as well as any notices of licensing and any
|
|
||||||
descriptive text identified therein as an "Attribution Notice." You must cause
|
|
||||||
the Source Code for any Derivative Works that You create to carry a prominent
|
|
||||||
Attribution Notice reasonably calculated to inform recipients that You have
|
|
||||||
modified the Original Work.
|
|
||||||
|
|
||||||
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
|
|
||||||
the copyright in and to the Original Work and the patent rights granted herein
|
|
||||||
by Licensor are owned by the Licensor or are sublicensed to You under the
|
|
||||||
terms of this License with the permission of the contributor(s) of those
|
|
||||||
copyrights and patent rights. Except as expressly stated in the immediately
|
|
||||||
preceding sentence, the Original Work is provided under this License on an "AS
|
|
||||||
IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without
|
|
||||||
limitation, the warranties of non-infringement, merchantability or fitness for
|
|
||||||
a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK
|
|
||||||
IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this
|
|
||||||
License. No license to the Original Work is granted by this License except
|
|
||||||
under this disclaimer.
|
|
||||||
|
|
||||||
8) Limitation of Liability. Under no circumstances and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise, shall the
|
|
||||||
Licensor be liable to anyone for any indirect, special, incidental, or
|
|
||||||
consequential damages of any character arising as a result of this License or
|
|
||||||
the use of the Original Work including, without limitation, damages for loss
|
|
||||||
of goodwill, work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses. This limitation of liability shall not
|
|
||||||
apply to the extent applicable law prohibits such limitation.
|
|
||||||
|
|
||||||
9) Acceptance and Termination. If, at any time, You expressly assented to this
|
|
||||||
License, that assent indicates your clear and irrevocable acceptance of this
|
|
||||||
License and all of its terms and conditions. If You distribute or communicate
|
|
||||||
copies of the Original Work or a Derivative Work, You must make a reasonable
|
|
||||||
effort under the circumstances to obtain the express assent of recipients to
|
|
||||||
the terms of this License. This License conditions your rights to undertake
|
|
||||||
the activities listed in Section 1, including your right to create Derivative
|
|
||||||
Works based upon the Original Work, and doing so without honoring these terms
|
|
||||||
and conditions is prohibited by copyright law and international treaty.
|
|
||||||
Nothing in this License is intended to affect copyright exceptions and
|
|
||||||
limitations (including "fair use" or "fair dealing"). This License shall
|
|
||||||
terminate immediately and You may no longer exercise any of the rights granted
|
|
||||||
to You by this License upon your failure to honor the conditions in Section
|
|
||||||
1(c).
|
|
||||||
|
|
||||||
10) Termination for Patent Action. This License shall terminate automatically
|
|
||||||
and You may no longer exercise any of the rights granted to You by this
|
|
||||||
License as of the date You commence an action, including a cross-claim or
|
|
||||||
counterclaim, against Licensor or any licensee alleging that the Original Work
|
|
||||||
infringes a patent. This termination provision shall not apply for an action
|
|
||||||
alleging patent infringement by combinations of the Original Work with other
|
|
||||||
software or hardware.
|
|
||||||
|
|
||||||
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
|
|
||||||
License may be brought only in the courts of a jurisdiction wherein the
|
|
||||||
Licensor resides or in which Licensor conducts its primary business, and under
|
|
||||||
the laws of that jurisdiction excluding its conflict-of-law provisions. The
|
|
||||||
application of the United Nations Convention on Contracts for the
|
|
||||||
International Sale of Goods is expressly excluded. Any use of the Original
|
|
||||||
Work outside the scope of this License or after its termination shall be
|
|
||||||
subject to the requirements and penalties of copyright or patent law in the
|
|
||||||
appropriate jurisdiction. This section shall survive the termination of this
|
|
||||||
License.
|
|
||||||
|
|
||||||
12) Attorneys' Fees. In any action to enforce the terms of this License or
|
|
||||||
seeking damages relating thereto, the prevailing party shall be entitled to
|
|
||||||
recover its costs and expenses, including, without limitation, reasonable
|
|
||||||
attorneys' fees and costs incurred in connection with such action, including
|
|
||||||
any appeal of such action. This section shall survive the termination of this
|
|
||||||
License.
|
|
||||||
|
|
||||||
13) Miscellaneous. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent necessary
|
|
||||||
to make it enforceable.
|
|
||||||
|
|
||||||
14) Definition of "You" in This License. "You" throughout this License,
|
|
||||||
whether in upper or lower case, means an individual or a legal entity
|
|
||||||
exercising rights under, and complying with all of the terms of, this License.
|
|
||||||
For legal entities, "You" includes any entity that controls, is controlled by,
|
|
||||||
or is under common control with you. For purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the direction or
|
|
||||||
management of such entity, whether by contract or otherwise, or (ii) ownership
|
|
||||||
of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
15) Right to Use. You may use the Original Work in all ways not otherwise
|
|
||||||
restricted or conditioned by this License or by law, and Licensor promises not
|
|
||||||
to interfere with or be responsible for such uses by You.
|
|
||||||
|
|
||||||
16) Modification of This License. This License is Copyright © 2005 Lawrence
|
|
||||||
Rosen. Permission is granted to copy, distribute, or communicate this License
|
|
||||||
without modification. Nothing in this License permits You to modify this
|
|
||||||
License as applied to the Original Work or to Derivative Works. However, You
|
|
||||||
may modify the text of this License and copy, distribute or communicate your
|
|
||||||
modified version (the "Modified License") and apply it to other original works
|
|
||||||
of authorship subject to the following conditions: (i) You may not indicate in
|
|
||||||
any way that your Modified License is the "Open Software License" or "OSL" and
|
|
||||||
you may not use those names in the name of your Modified License; (ii) You
|
|
||||||
must replace the notice specified in the first paragraph above with the notice
|
|
||||||
"Licensed under <insert your license name here>" or with a notice of your own
|
|
||||||
that is not confusingly similar to the notice in this License; and (iii) You
|
|
||||||
may not claim that your original works are open source software unless your
|
|
||||||
Modified License has been approved by Open Source Initiative (OSI) and You
|
|
||||||
comply with its license review and certification process.
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
|
||||||
|
|
||||||
version = "8.18.0"
|
|
||||||
description = "Beta backport of SlashCommandsFix"
|
|
||||||
|
|
||||||
aliucord {
|
|
||||||
changelog.set("""
|
|
||||||
# 8.18.0
|
|
||||||
* Don't use custom props anymore (core has them)
|
|
||||||
|
|
||||||
# 7.16.2
|
|
||||||
* Use new props
|
|
||||||
|
|
||||||
# 7.16.1
|
|
||||||
* Prompt restarts
|
|
||||||
|
|
||||||
# 7.16.0
|
|
||||||
* Initial port >w< thanks @jedenastka
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
deploy.set(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
apply {
|
|
||||||
plugin(libs.plugins.shadow.get().pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val shadowDir = File(buildDir, "intermediates/shadowed")
|
|
||||||
|
|
||||||
tasks.register<ShadowJar>("relocateJar") {
|
|
||||||
val javaTask = tasks.findByName("compileDebugJavaWithJavac")!!
|
|
||||||
val kotlinTask = tasks.findByName("compileDebugKotlin")!!
|
|
||||||
from(javaTask.outputs, kotlinTask.outputs)
|
|
||||||
relocate("com.aliucord.coreplugins.slashcommandsfix", "moe.lava.corenary.slashcommandsfix")
|
|
||||||
archiveClassifier.set("shadowed")
|
|
||||||
destinationDirectory.set(File(buildDir, "intermediates"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Sync>("copyShadowed") {
|
|
||||||
val reloc = tasks.findByName("relocateJar")!! as ShadowJar
|
|
||||||
dependsOn(reloc)
|
|
||||||
from(zipTree(reloc.archiveFile))
|
|
||||||
into(shadowDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
project.afterEvaluate {
|
|
||||||
tasks.compileDex {
|
|
||||||
val copyShadowed = tasks.findByName("copyShadowed")!! as Sync
|
|
||||||
dependsOn(copyShadowed)
|
|
||||||
input.setFrom(shadowDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import com.discord.models.user.User;
|
|
||||||
import com.discord.stores.StoreStream;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class ApiApplication {
|
|
||||||
public final long id;
|
|
||||||
public final String name;
|
|
||||||
public final String icon;
|
|
||||||
public final ApiPermissions permissions;
|
|
||||||
public final Long botId;
|
|
||||||
|
|
||||||
public ApiApplication() {
|
|
||||||
this.id = 0;
|
|
||||||
this.name = null;
|
|
||||||
this.icon = null;
|
|
||||||
this.permissions = null;
|
|
||||||
this.botId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Application toModel() {
|
|
||||||
Permissions permissions = null;
|
|
||||||
if (this.permissions != null) {
|
|
||||||
permissions = this.permissions.toModel(Optional.empty());
|
|
||||||
} else {
|
|
||||||
permissions = new Permissions(null, null, null, null);
|
|
||||||
}
|
|
||||||
var usersStore = StoreStream.getUsers();
|
|
||||||
Optional<User> botUser = Optional.ofNullable(this.botId).map(userId -> usersStore.getUsers().get(userId));
|
|
||||||
return new Application(this.id, this.name, this.icon, permissions, botUser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import com.discord.models.commands.ApplicationCommand;
|
|
||||||
import com.discord.stores.StoreApplicationCommandsKt;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
class ApiApplicationCommand {
|
|
||||||
public final long id;
|
|
||||||
public final long applicationId;
|
|
||||||
public final String name;
|
|
||||||
public final String description;
|
|
||||||
public final List<com.discord.api.commands.ApplicationCommandOption> options;
|
|
||||||
public final ApiPermissions permissions;
|
|
||||||
public final Long defaultMemberPermissions;
|
|
||||||
public final Long guildId;
|
|
||||||
public final String version;
|
|
||||||
public final int type;
|
|
||||||
|
|
||||||
public ApiApplicationCommand() {
|
|
||||||
this.id = 0;
|
|
||||||
this.applicationId = 0;
|
|
||||||
this.name = null;
|
|
||||||
this.description = null;
|
|
||||||
this.options = null;
|
|
||||||
this.permissions = null;
|
|
||||||
this.defaultMemberPermissions = null;
|
|
||||||
this.guildId = null;
|
|
||||||
this.version = null;
|
|
||||||
this.type = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RemoteApplicationCommand toModel() {
|
|
||||||
var apiOptions = this.options;
|
|
||||||
if (apiOptions == null) {
|
|
||||||
apiOptions = new ArrayList<>();
|
|
||||||
}
|
|
||||||
var options = apiOptions
|
|
||||||
.stream()
|
|
||||||
.map(option -> StoreApplicationCommandsKt.toSlashCommandOption(option))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
Permissions permissions = null;
|
|
||||||
var defaultMemberPermissions = Optional.ofNullable(this.defaultMemberPermissions);
|
|
||||||
if (this.permissions != null) {
|
|
||||||
permissions = this.permissions.toModel(defaultMemberPermissions);
|
|
||||||
} else {
|
|
||||||
permissions = new Permissions(null, null, null, defaultMemberPermissions);
|
|
||||||
}
|
|
||||||
return new RemoteApplicationCommand(String.valueOf(this.id), this.applicationId, this.name, this.description, options, permissions, this.guildId, this.version, this.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
class ApiApplicationIndex {
|
|
||||||
public List<ApiApplication> applications;
|
|
||||||
public List<ApiApplicationCommand> applicationCommands;
|
|
||||||
|
|
||||||
public ApiApplicationIndex() {
|
|
||||||
this.applications = null;
|
|
||||||
this.applicationCommands = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApplicationIndex toModel() {
|
|
||||||
var applications = new HashMap<Long, Application>();
|
|
||||||
for (var application: this.applications) {
|
|
||||||
applications.put(application.id, application.toModel());
|
|
||||||
}
|
|
||||||
var applicationCommands = new HashMap<Long, RemoteApplicationCommand>();
|
|
||||||
for (var applicationCommand: this.applicationCommands) {
|
|
||||||
applicationCommands.put(applicationCommand.id, applicationCommand.toModel());
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ApplicationIndex(applications, applicationCommands);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
class ApiGuildApplicationCommandIndexUpdate {
|
|
||||||
public long guildId;
|
|
||||||
|
|
||||||
public ApiGuildApplicationCommandIndexUpdate() {
|
|
||||||
this.guildId = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class ApiPermissions {
|
|
||||||
public Boolean user;
|
|
||||||
public Map<Long, Boolean> roles;
|
|
||||||
public Map<Long, Boolean> channels;
|
|
||||||
|
|
||||||
public ApiPermissions() {
|
|
||||||
this.user = null;
|
|
||||||
this.roles = null;
|
|
||||||
this.channels = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Permissions toModel(Optional<Long> defaultMemberPermissions) {
|
|
||||||
return new Permissions(Optional.ofNullable(user), roles, channels, defaultMemberPermissions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import com.aliucord.Logger;
|
|
||||||
import com.discord.models.user.User;
|
|
||||||
import com.discord.utilities.user.UserUtils;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class Application extends com.discord.models.commands.Application {
|
|
||||||
public Permissions permissions_;
|
|
||||||
|
|
||||||
public Application(long id, String name, String icon, Permissions permissions, Optional<User> botUser) {
|
|
||||||
super(id, name, icon, null, -1, botUser.map(user -> UserUtils.INSTANCE.synthesizeApiUser(user)).orElse(null), false);
|
|
||||||
this.permissions_ = permissions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import java.lang.IllegalAccessException;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
class ApplicationIndex {
|
|
||||||
public Map<Long, Application> applications;
|
|
||||||
public Map<Long, RemoteApplicationCommand> applicationCommands;
|
|
||||||
|
|
||||||
public ApplicationIndex(Map<Long, Application> applications, Map<Long, RemoteApplicationCommand> applicationCommands) {
|
|
||||||
this.applications = applications;
|
|
||||||
this.applicationCommands = applicationCommands;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApplicationIndex(List<ApplicationIndex> applicationIndexes) {
|
|
||||||
this.applications = new HashMap();
|
|
||||||
this.applicationCommands = new HashMap();
|
|
||||||
for (var applicationIndex: applicationIndexes) {
|
|
||||||
this.applications.putAll(applicationIndex.applications);
|
|
||||||
this.applicationCommands.putAll(applicationIndex.applicationCommands);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateCommandCounts(Field applicationCommandCountField) throws IllegalAccessException {
|
|
||||||
var applicationCommandCounts = new HashMap<Long, Integer>();
|
|
||||||
for (var applicationCommand: this.applicationCommands.values()) {
|
|
||||||
var count = applicationCommandCounts.getOrDefault(applicationCommand.getApplicationId(), 0);
|
|
||||||
count += 1;
|
|
||||||
applicationCommandCounts.put(applicationCommand.getApplicationId(), count);
|
|
||||||
}
|
|
||||||
for (var application: this.applications.values()) {
|
|
||||||
applicationCommandCountField.setInt(application, applicationCommandCounts.getOrDefault(application.getId(), 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class ApplicationIndexCache {
|
|
||||||
public Map<Long, ApplicationIndex> guild;
|
|
||||||
public Map<Long, ApplicationIndex> dm;
|
|
||||||
public Optional<ApplicationIndex> user;
|
|
||||||
|
|
||||||
public ApplicationIndexCache() {
|
|
||||||
this.guild = new HashMap<>();
|
|
||||||
this.dm = new HashMap<>();
|
|
||||||
this.user = Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
interface ApplicationIndexSource {
|
|
||||||
String getEndpoint();
|
|
||||||
Optional<ApplicationIndex> getFromCache(ApplicationIndexCache cache);
|
|
||||||
void insertIntoCache(ApplicationIndexCache cache, ApplicationIndex index);
|
|
||||||
void removeFromCache(ApplicationIndexCache cache);
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class ApplicationIndexSourceDm implements ApplicationIndexSource {
|
|
||||||
long channelId;
|
|
||||||
|
|
||||||
public ApplicationIndexSourceDm(long channelId) {
|
|
||||||
this.channelId = channelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getEndpoint() {
|
|
||||||
return String.format("/channels/%d/application-command-index", this.channelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<ApplicationIndex> getFromCache(ApplicationIndexCache cache) {
|
|
||||||
return Optional.ofNullable(
|
|
||||||
cache.dm.get(this.channelId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void insertIntoCache(ApplicationIndexCache cache, ApplicationIndex index) {
|
|
||||||
cache.dm.put(this.channelId, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeFromCache(ApplicationIndexCache cache) {
|
|
||||||
cache.dm.remove(this.channelId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class ApplicationIndexSourceGuild implements ApplicationIndexSource {
|
|
||||||
long guildId;
|
|
||||||
|
|
||||||
public ApplicationIndexSourceGuild(long guildId) {
|
|
||||||
this.guildId = guildId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getEndpoint() {
|
|
||||||
return String.format("/guilds/%d/application-command-index", this.guildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<ApplicationIndex> getFromCache(ApplicationIndexCache cache) {
|
|
||||||
return Optional.ofNullable(
|
|
||||||
cache.guild.get(this.guildId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void insertIntoCache(ApplicationIndexCache cache, ApplicationIndex index) {
|
|
||||||
cache.guild.put(this.guildId, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeFromCache(ApplicationIndexCache cache) {
|
|
||||||
cache.guild.remove(this.guildId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class ApplicationIndexSourceUser implements ApplicationIndexSource {
|
|
||||||
public ApplicationIndexSourceUser() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getEndpoint() {
|
|
||||||
return "/users/@me/application-command-index";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<ApplicationIndex> getFromCache(ApplicationIndexCache cache) {
|
|
||||||
return cache.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void insertIntoCache(ApplicationIndexCache cache, ApplicationIndex index) {
|
|
||||||
cache.user = Optional.of(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeFromCache(ApplicationIndexCache cache) {
|
|
||||||
cache.user = Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
package com.aliucord.coreplugins.slashcommandsfix
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.aliucord.*
|
|
||||||
import com.aliucord.fragments.ConfirmDialog
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
object ConflictCheck {
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
@JvmStatic
|
|
||||||
fun run(context: Context): Boolean {
|
|
||||||
val hasFix = PluginManager.plugins.containsKey("SlashCommandsFix")
|
|
||||||
val hasForcedFix = PluginManager.plugins.containsKey("ForceSlashCommandsFixNOW")
|
|
||||||
val fromStorage = Main.settings.getBool("AC_from_storage", false)
|
|
||||||
|
|
||||||
if (hasFix) {
|
|
||||||
Logger("SlashCommandsFixBeta").warn("conflict detected")
|
|
||||||
if (hasForcedFix || fromStorage) {
|
|
||||||
Utils.threadPool.execute {
|
|
||||||
Thread.sleep(5000) // wait for app to load guh
|
|
||||||
Utils.mainThread.post {
|
|
||||||
val dialog = ConfirmDialog()
|
|
||||||
dialog
|
|
||||||
.setTitle("SlashCommandsFix Conflict")
|
|
||||||
.setDescription("You have another variant of SlashCommandsFix installed. Do you want to disable it?")
|
|
||||||
.setIsDangerous(true)
|
|
||||||
.setOnOkListener {
|
|
||||||
File(context.codeCacheDir, "Aliucord.zip").delete()
|
|
||||||
if (fromStorage)
|
|
||||||
Main.settings.setBool("AC_from_storage", false)
|
|
||||||
if (hasForcedFix)
|
|
||||||
PluginManager.disablePlugin("ForceSlashCommandsFixNOW")
|
|
||||||
val ctx = it.context
|
|
||||||
val intent = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)
|
|
||||||
Utils.appActivity.startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
.apply { isCancelable = false }
|
|
||||||
.show(Utils.appActivity.supportFragmentManager, "SlashCommandsFix conflict")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger("SlashCommandsFixBeta").warn("removing myself... bye!")
|
|
||||||
File("${Constants.PLUGINS_PATH}/SlashCommandsFixBeta.zip").delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasFix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,318 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Base64;
|
|
||||||
|
|
||||||
import com.aliucord.api.GatewayAPI;
|
|
||||||
import com.aliucord.Http;
|
|
||||||
import com.aliucord.Logger;
|
|
||||||
import com.aliucord.patcher.InsteadHook;
|
|
||||||
import com.aliucord.patcher.Patcher;
|
|
||||||
import com.aliucord.patcher.PreHook;
|
|
||||||
import com.aliucord.Utils;
|
|
||||||
import com.aliucord.utils.GsonUtils;
|
|
||||||
import com.aliucord.utils.RNSuperProperties;
|
|
||||||
import com.discord.api.channel.Channel;
|
|
||||||
import com.discord.models.commands.Application;
|
|
||||||
import com.discord.models.commands.ApplicationCommand;
|
|
||||||
import com.discord.models.commands.ApplicationCommandKt;
|
|
||||||
import com.discord.models.commands.ApplicationCommandLocalSendData;
|
|
||||||
import com.discord.stores.BuiltInCommandsProvider;
|
|
||||||
import com.discord.stores.StoreApplicationCommands;
|
|
||||||
import com.discord.stores.StoreApplicationCommands$requestApplicationCommands$1;
|
|
||||||
import com.discord.stores.StoreApplicationCommands$requestApplicationCommandsQuery$1;
|
|
||||||
import com.discord.stores.StoreApplicationInteractions;
|
|
||||||
import com.discord.stores.StoreChannelsSelected;
|
|
||||||
import com.discord.stores.StoreStream;
|
|
||||||
import com.discord.utilities.error.Error;
|
|
||||||
import com.discord.utilities.messagesend.MessageResult;
|
|
||||||
import com.discord.utilities.permissions.PermissionUtils;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import kotlin.jvm.functions.Function0;
|
|
||||||
import kotlin.jvm.functions.Function1;
|
|
||||||
|
|
||||||
final class Patches {
|
|
||||||
private ApplicationIndexCache applicationIndexCache;
|
|
||||||
private Logger logger;
|
|
||||||
private Method handleGuildApplicationsUpdateMethod;
|
|
||||||
private Method handleDiscoverCommandsUpdateMethod;
|
|
||||||
private Method handleQueryCommandsUpdateMethod;
|
|
||||||
private Field applicationCommandCountField;
|
|
||||||
private Field storeApplicationCommandsQueryField;
|
|
||||||
private Field errorResponseErrorField;
|
|
||||||
private Field skemaErrorSubErrorsField;
|
|
||||||
private Field skemaErrorErrorsField;
|
|
||||||
private Field skemaErrorItemCodeField;
|
|
||||||
private Field skemaErrorItemMessageField;
|
|
||||||
private Field storeApplicationCommandsBuiltInCommandsProviderField;
|
|
||||||
|
|
||||||
Patches(Logger logger) throws Throwable {
|
|
||||||
this.logger = logger;
|
|
||||||
this.applicationIndexCache = new ApplicationIndexCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public void loadPatches(Context context) throws Throwable {
|
|
||||||
this.handleGuildApplicationsUpdateMethod = StoreApplicationCommands.class.getDeclaredMethod("handleGuildApplicationsUpdate", List.class);
|
|
||||||
this.handleGuildApplicationsUpdateMethod.setAccessible(true);
|
|
||||||
this.handleDiscoverCommandsUpdateMethod = StoreApplicationCommands.class.getDeclaredMethod("handleDiscoverCommandsUpdate", List.class);
|
|
||||||
this.handleDiscoverCommandsUpdateMethod.setAccessible(true);
|
|
||||||
this.handleQueryCommandsUpdateMethod = StoreApplicationCommands.class.getDeclaredMethod("handleQueryCommandsUpdate", List.class);
|
|
||||||
this.handleQueryCommandsUpdateMethod.setAccessible(true);
|
|
||||||
this.applicationCommandCountField = Application.class.getDeclaredField("commandCount");
|
|
||||||
this.applicationCommandCountField.setAccessible(true);
|
|
||||||
this.storeApplicationCommandsQueryField = StoreApplicationCommands.class.getDeclaredField("query");
|
|
||||||
this.storeApplicationCommandsQueryField.setAccessible(true);
|
|
||||||
this.errorResponseErrorField = Error.Response.class.getDeclaredField("skemaError");
|
|
||||||
this.errorResponseErrorField.setAccessible(true);
|
|
||||||
this.skemaErrorSubErrorsField = Error.SkemaError.class.getDeclaredField("subErrors");
|
|
||||||
this.skemaErrorSubErrorsField.setAccessible(true);
|
|
||||||
this.skemaErrorErrorsField = Error.SkemaError.class.getDeclaredField("errors");
|
|
||||||
this.skemaErrorErrorsField.setAccessible(true);
|
|
||||||
this.skemaErrorItemCodeField = Error.SkemaErrorItem.class.getDeclaredField("code");
|
|
||||||
this.skemaErrorItemCodeField.setAccessible(true);
|
|
||||||
this.skemaErrorItemMessageField = Error.SkemaErrorItem.class.getDeclaredField("message");
|
|
||||||
this.skemaErrorItemMessageField.setAccessible(true);
|
|
||||||
this.storeApplicationCommandsBuiltInCommandsProviderField = StoreApplicationCommands.class.getDeclaredField("builtInCommandsProvider");
|
|
||||||
this.storeApplicationCommandsBuiltInCommandsProviderField.setAccessible(true);
|
|
||||||
|
|
||||||
var storeApplicationCommands = StoreStream.getApplicationCommands();
|
|
||||||
var storeChannelsSelected = StoreStream.getChannelsSelected();
|
|
||||||
var storeUsers = StoreStream.getUsers();
|
|
||||||
var storePermissions = StoreStream.getPermissions();
|
|
||||||
var storeGuilds = StoreStream.getGuilds();
|
|
||||||
|
|
||||||
// Browsing commands (when just a '/' is typed)
|
|
||||||
Patcher.addPatch(
|
|
||||||
StoreApplicationCommands$requestApplicationCommands$1.class.getDeclaredMethod("invoke"),
|
|
||||||
new PreHook(param -> {
|
|
||||||
var this_ = (StoreApplicationCommands$requestApplicationCommands$1) param.thisObject;
|
|
||||||
|
|
||||||
if (this_.$guildId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var applicationIndexSource = Patches.applicationIndexSourceFromContext(this_.$guildId, storeChannelsSelected);
|
|
||||||
try {
|
|
||||||
this.passCommandData(this_.this$0, applicationIndexSource, RequestSource.BROWSE);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
param.setResult(null);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Completing commands
|
|
||||||
Patcher.addPatch(
|
|
||||||
StoreApplicationCommands$requestApplicationCommandsQuery$1.class.getDeclaredMethod("invoke"),
|
|
||||||
new PreHook(param -> {
|
|
||||||
var this_ = (StoreApplicationCommands$requestApplicationCommandsQuery$1) param.thisObject;
|
|
||||||
|
|
||||||
if (this_.$guildId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var applicationIndexSource = Patches.applicationIndexSourceFromContext(this_.$guildId, storeChannelsSelected);
|
|
||||||
try {
|
|
||||||
storeApplicationCommandsQueryField.set(this_.this$0, this_.$query);
|
|
||||||
this.passCommandData(this_.this$0, applicationIndexSource, RequestSource.QUERY);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
param.setResult(null);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Command permission check
|
|
||||||
Patcher.addPatch(
|
|
||||||
ApplicationCommandKt.class.getDeclaredMethod("hasPermission", ApplicationCommand.class, long.class, List.class),
|
|
||||||
new InsteadHook(param -> {
|
|
||||||
var applicationCommand = (ApplicationCommand) param.args[0];
|
|
||||||
var roleIds = (List<Long>) param.args[2];
|
|
||||||
|
|
||||||
if (!(applicationCommand instanceof RemoteApplicationCommand)) {
|
|
||||||
// Allow all builtin commands
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
var remoteApplicationCommand = (RemoteApplicationCommand) applicationCommand;
|
|
||||||
|
|
||||||
var channel = storeChannelsSelected.getSelectedChannel();
|
|
||||||
var guildId = channel.i();
|
|
||||||
|
|
||||||
if (guildId == 0) {
|
|
||||||
// Allow all commands in DMs
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var applicationId = remoteApplicationCommand.getApplicationId();
|
|
||||||
var isUser = this.requestApplicationIndex(new ApplicationIndexSourceUser())
|
|
||||||
.applications
|
|
||||||
.containsKey(applicationId);
|
|
||||||
if (isUser) {
|
|
||||||
// Allow all user application commands
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
var application = this.requestApplicationIndex(new ApplicationIndexSourceGuild(guildId))
|
|
||||||
.applications
|
|
||||||
.get(applicationId);
|
|
||||||
if (application == null) {
|
|
||||||
// Discord requested checking a command from the previous guild - ignore
|
|
||||||
// Some such requests are still processed (if the command exists in both guilds), but it's not an issue as the result doesn't matter for them anyways.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var user = storeUsers.getMe();
|
|
||||||
var memberPermissions = storePermissions.getGuildPermissions()
|
|
||||||
.get(guildId);
|
|
||||||
var guild = storeGuilds.getGuild(guildId);
|
|
||||||
|
|
||||||
var applicationPermission = application.permissions_.checkFor(roleIds, channel, guild, memberPermissions, user, true);
|
|
||||||
var commandPermission = remoteApplicationCommand.permissions_.checkFor(roleIds, channel, guild, memberPermissions, user, applicationPermission);
|
|
||||||
|
|
||||||
return commandPermission;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Command error handling
|
|
||||||
Patcher.addPatch(
|
|
||||||
StoreApplicationInteractions.class.getDeclaredMethod("handleApplicationCommandResult", MessageResult.class, ApplicationCommandLocalSendData.class, Function0.class, Function1.class),
|
|
||||||
new PreHook(param -> {
|
|
||||||
var result = (MessageResult) param.args[0];
|
|
||||||
var localSendData = (ApplicationCommandLocalSendData) param.args[1];
|
|
||||||
|
|
||||||
if (result instanceof MessageResult.UnknownFailure) {
|
|
||||||
boolean invalidCommandVersion = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var errorResponse = ((MessageResult.UnknownFailure) result)
|
|
||||||
.getError()
|
|
||||||
.getResponse();
|
|
||||||
var error = this.errorResponseErrorField.get(errorResponse);
|
|
||||||
var subErrors = ((Map<String, Error.SkemaError>) skemaErrorSubErrorsField.get(error));
|
|
||||||
var dataErrors = (List<Error.SkemaErrorItem>) skemaErrorErrorsField.get(subErrors.get("data"));
|
|
||||||
|
|
||||||
for (var dataError: dataErrors) {
|
|
||||||
var errorCode = (String) this.skemaErrorItemCodeField.get(dataError);
|
|
||||||
if (errorCode.equals("INTERACTION_APPLICATION_COMMAND_INVALID_VERSION")) {
|
|
||||||
ApplicationIndexSource applicationIndexSource = null;
|
|
||||||
var guildId = localSendData.component3();
|
|
||||||
if (guildId != null) {
|
|
||||||
applicationIndexSource = new ApplicationIndexSourceGuild(guildId);
|
|
||||||
} else {
|
|
||||||
var channelId = localSendData.component2();
|
|
||||||
applicationIndexSource = new ApplicationIndexSourceDm(channelId);
|
|
||||||
}
|
|
||||||
this.cleanApplicationIndexCache(applicationIndexSource);
|
|
||||||
|
|
||||||
var errorMessage = (String) this.skemaErrorItemMessageField.get(dataError);
|
|
||||||
Utils.showToast(errorMessage);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
GatewayAPI.onEvent("GUILD_APPLICATION_COMMAND_INDEX_UPDATE", ApiGuildApplicationCommandIndexUpdate.class, guildApplicationCommandIndexUpdate -> {
|
|
||||||
this.cleanApplicationIndexCache(new ApplicationIndexSourceGuild(guildApplicationCommandIndexUpdate.guildId));
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void passCommandData(StoreApplicationCommands storeApplicationCommands, Optional<ApplicationIndexSource> applicationIndexSource, RequestSource requestSource) throws Exception {
|
|
||||||
var applicationIndexes = new ArrayList();
|
|
||||||
if (applicationIndexSource.isPresent()) {
|
|
||||||
applicationIndexes.add(this.requestApplicationIndex(applicationIndexSource.get()));
|
|
||||||
}
|
|
||||||
applicationIndexes.add(this.requestApplicationIndex(new ApplicationIndexSourceUser()));
|
|
||||||
var applicationIndex = new ApplicationIndex(applicationIndexes);
|
|
||||||
applicationIndex
|
|
||||||
.applicationCommands
|
|
||||||
.entrySet()
|
|
||||||
.removeIf(applicationCommand -> applicationCommand.getValue().type != RemoteApplicationCommand.TYPE_CHAT_INPUT);
|
|
||||||
applicationIndex.populateCommandCounts(this.applicationCommandCountField);
|
|
||||||
|
|
||||||
var applications = new ArrayList<Application>(applicationIndex.applications.values());
|
|
||||||
Collections.sort(applications, (left, right) -> left.getName().compareTo(right.getName()));
|
|
||||||
applications.add(((BuiltInCommandsProvider) this.storeApplicationCommandsBuiltInCommandsProviderField.get(storeApplicationCommands)).getBuiltInApplication());
|
|
||||||
this.handleGuildApplicationsUpdateMethod.invoke(storeApplicationCommands, applications);
|
|
||||||
|
|
||||||
switch (requestSource) {
|
|
||||||
case BROWSE:
|
|
||||||
this.handleDiscoverCommandsUpdateMethod.invoke(storeApplicationCommands, new ArrayList(applicationIndex.applicationCommands.values()));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case QUERY:
|
|
||||||
this.handleQueryCommandsUpdateMethod.invoke(storeApplicationCommands, new ArrayList(applicationIndex.applicationCommands.values()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ApplicationIndex requestApplicationIndex(ApplicationIndexSource source) {
|
|
||||||
// Reuse application index from cache
|
|
||||||
var applicationIndex = source.getFromCache(applicationIndexCache);
|
|
||||||
if (!applicationIndex.isPresent()) {
|
|
||||||
try {
|
|
||||||
// Request application index from API
|
|
||||||
applicationIndex = Optional.of(
|
|
||||||
Http.Request.newDiscordRNRequest(source.getEndpoint())
|
|
||||||
.execute()
|
|
||||||
.json(GsonUtils.getGsonRestApi(), ApiApplicationIndex.class)
|
|
||||||
.toModel()
|
|
||||||
);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
source.insertIntoCache(applicationIndexCache, applicationIndex.get());
|
|
||||||
}
|
|
||||||
return applicationIndex.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanApplicationIndexCache(ApplicationIndexSource source) {
|
|
||||||
source.removeFromCache(applicationIndexCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<ApplicationIndexSource> applicationIndexSourceFromContext(long guildId, StoreChannelsSelected storeChannelsSelected) {
|
|
||||||
Optional<ApplicationIndexSource> applicationIndexSource = Optional.empty();
|
|
||||||
// guildId being 0 means this is a DM or a DM group
|
|
||||||
if (guildId != 0) {
|
|
||||||
applicationIndexSource = Optional.of(new ApplicationIndexSourceGuild(guildId));
|
|
||||||
} else {
|
|
||||||
// Only create a DM index source for bots
|
|
||||||
var channel = storeChannelsSelected.getSelectedChannel();
|
|
||||||
var channelType = channel.D();
|
|
||||||
if (channelType == Channel.DM) {
|
|
||||||
var user = channel.z().get(0);
|
|
||||||
var userIsBot = Optional.ofNullable(user.e())
|
|
||||||
.orElse(false);
|
|
||||||
if (userIsBot) {
|
|
||||||
var channelId = channel.k();
|
|
||||||
applicationIndexSource = Optional.of(new ApplicationIndexSourceDm(channelId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return applicationIndexSource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import com.discord.api.channel.Channel;
|
|
||||||
import com.discord.api.permission.Permission;
|
|
||||||
import com.discord.models.guild.Guild;
|
|
||||||
import com.discord.models.user.MeUser;
|
|
||||||
import com.discord.utilities.permissions.PermissionUtils;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
class Permissions {
|
|
||||||
public Optional<Boolean> user;
|
|
||||||
public Map<Long, Boolean> roles;
|
|
||||||
public Map<Long, Boolean> channels;
|
|
||||||
public Optional<Long> defaultMemberPermissions;
|
|
||||||
|
|
||||||
public Permissions(Optional<Boolean> user, Map<Long, Boolean> roles, Map<Long, Boolean> channels, Optional<Long> defaultMemberPermissions) {
|
|
||||||
this.user = Optional.ofNullable(user).orElse(Optional.empty());
|
|
||||||
this.roles = Optional.ofNullable(roles).orElse(new HashMap<>());
|
|
||||||
this.channels = Optional.ofNullable(channels).orElse(new HashMap<>());
|
|
||||||
this.defaultMemberPermissions = Optional.ofNullable(defaultMemberPermissions).orElse(Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkFor(List<Long> roleIds, Channel channel, Guild guild, long memberPermissions, MeUser user, boolean defaultPermission) {
|
|
||||||
var guildId = guild.component7();
|
|
||||||
var defaultChannelPermissionId = guildId - 1;
|
|
||||||
var defaultChannelPermission = this.channels.getOrDefault(defaultChannelPermissionId, defaultPermission);
|
|
||||||
var channelType = channel.D();
|
|
||||||
var channelId = channel.k();
|
|
||||||
var permissionChannelId = channelId;
|
|
||||||
// Threads inherit permissions from their parent channels
|
|
||||||
if (channelType == Channel.ANNOUNCEMENT_THREAD || channelType == Channel.PUBLIC_THREAD || channelType == Channel.PRIVATE_THREAD) {
|
|
||||||
var channelParentId = channel.u();
|
|
||||||
permissionChannelId = channelParentId;
|
|
||||||
}
|
|
||||||
var channelPermission = Optional.ofNullable(this.channels.get(permissionChannelId))
|
|
||||||
.orElse(defaultChannelPermission);
|
|
||||||
var defaultMemberPermission = this.defaultMemberPermissions
|
|
||||||
.map(
|
|
||||||
defaultMemberPermissions -> defaultMemberPermissions != 0
|
|
||||||
&& PermissionUtils.canAndIsElevated(
|
|
||||||
defaultMemberPermissions,
|
|
||||||
memberPermissions,
|
|
||||||
user.getMfaEnabled(),
|
|
||||||
guild.getMfaLevel()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orElse(defaultPermission);
|
|
||||||
var everyoneRoleId = guildId;
|
|
||||||
var defaultRolePermission = this.roles.getOrDefault(everyoneRoleId, defaultMemberPermission);
|
|
||||||
var rolePermission = this.calculateRolePermission(roleIds, defaultRolePermission);
|
|
||||||
var userPermission = this.user.orElse(defaultMemberPermission);
|
|
||||||
var administratorPermission = PermissionUtils.canAndIsElevated(
|
|
||||||
Permission.ADMINISTRATOR,
|
|
||||||
memberPermissions,
|
|
||||||
user.getMfaEnabled(),
|
|
||||||
guild.getMfaLevel()
|
|
||||||
);
|
|
||||||
|
|
||||||
return administratorPermission || (channelPermission && (userPermission || rolePermission));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean calculateRolePermission(List<Long> roleIds, boolean defaultPermission) {
|
|
||||||
var calculatedRolePermission = defaultPermission;
|
|
||||||
for (var roleId: roleIds) {
|
|
||||||
var rolePermission = this.roles.get(roleId);
|
|
||||||
if (rolePermission != null) {
|
|
||||||
calculatedRolePermission = rolePermission;
|
|
||||||
if (rolePermission) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return calculatedRolePermission;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import com.discord.models.commands.ApplicationCommandOption;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
class RemoteApplicationCommand extends com.discord.models.commands.RemoteApplicationCommand {
|
|
||||||
public Permissions permissions_;
|
|
||||||
public int type;
|
|
||||||
|
|
||||||
public static final int TYPE_CHAT_INPUT = 1;
|
|
||||||
|
|
||||||
public RemoteApplicationCommand(String id, long applicationId, String name, String description, List<ApplicationCommandOption> options, Permissions permissions, Long guildId, String version, int type) {
|
|
||||||
super(id, applicationId, name, description, options, guildId, version, null, null, null);
|
|
||||||
this.permissions_ = permissions;
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
enum RequestSource {
|
|
||||||
BROWSE,
|
|
||||||
QUERY;
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Aliucord, an Android Discord client mod.
|
|
||||||
* Copyright (c) 2021 Juby210 & Vendicated
|
|
||||||
* Licensed under the Open Software License version 3.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aliucord.coreplugins.slashcommandsfix;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.aliucord.annotations.AliucordPlugin;
|
|
||||||
import com.aliucord.entities.Plugin;
|
|
||||||
|
|
||||||
import de.robv.android.xposed.XposedBridge;
|
|
||||||
|
|
||||||
@AliucordPlugin(requiresRestart = true)
|
|
||||||
public final class SlashCommandsFix extends Plugin {
|
|
||||||
public SlashCommandsFix() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Context context) throws Throwable {
|
|
||||||
if (ConflictCheck.run(context)) return;
|
|
||||||
|
|
||||||
XposedBridge.makeClassInheritable(com.discord.models.commands.Application.class);
|
|
||||||
XposedBridge.makeClassInheritable(com.discord.models.commands.RemoteApplicationCommand.class);
|
|
||||||
|
|
||||||
new Patches(this.logger).loadPatches(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop(Context context) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# Gradle
|
|
||||||
org.gradle.caching=true
|
|
||||||
org.gradle.configuration-cache=true
|
|
||||||
org.gradle.configureondemand=true
|
|
||||||
org.gradle.parallel=true
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
||||||
|
|
||||||
# Kotlin
|
|
||||||
kotlin.code.style=official
|
|
||||||
|
|
||||||
# Android
|
|
||||||
android.useAndroidX=true
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
[versions]
|
|
||||||
aliucord = "2.6.0"
|
|
||||||
aliuhook = "1.1.4"
|
|
||||||
aliucord-gradle = "2.3.0"
|
|
||||||
android = "8.13.0"
|
|
||||||
discord = "126021"
|
|
||||||
kotlin = "2.2.20"
|
|
||||||
#noinspection GradleDependency
|
|
||||||
kotlin-stdlib = "1.5.21"
|
|
||||||
ktlint = "1.7.1"
|
|
||||||
ktlint-plugin = "13.1.0"
|
|
||||||
shadow = "8.3.8"
|
|
||||||
|
|
||||||
[libraries]
|
|
||||||
aliucord = { module = "com.aliucord:Aliucord", version.ref = "aliucord" }
|
|
||||||
aliuhook = { module = "com.aliucord:Aliuhook", version.ref = "aliuhook" }
|
|
||||||
discord = { module = "com.discord:discord", version.ref = "discord" }
|
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-stdlib" }
|
|
||||||
|
|
||||||
[plugins]
|
|
||||||
aliucord-plugin = { id = "com.aliucord.plugin", version.ref = "aliucord-gradle" }
|
|
||||||
android-library = { id = "com.android.library", version.ref = "android" }
|
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|
||||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-plugin" }
|
|
||||||
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
8
gradle/wrapper/gradle-wrapper.properties
vendored
8
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,8 +0,0 @@
|
||||||
#Wed May 28 17:22:29 GMT 2025
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
234
gradlew
vendored
234
gradlew
vendored
|
|
@ -1,234 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
89
gradlew.bat
vendored
89
gradlew.bat
vendored
|
|
@ -1,89 +0,0 @@
|
||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
|
||||||
exit /b 1
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
version = "1.0.3"
|
|
||||||
description = "More lenient message grouping"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "moe.lava.awoocord.bocchi"
|
|
||||||
}
|
|
||||||
|
|
||||||
aliucord {
|
|
||||||
// Changelog of your plugin
|
|
||||||
changelog.set("""
|
|
||||||
# 1.0.3
|
|
||||||
* Clump more than 6 messages together
|
|
||||||
|
|
||||||
# 1.0.2
|
|
||||||
* Fix (inverted) webhook clumping
|
|
||||||
|
|
||||||
# 1.0.1
|
|
||||||
* Hide blank space w.r.t attachments and embeds
|
|
||||||
|
|
||||||
# 1.0.0
|
|
||||||
* Initial release >w<
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
deploy.set(true)
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
package moe.lava.awoocord.bocchi
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import com.aliucord.annotations.AliucordPlugin
|
|
||||||
import com.aliucord.entities.Plugin
|
|
||||||
import com.aliucord.patcher.*
|
|
||||||
import com.aliucord.utils.accessField
|
|
||||||
import com.discord.api.message.MessageTypes
|
|
||||||
import com.discord.models.message.Message
|
|
||||||
import com.discord.utilities.view.text.SimpleDraweeSpanTextView
|
|
||||||
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
|
|
||||||
import com.discord.widgets.chat.list.entries.ChatListEntry
|
|
||||||
import com.discord.widgets.chat.list.entries.MessageEntry
|
|
||||||
import com.discord.widgets.chat.list.model.WidgetChatListModelMessages
|
|
||||||
|
|
||||||
private val WidgetChatListAdapterItemMessage.itemText by accessField<SimpleDraweeSpanTextView>()
|
|
||||||
|
|
||||||
@AliucordPlugin(requiresRestart = true)
|
|
||||||
@Suppress("unused")
|
|
||||||
class Bocchi : Plugin() {
|
|
||||||
override fun start(context: Context) {
|
|
||||||
patcher.after<WidgetChatListAdapterItemMessage>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.java,
|
|
||||||
ChatListEntry::class.java,
|
|
||||||
) { (_, _: Int, entry: MessageEntry) ->
|
|
||||||
if (entry.type == ChatListEntry.MESSAGE_MINIMAL && entry.message.content.isNullOrEmpty()) {
|
|
||||||
itemText.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patcher.instead<WidgetChatListModelMessages.Companion>(
|
|
||||||
"shouldConcatMessage",
|
|
||||||
WidgetChatListModelMessages.Items::class.java,
|
|
||||||
Message::class.java,
|
|
||||||
Message::class.java,
|
|
||||||
) { (_, items: WidgetChatListModelMessages.Items, message: Message, message2: Message?) ->
|
|
||||||
val timeDiff = (message.timestamp?.g() ?: 0L) - (message2?.timestamp?.g() ?: 0L)
|
|
||||||
return@instead !(
|
|
||||||
message2 == null ||
|
|
||||||
message2.isSystemMessage ||
|
|
||||||
message.hasThread() ||
|
|
||||||
message2.hasThread() ||
|
|
||||||
message.type !in arrayOf(MessageTypes.DEFAULT, MessageTypes.LOCAL) ||
|
|
||||||
message.author.id != message2.author.id ||
|
|
||||||
timeDiff >= 420000 || // WidgetChatListModelMessages.MESSAGE_CONCAT_TIMESTAMP_DELTA_THRESHOLD
|
|
||||||
// items.listItemMostRecentlyAdded.type !in arrayOf(0, 1, 4, 21) ||
|
|
||||||
// message2.hasAttachments() ||
|
|
||||||
// message2.hasEmbeds() ||
|
|
||||||
// message2.mentions?.isNotEmpty() == true ||
|
|
||||||
// message.mentions?.isNotEmpty() == true ||
|
|
||||||
// message.hasAttachments() ||
|
|
||||||
// message.hasEmbeds() ||
|
|
||||||
// items.concatCount >= 5 ||
|
|
||||||
(message.isWebhook && message.author?.username != message2.author.username)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
version = "1.0.0"
|
|
||||||
description = "Bubbled messages"
|
|
||||||
|
|
||||||
aliucord {
|
|
||||||
// Changelog of your plugin
|
|
||||||
changelog.set("""
|
|
||||||
# 1.0.0
|
|
||||||
* Initial release >w<
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
deploy.set(true)
|
|
||||||
}
|
|
||||||
|
|
@ -1,475 +0,0 @@
|
||||||
package moe.lava.awoocord.crocosmia
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.GONE
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
|
||||||
import com.aliucord.PluginManager
|
|
||||||
import com.aliucord.Utils
|
|
||||||
import com.aliucord.annotations.AliucordPlugin
|
|
||||||
import com.aliucord.api.SettingsAPI
|
|
||||||
import com.aliucord.entities.Plugin
|
|
||||||
import com.aliucord.patcher.*
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.ViewUtils.addTo
|
|
||||||
import com.aliucord.utils.ViewUtils.findViewById
|
|
||||||
import com.aliucord.utils.accessField
|
|
||||||
import com.discord.databinding.WidgetChatListAdapterItemBotComponentRowBinding
|
|
||||||
import com.discord.databinding.WidgetChatListAdapterItemEmbedBinding
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.discord.utilities.display.DisplayUtils
|
|
||||||
import com.discord.utilities.embed.EmbedResourceUtils
|
|
||||||
import com.discord.widgets.chat.list.adapter.*
|
|
||||||
import com.discord.widgets.chat.list.entries.*
|
|
||||||
import com.google.android.material.card.MaterialCardView
|
|
||||||
import com.google.android.material.shape.CornerFamily
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
|
||||||
import java.util.WeakHashMap
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
private val padding get() = 12.dp
|
|
||||||
//private val topPad get() = 14.dp
|
|
||||||
private val topPad get() = 6.dp
|
|
||||||
private val bigCorner get() = 24.dp.toFloat()
|
|
||||||
private val smallCorner get() = 4.dp.toFloat()
|
|
||||||
private val ChatListEntry.connectBefore get() = this.type in arrayOf(
|
|
||||||
ChatListEntry.MESSAGE_MINIMAL,
|
|
||||||
ChatListEntry.MESSAGE_EMBED,
|
|
||||||
ChatListEntry.MESSAGE_ATTACHMENT,
|
|
||||||
ChatListEntry.STICKER,
|
|
||||||
ChatListEntry.BOT_UI_COMPONENT,
|
|
||||||
101,
|
|
||||||
)
|
|
||||||
private val ChatListEntry.excepted get() = this.type in arrayOf(
|
|
||||||
ChatListEntry.REACTIONS,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val WidgetChatListAdapterItemBotComponentRow.binding by accessField<WidgetChatListAdapterItemBotComponentRowBinding>()
|
|
||||||
private val WidgetChatListAdapterItemAttachment.binding get() = WidgetChatListAdapterItemAttachment.`access$getBinding$p`(this)
|
|
||||||
private val WidgetChatListAdapterItemEmbed.binding by accessField<WidgetChatListAdapterItemEmbedBinding>()
|
|
||||||
private val WidgetChatListAdapterItemSticker.binding get() = WidgetChatListAdapterItemSticker.`access$getBinding$p`(this)
|
|
||||||
|
|
||||||
private var MessageEntry.keyField by accessField<String>()
|
|
||||||
|
|
||||||
private val fullId = Utils.getResId("widget_chat_list_adapter_item_text", "layout")
|
|
||||||
private val minimalId = Utils.getResId("widget_chat_list_adapter_item_minimal", "layout")
|
|
||||||
private val bubbleId = View.generateViewId()
|
|
||||||
|
|
||||||
private const val messageLayoutTag = R.f.message // Just some random id
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
@AliucordPlugin
|
|
||||||
class Crocosmia : Plugin() {
|
|
||||||
private fun createBubble(context: Context, parentHandler: View? = null): MaterialCardView {
|
|
||||||
return MaterialCardView(context).apply {
|
|
||||||
id = bubbleId
|
|
||||||
setCardBackgroundColor(
|
|
||||||
ColorCompat.getThemedColor(
|
|
||||||
this,
|
|
||||||
R.b.colorBackgroundSecondary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
parentHandler?.let { parent ->
|
|
||||||
setOnClickListener { parent.performClick() }
|
|
||||||
setOnLongClickListener { parent.performLongClick() }
|
|
||||||
}
|
|
||||||
elevation = 0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun WidgetChatListItem.configBubble(entry: ChatListEntry) {
|
|
||||||
itemView.findViewById<MaterialCardView>(bubbleId)?.let {
|
|
||||||
configBubble(it, entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun WidgetChatListItem.configBubble(view: MaterialCardView, entry: ChatListEntry) {
|
|
||||||
val idx = adapter.data.list.indexOf(entry)
|
|
||||||
val previousEntry = adapter.data.list.getOrNull(idx + 1)
|
|
||||||
val nextEntry = if (idx < 1) null else adapter.data.list[idx - 1]
|
|
||||||
view.shapeAppearanceModel = view.shapeAppearanceModel.toBuilder().run {
|
|
||||||
setAllCorners(CornerFamily.ROUNDED, bigCorner)
|
|
||||||
if (entry.connectBefore && previousEntry?.excepted != true) {
|
|
||||||
setTopLeftCornerSize(smallCorner)
|
|
||||||
setTopRightCornerSize(smallCorner)
|
|
||||||
}
|
|
||||||
if (nextEntry?.connectBefore == true) {
|
|
||||||
setBottomLeftCornerSize(smallCorner)
|
|
||||||
setBottomRightCornerSize(smallCorner)
|
|
||||||
}
|
|
||||||
build()
|
|
||||||
}
|
|
||||||
view.clipToOutline = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun load(context: Context) {
|
|
||||||
hasCompactMode = PluginManager.isPluginEnabled("CompactMode")
|
|
||||||
hasHighlightMessages = PluginManager.isPluginEnabled("HighlightOwnMessages")
|
|
||||||
if (hasCompactMode) {
|
|
||||||
logger.info("Enabling compatibility with CompactMode")
|
|
||||||
compactCompatOverride = SettingsAPI("CompactMode").getInt("contentMargin", 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun compatHighlightMessages() {
|
|
||||||
val cls = try {
|
|
||||||
val cl = PluginManager.plugins["HighlightOwnMessages"]!!.javaClass
|
|
||||||
val loader = cl.classLoader!!
|
|
||||||
loader.loadClass(
|
|
||||||
$$$"cloudburst.plugins.highlightownmessages.HighlightOwnMessages$$ExternalSyntheticLambda0"
|
|
||||||
)
|
|
||||||
} catch(e: Throwable) {
|
|
||||||
logger.warn("Tried to enable compatibility with HighlightOwnMessages, but no lambda class found", e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.info("Enabling compatibility with HighlightOwnMessages")
|
|
||||||
val method = cls.getDeclaredMethod("call", Object::class.java)
|
|
||||||
patcher.patch(method) { mparam ->
|
|
||||||
val param = mparam.args[0] as XC_MethodHook.MethodHookParam
|
|
||||||
val self = param.thisObject as? WidgetChatListAdapterItemMessage
|
|
||||||
?: return@patch logger.warn("Failed to cast thisObject (found: ${param.thisObject.javaClass.name})")
|
|
||||||
self.run {
|
|
||||||
val isFull = itemView.getTag(messageLayoutTag) as? Boolean
|
|
||||||
?: return@patch
|
|
||||||
itemView.findViewById<View>("chat_list_adapter_item_text").apply {
|
|
||||||
layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
if (isFull) {
|
|
||||||
setPadding(padding, 0, padding, padding)
|
|
||||||
} else {
|
|
||||||
setPadding(padding, padding + 2.dp, padding, padding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop(context: Context) { patcher.unpatchAll() }
|
|
||||||
|
|
||||||
var hasCompactMode = false
|
|
||||||
var compactCompatOverride: Int? = null
|
|
||||||
var hasHighlightMessages = false
|
|
||||||
|
|
||||||
override fun start(context: Context) {
|
|
||||||
patcher.after<WidgetChatListAdapter>(
|
|
||||||
"setData",
|
|
||||||
WidgetChatListAdapter.Data::class.java,
|
|
||||||
) {
|
|
||||||
notifyItemChanged(1, Unit.a)
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.after<WidgetChatListAdapterItemEmbed>(
|
|
||||||
WidgetChatListAdapter::class.java,
|
|
||||||
) {
|
|
||||||
binding.a.layoutParams = binding.a.layoutParams.apply {
|
|
||||||
width = MATCH_PARENT
|
|
||||||
}
|
|
||||||
(binding.f.getChildAt(0) as? ConstraintLayout)?.run {
|
|
||||||
layoutParams = (layoutParams as FrameLayout.LayoutParams).apply {
|
|
||||||
width = WRAP_CONTENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.f.setPadding(padding, padding, padding, padding)
|
|
||||||
binding.f.layoutParams = (binding.f.layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
marginEnd = binding.f.resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.instead<EmbedResourceUtils>(
|
|
||||||
"computeMaximumImageWidthPx",
|
|
||||||
Context::class.java,
|
|
||||||
) { (_, context: Context) ->
|
|
||||||
val res = context.resources
|
|
||||||
val screenWidth = DisplayUtils.getScreenSize(context).width()
|
|
||||||
val space = res.getDimensionPixelSize(R.d.uikit_guideline_chat) + res.getDimensionPixelSize(R.d.chat_cell_horizontal_spacing_total) + padding * 2
|
|
||||||
return@instead min(1440, screenWidth - space);
|
|
||||||
}
|
|
||||||
|
|
||||||
patchEmbed()
|
|
||||||
patchAttachmentInit()
|
|
||||||
patchAttachmentConfig()
|
|
||||||
patchComponentsConfig()
|
|
||||||
patchMessageInit()
|
|
||||||
patchMessageConfig()
|
|
||||||
patchStickerInit()
|
|
||||||
patchStickerConfig()
|
|
||||||
patchPollConfig()
|
|
||||||
|
|
||||||
if (hasHighlightMessages) {
|
|
||||||
compatHighlightMessages()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchAttachmentConfig() {
|
|
||||||
patcher.after<WidgetChatListAdapterItemAttachment>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
ChatListEntry::class.java,
|
|
||||||
) { (_, _: Int, entry: AttachmentEntry) ->
|
|
||||||
configBubble(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchAttachmentInit() {
|
|
||||||
patcher.after<WidgetChatListAdapterItemAttachment>(
|
|
||||||
WidgetChatListAdapter::class.java,
|
|
||||||
) {
|
|
||||||
val mediaView = binding.h
|
|
||||||
mediaView.layoutParams =
|
|
||||||
(mediaView.layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
topMargin = padding
|
|
||||||
bottomMargin = padding
|
|
||||||
marginStart = padding
|
|
||||||
marginEnd = padding
|
|
||||||
}
|
|
||||||
itemView.layoutParams = (itemView.layoutParams as ViewGroup.MarginLayoutParams).apply {
|
|
||||||
bottomMargin = 2.dp
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.d.radius = 0f
|
|
||||||
binding.d.elevation = 0f
|
|
||||||
binding.d.strokeWidth = 0
|
|
||||||
binding.d.setCardBackgroundColor(Color.TRANSPARENT)
|
|
||||||
|
|
||||||
createBubble(itemView.context, binding.a).addTo(itemView as ConstraintLayout, 1) {
|
|
||||||
layoutParams = ConstraintLayout.LayoutParams(0, 0).apply {
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
bottomToBottom = PARENT_ID
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
marginStart = compactCompatOverride?.dp
|
|
||||||
?: resources.getDimension(R.d.uikit_guideline_chat).toInt()
|
|
||||||
marginEnd = resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val marked = WeakHashMap<LinearLayout, Unit>()
|
|
||||||
private fun patchComponentsConfig() {
|
|
||||||
patcher.after<WidgetChatListAdapterItemBotComponentRow>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
ChatListEntry::class.java,
|
|
||||||
) { (_, _: Int, entry: BotUiComponentEntry) ->
|
|
||||||
var i = 0
|
|
||||||
val layout = binding.b
|
|
||||||
layout.layoutParams = (layout.layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
marginEnd = layout.resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt()
|
|
||||||
}
|
|
||||||
while (i < layout.childCount) {
|
|
||||||
val child = layout.getChildAt(i)
|
|
||||||
?: break
|
|
||||||
val bubble: MaterialCardView
|
|
||||||
if (child.javaClass.simpleName == "ContainerComponentView") {
|
|
||||||
bubble = (child as? ConstraintLayout)?.getChildAt(0) as? MaterialCardView
|
|
||||||
?: continue
|
|
||||||
if (i == (layout.childCount - 1)) {
|
|
||||||
((bubble.getChildAt(0) as? ConstraintLayout)?.getChildAt(1) as? LinearLayout)?.run {
|
|
||||||
if (!marked.contains(this)) {
|
|
||||||
marked[this] = Unit.a
|
|
||||||
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom + padding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
layout.removeViewAt(i)
|
|
||||||
bubble = createBubble(itemView.context).addTo(layout, i) {
|
|
||||||
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
|
||||||
child.addTo(this) {
|
|
||||||
layoutParams = (layoutParams as LinearLayout.LayoutParams).apply {
|
|
||||||
topMargin += padding
|
|
||||||
bottomMargin += padding
|
|
||||||
rightMargin += padding
|
|
||||||
leftMargin += padding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bubble.setOnClickListener {
|
|
||||||
adapter.eventHandler.onMessageClicked(entry.message, false)
|
|
||||||
}
|
|
||||||
bubble.setOnLongClickListener {
|
|
||||||
adapter.eventHandler.onMessageLongClicked(entry.message, "", false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bubble.shapeAppearanceModel = bubble.shapeAppearanceModel.toBuilder().run {
|
|
||||||
setAllCorners(CornerFamily.ROUNDED, smallCorner)
|
|
||||||
if (i == (layout.childCount - 1)) {
|
|
||||||
setBottomLeftCornerSize(bigCorner)
|
|
||||||
setBottomRightCornerSize(bigCorner)
|
|
||||||
}
|
|
||||||
build()
|
|
||||||
}
|
|
||||||
bubble.clipToOutline = true
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchEmbed() {
|
|
||||||
patcher.after<WidgetChatListAdapterItemEmbed>(
|
|
||||||
WidgetChatListAdapter::class.java,
|
|
||||||
) { (_) ->
|
|
||||||
binding.t.layoutParams =
|
|
||||||
(binding.t.layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
topMargin = padding
|
|
||||||
bottomMargin = padding
|
|
||||||
marginStart = padding
|
|
||||||
marginEnd = padding
|
|
||||||
}
|
|
||||||
createBubble(itemView.context, binding.a).addTo(itemView as ConstraintLayout, 1) {
|
|
||||||
visibility = GONE
|
|
||||||
layoutParams = ConstraintLayout.LayoutParams(0, 0).apply {
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
topToTop = PARENT_ID
|
|
||||||
bottomToBottom = PARENT_ID
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
marginStart = compactCompatOverride?.dp
|
|
||||||
?: resources.getDimension(R.d.uikit_guideline_chat).toInt()
|
|
||||||
marginEnd = resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patcher.after<WidgetChatListAdapterItemEmbed>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
ChatListEntry::class.java,
|
|
||||||
) { (_, _: Int, entry: EmbedEntry) ->
|
|
||||||
if (EmbedResourceUtils.INSTANCE.isInlineEmbed(entry.embed)) {
|
|
||||||
itemView.findViewById<View>(bubbleId).visibility = View.VISIBLE
|
|
||||||
configBubble(entry)
|
|
||||||
} else {
|
|
||||||
itemView.findViewById<View>(bubbleId).visibility = View.GONE
|
|
||||||
configBubble(binding.f, entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchMessageInit() {
|
|
||||||
patcher.after<WidgetChatListAdapterItemMessage>(
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
WidgetChatListAdapter::class.java,
|
|
||||||
) { (_, layoutId: Int) ->
|
|
||||||
val isFull = when (layoutId) {
|
|
||||||
fullId -> !hasCompactMode
|
|
||||||
minimalId -> false
|
|
||||||
else -> return@after
|
|
||||||
}
|
|
||||||
|
|
||||||
itemView.layoutParams = (itemView.layoutParams as ViewGroup.MarginLayoutParams).apply {
|
|
||||||
bottomMargin = 2.dp
|
|
||||||
}
|
|
||||||
itemView.setTag(messageLayoutTag, isFull)
|
|
||||||
if (isFull) {
|
|
||||||
itemView.findViewById<View?>("chat_list_adapter_item_text_header")?.apply {
|
|
||||||
layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
setPadding(
|
|
||||||
paddingLeft + padding,
|
|
||||||
paddingTop + topPad,
|
|
||||||
paddingRight + padding,
|
|
||||||
paddingBottom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
itemView.findViewById<View>("chat_list_adapter_item_text").apply {
|
|
||||||
layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
|
|
||||||
if (isFull) {
|
|
||||||
setPadding(padding, 0, padding, padding)
|
|
||||||
} else {
|
|
||||||
setPadding(padding, padding + 2.dp, padding, padding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createBubble(itemView.context, itemView).addTo(itemView as ConstraintLayout, 2) {
|
|
||||||
layoutParams = ConstraintLayout.LayoutParams(0, 0).apply {
|
|
||||||
if (isFull) {
|
|
||||||
startToStart = Utils.getResId("uikit_chat_guideline", "id")
|
|
||||||
topToTop = Utils.getResId("chat_list_adapter_item_text_header", "id")
|
|
||||||
} else {
|
|
||||||
startToStart = PARENT_ID
|
|
||||||
topToTop = Utils.getResId("chat_list_adapter_item_text", "id")
|
|
||||||
marginStart = compactCompatOverride?.dp
|
|
||||||
?: resources.getDimension(R.d.uikit_guideline_chat).toInt()
|
|
||||||
}
|
|
||||||
bottomToBottom = PARENT_ID
|
|
||||||
endToEnd = PARENT_ID
|
|
||||||
marginEnd = resources.getDimension(R.d.chat_cell_horizontal_spacing_total).toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchMessageConfig() {
|
|
||||||
patcher.after<WidgetChatListAdapterItemMessage>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
ChatListEntry::class.java,
|
|
||||||
) { (_, _: Int, entry: MessageEntry) ->
|
|
||||||
if (entry.message.content.isNullOrEmpty()) {
|
|
||||||
itemView.findViewById<View>("chat_list_adapter_item_text").visibility = View.GONE
|
|
||||||
}
|
|
||||||
configBubble(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchStickerInit() {
|
|
||||||
patcher.after<WidgetChatListAdapterItemSticker>(
|
|
||||||
WidgetChatListAdapter::class.java,
|
|
||||||
) {
|
|
||||||
binding.b.layoutParams = (binding.b.layoutParams as FrameLayout.LayoutParams).apply {
|
|
||||||
topMargin = padding
|
|
||||||
bottomMargin = padding
|
|
||||||
marginStart = padding
|
|
||||||
marginEnd = padding
|
|
||||||
}
|
|
||||||
binding.a.layoutParams = binding.a.layoutParams.apply {
|
|
||||||
width = MATCH_PARENT
|
|
||||||
}
|
|
||||||
binding.a.removeView(binding.b)
|
|
||||||
createBubble(itemView.context, binding.b).addTo(binding.a, 0) {
|
|
||||||
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
|
||||||
binding.b.addTo(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun patchStickerConfig() {
|
|
||||||
patcher.after<WidgetChatListAdapterItemSticker>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
ChatListEntry::class.java,
|
|
||||||
) { (_, _: Int, entry: StickerEntry) ->
|
|
||||||
configBubble(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollClass = try {
|
|
||||||
Class.forName("com.aliucord.coreplugins.polls.chatview.WidgetChatListAdapterItemPoll")
|
|
||||||
} catch(_: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
private val pollField = pollClass?.getDeclaredField("pollView")?.apply { isAccessible = true }
|
|
||||||
private fun patchPollConfig() {
|
|
||||||
if (pollClass == null) return
|
|
||||||
patcher.patch(pollClass.getDeclaredMethod(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
ChatListEntry::class.java,
|
|
||||||
)) { (param, _: Int, entry: ChatListEntry) ->
|
|
||||||
val view = pollField?.get(param.thisObject) as? MaterialCardView
|
|
||||||
view?.let {
|
|
||||||
(param.thisObject as WidgetChatListItem).configBubble(it, entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
version = "1.0.0"
|
|
||||||
description = "Backports DM previews"
|
|
||||||
|
|
||||||
aliucord {
|
|
||||||
// Changelog of your plugin
|
|
||||||
changelog.set("""
|
|
||||||
# 1.0.0
|
|
||||||
* Initial release >w<
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
deploy.set(true)
|
|
||||||
}
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
package moe.lava.awoocord.myosotis
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.aliucord.Http
|
|
||||||
import com.aliucord.Utils
|
|
||||||
import com.aliucord.annotations.AliucordPlugin
|
|
||||||
import com.aliucord.api.GatewayAPI
|
|
||||||
import com.aliucord.entities.Plugin
|
|
||||||
import com.aliucord.patcher.after
|
|
||||||
import com.aliucord.patcher.before
|
|
||||||
import com.aliucord.patcher.component1
|
|
||||||
import com.aliucord.patcher.component2
|
|
||||||
import com.aliucord.patcher.component3
|
|
||||||
import com.aliucord.utils.ChannelUtils
|
|
||||||
import com.aliucord.utils.GsonUtils
|
|
||||||
import com.aliucord.utils.SerializedName
|
|
||||||
import com.aliucord.utils.accessField
|
|
||||||
import com.aliucord.wrappers.ChannelWrapper.Companion.id
|
|
||||||
import com.aliucord.wrappers.users.globalName
|
|
||||||
import com.discord.api.message.Message
|
|
||||||
import com.discord.databinding.WidgetChannelsListItemChannelPrivateBinding
|
|
||||||
import com.discord.models.domain.ModelMessageDelete
|
|
||||||
import com.discord.stores.StoreStream
|
|
||||||
import com.discord.utilities.color.ColorCompat
|
|
||||||
import com.discord.utilities.textprocessing.DiscordParser
|
|
||||||
import com.discord.utilities.textprocessing.MessagePreprocessor
|
|
||||||
import com.discord.utilities.textprocessing.MessageRenderContext
|
|
||||||
import com.discord.utilities.view.text.SimpleDraweeSpanTextView
|
|
||||||
import com.discord.widgets.channels.list.WidgetChannelsListAdapter
|
|
||||||
import com.discord.widgets.channels.list.items.ChannelListItem
|
|
||||||
import com.discord.widgets.channels.list.items.ChannelListItemPrivate
|
|
||||||
import com.discord.widgets.chat.list.adapter.`WidgetChatListAdapterItemMessage$getMessageRenderContext$1`
|
|
||||||
import com.discord.widgets.chat.list.adapter.`WidgetChatListAdapterItemMessage$getMessageRenderContext$4`
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.lytefast.flexinput.R
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
private val WidgetChannelsListAdapter.ItemChannelPrivate.binding
|
|
||||||
by accessField<WidgetChannelsListItemChannelPrivateBinding>()
|
|
||||||
|
|
||||||
private val responseType = TypeToken.getParameterized(List::class.java, Message::class.java).type
|
|
||||||
|
|
||||||
data class ChannelIdsPayload(
|
|
||||||
@SerializedName("channel_ids") val channelIds: List<Long>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MessageItem(
|
|
||||||
val id: Long,
|
|
||||||
val content: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Message.wrap(): MessageItem {
|
|
||||||
val author = this.e()
|
|
||||||
val authorName = if (author.id == StoreStream.getUsers().me.id) {
|
|
||||||
"You"
|
|
||||||
} else {
|
|
||||||
author.globalName ?: author.username
|
|
||||||
}
|
|
||||||
val content = this.i()
|
|
||||||
.takeIf { it.isNotEmpty() }
|
|
||||||
?.let { content -> "$authorName: ${content.takeWhile { it != '\n' }}" }
|
|
||||||
|
|
||||||
return MessageItem(
|
|
||||||
id = this.o(),
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SimpleDraweeSpanTextView.renderText(content: String, other: Pair<Long, String>) {
|
|
||||||
val me = StoreStream.getUsers().me
|
|
||||||
val meId = me.id
|
|
||||||
val meName = me.globalName ?: me.username
|
|
||||||
val processor = MessagePreprocessor(meId, listOf(), null, false, 50)
|
|
||||||
val parseChannelMessage = DiscordParser.parseChannelMessage(
|
|
||||||
context,
|
|
||||||
content,
|
|
||||||
MessageRenderContext(
|
|
||||||
context,
|
|
||||||
meId,
|
|
||||||
false,
|
|
||||||
mapOf(meId to meName, other),
|
|
||||||
StoreStream.getChannels().channelNames,
|
|
||||||
mapOf(),
|
|
||||||
R.b.colorTextLink,
|
|
||||||
`WidgetChatListAdapterItemMessage$getMessageRenderContext$1`.INSTANCE,
|
|
||||||
{ },
|
|
||||||
ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg),
|
|
||||||
ColorCompat.getThemedColor(context, R.b.theme_chat_spoiler_bg_visible),
|
|
||||||
{ },
|
|
||||||
{ },
|
|
||||||
`WidgetChatListAdapterItemMessage$getMessageRenderContext$4`(context)
|
|
||||||
),
|
|
||||||
processor,
|
|
||||||
DiscordParser.ParserOptions.DEFAULT,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
setDraweeSpanStringBuilder(parseChannelMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AliucordPlugin
|
|
||||||
class Myosotis : Plugin() {
|
|
||||||
var cache = mutableMapOf<Long, MessageItem>()
|
|
||||||
var adapterRef: WeakReference<WidgetChannelsListAdapter>? = null
|
|
||||||
|
|
||||||
override fun stop(context: Context) { patcher.unpatchAll() }
|
|
||||||
|
|
||||||
override fun start(context: Context) {
|
|
||||||
GatewayAPI.onEvent<Any>("READY") { refreshAll() }
|
|
||||||
GatewayAPI.onEvent<Any>("RESUMED") { refreshAll() }
|
|
||||||
|
|
||||||
patcher.after<WidgetChannelsListAdapter.ItemChannelPrivate>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.java,
|
|
||||||
ChannelListItem::class.java,
|
|
||||||
) { (_, _: Int, item: ChannelListItemPrivate) ->
|
|
||||||
cache[item.channel.id]?.let { msg ->
|
|
||||||
val content = msg.content
|
|
||||||
?: return@let
|
|
||||||
|
|
||||||
val descView = binding.d
|
|
||||||
descView.visibility = View.VISIBLE
|
|
||||||
val user = ChannelUtils.getDMRecipient(item.channel)
|
|
||||||
descView.renderText(content, user.id to (user.globalName ?: user.username))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patcher.after<WidgetChannelsListAdapter>(
|
|
||||||
RecyclerView::class.java,
|
|
||||||
FragmentManager::class.java,
|
|
||||||
) {
|
|
||||||
adapterRef = WeakReference(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.before<StoreStream>(
|
|
||||||
"handleMessageCreate",
|
|
||||||
Message::class.java
|
|
||||||
) { (_, msg: Message) ->
|
|
||||||
handleMessageUpdate(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.before<StoreStream>(
|
|
||||||
"handleMessageUpdate",
|
|
||||||
Message::class.java
|
|
||||||
) { (_, msg: Message) ->
|
|
||||||
handleMessageUpdate(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.before<StoreStream>(
|
|
||||||
"handleMessageDelete",
|
|
||||||
ModelMessageDelete::class.java
|
|
||||||
) { (_, deleteModel: ModelMessageDelete) ->
|
|
||||||
cache[deleteModel.channelId]?.let { msg ->
|
|
||||||
if (msg.id in deleteModel.messageIds) {
|
|
||||||
cache.remove(deleteModel.channelId)
|
|
||||||
rerender(deleteModel.channelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMessageUpdate(msg: Message) {
|
|
||||||
val gid = msg.m()
|
|
||||||
if (gid == null) {
|
|
||||||
val channelId = msg.g()
|
|
||||||
|
|
||||||
val oldMsgId = cache[channelId]?.id ?: 0
|
|
||||||
if (msg.o() > oldMsgId) {
|
|
||||||
cache[channelId] = msg.wrap()
|
|
||||||
rerender(channelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
private fun refreshAll() {
|
|
||||||
val channels = StoreStream.getChannels().getChannelsForGuild(0)
|
|
||||||
.filterValues { it.D() == 1 } // type == Type.DM
|
|
||||||
.keys.take(100)
|
|
||||||
Utils.threadPool.execute {
|
|
||||||
val res = Http.Request.newDiscordRNRequest("/channels/preload-messages", "POST")
|
|
||||||
.executeWithJson(ChannelIdsPayload(channels))
|
|
||||||
.json<List<Message>>(GsonUtils.gsonRestApi, responseType)
|
|
||||||
cache = mutableMapOf(*res.map { it.g() to it.wrap() }.toTypedArray())
|
|
||||||
|
|
||||||
Utils.mainThread.post {
|
|
||||||
@SuppressLint("NotifyDataSetChanged") // I DONT CARE HAHAHAAHJAHAAJHDLAHD
|
|
||||||
adapterRef?.get()?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rerender(id: Long) {
|
|
||||||
val adapter = adapterRef?.get() ?: return
|
|
||||||
val idx = adapter.internalData.indexOfFirst { it.key == "3$id" }
|
|
||||||
logger.info("found $idx for $id")
|
|
||||||
if (idx != -1) {
|
|
||||||
Utils.mainThread.post {
|
|
||||||
adapter.notifyItemChanged(idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
version = "1.4.0"
|
|
||||||
description = "Backported and improved search functionality"
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "moe.lava.awoocord.scout"
|
|
||||||
}
|
|
||||||
|
|
||||||
aliucord {
|
|
||||||
// Changelog of your plugin
|
|
||||||
changelog.set("""
|
|
||||||
!!! Minimum Aliucord version requirement {fixed}
|
|
||||||
======================
|
|
||||||
* Scout now requires Aliucord 2.4.0, please update before reporting issues.
|
|
||||||
|
|
||||||
Changelog {added marginTop}
|
|
||||||
======================
|
|
||||||
# 1.4.0 - Scout is searching for clues about the elusive MvM update
|
|
||||||
* Added the authorType filter option to search by user, bot, or webhook
|
|
||||||
* Moved sort filter to the top of the new ones
|
|
||||||
* Fixes a Discord bug where typing "mentions" would also suggest "has"
|
|
||||||
* Some people said the options were getting bloated, so they're all hidden behind a "Show all" button now. They'll still show up in auto suggestions.
|
|
||||||
|
|
||||||
# 1.3.0
|
|
||||||
* Removes empty discriminator when searching with users
|
|
||||||
|
|
||||||
# 1.2.2
|
|
||||||
* Fix possible rare crash related to thread searching
|
|
||||||
|
|
||||||
# 1.2.1
|
|
||||||
* Fixes off-looking thread icon
|
|
||||||
Only Discord will name an icon "thread_white_24dp", and it's neither white nor 24dp. Seriously, what were they thinking?
|
|
||||||
|
|
||||||
# 1.2.0 - Scout is in:to knitting
|
|
||||||
* Adds support for searching threads; simply use in:
|
|
||||||
|
|
||||||
# 1.1.3
|
|
||||||
* Patch to fix the biggggg top padding in results
|
|
||||||
|
|
||||||
# 1.1.2
|
|
||||||
* Fix month being one month behind after using the date picker
|
|
||||||
|
|
||||||
# 1.1.1
|
|
||||||
* Use proper icons for search filter suggestions
|
|
||||||
|
|
||||||
# 1.1.0 - Look out, Scout has:updates
|
|
||||||
* Add "has:forward" and "has:poll" filters
|
|
||||||
* Add "exclude:" filter. It is the opposite of "has:" and filters out matching elements
|
|
||||||
|
|
||||||
# 1.0.1
|
|
||||||
* Fix not being able to search more than one page with sort:old
|
|
||||||
|
|
||||||
# 1.0.0
|
|
||||||
* Initial release >w<
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
deploy.set(true)
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package moe.lava.awoocord.scout
|
|
||||||
|
|
||||||
import com.discord.utilities.search.query.FilterType
|
|
||||||
|
|
||||||
object FilterTypeExtension {
|
|
||||||
lateinit var EXPAND: FilterType
|
|
||||||
lateinit var SORT: FilterType
|
|
||||||
lateinit var BEFORE: FilterType
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package moe.lava.awoocord.scout
|
|
||||||
|
|
||||||
import com.discord.utilities.search.query.node.answer.HasAnswerOption
|
|
||||||
|
|
||||||
object HasAnswerOptionExtension {
|
|
||||||
lateinit var POLL: HasAnswerOption
|
|
||||||
lateinit var SNAPSHOT: HasAnswerOption
|
|
||||||
lateinit var values: Array<HasAnswerOption>
|
|
||||||
}
|
|
||||||
|
|
@ -1,940 +0,0 @@
|
||||||
@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
|
|
||||||
import com.aliucord.annotations.AliucordPlugin
|
|
||||||
import com.aliucord.entities.Plugin
|
|
||||||
import com.aliucord.patcher.PreHook
|
|
||||||
import com.aliucord.patcher.after
|
|
||||||
import com.aliucord.patcher.before
|
|
||||||
import com.aliucord.patcher.component1
|
|
||||||
import com.aliucord.patcher.component2
|
|
||||||
import com.aliucord.patcher.component3
|
|
||||||
import com.aliucord.patcher.component4
|
|
||||||
import com.aliucord.patcher.component5
|
|
||||||
import com.aliucord.patcher.instead
|
|
||||||
import com.aliucord.utils.DimenUtils.dp
|
|
||||||
import com.aliucord.utils.RxUtils.subscribe
|
|
||||||
import com.aliucord.utils.ViewUtils.findViewById
|
|
||||||
import com.aliucord.utils.accessField
|
|
||||||
import com.aliucord.wrappers.ChannelWrapper.Companion.id
|
|
||||||
import com.discord.BuildConfig
|
|
||||||
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
|
|
||||||
import com.discord.models.user.User
|
|
||||||
import com.discord.restapi.RequiredHeadersInterceptor
|
|
||||||
import com.discord.restapi.RestAPIBuilder
|
|
||||||
import com.discord.simpleast.core.parser.ParseSpec
|
|
||||||
import com.discord.simpleast.core.parser.Parser
|
|
||||||
import com.discord.simpleast.core.parser.Rule
|
|
||||||
import com.discord.stores.StoreSearch
|
|
||||||
import com.discord.stores.StoreSearchInput
|
|
||||||
import com.discord.stores.StoreStream
|
|
||||||
import com.discord.utilities.mg_recycler.MGRecyclerDataPayload
|
|
||||||
import com.discord.utilities.mg_recycler.SingleTypePayload
|
|
||||||
import com.discord.utilities.rest.RestAPI.AppHeadersProvider
|
|
||||||
import com.discord.utilities.search.network.`SearchFetcher$getRestObservable$3`
|
|
||||||
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.ChannelNode
|
|
||||||
import com.discord.utilities.search.query.node.answer.HasAnswerOption
|
|
||||||
import com.discord.utilities.search.query.node.answer.HasNode
|
|
||||||
import com.discord.utilities.search.query.node.answer.UserNode
|
|
||||||
import com.discord.utilities.search.query.node.content.ContentNode
|
|
||||||
import com.discord.utilities.search.query.node.filter.FilterNode
|
|
||||||
import com.discord.utilities.search.query.parsing.QueryParser
|
|
||||||
import com.discord.utilities.search.query.parsing.`QueryParser$Companion$getInAnswerRule$1`
|
|
||||||
import com.discord.utilities.search.strings.ContextSearchStringProvider
|
|
||||||
import com.discord.utilities.search.strings.SearchStringProvider
|
|
||||||
import com.discord.utilities.search.suggestion.SearchSuggestionEngine
|
|
||||||
import com.discord.utilities.search.suggestion.entries.ChannelSuggestion
|
|
||||||
import com.discord.utilities.search.suggestion.entries.FilterSuggestion
|
|
||||||
import com.discord.utilities.search.suggestion.entries.HasSuggestion
|
|
||||||
import com.discord.utilities.search.suggestion.entries.SearchSuggestion
|
|
||||||
import com.discord.utilities.search.validation.SearchData
|
|
||||||
import com.discord.widgets.search.results.WidgetSearchResults
|
|
||||||
import com.discord.widgets.search.suggestions.WidgetSearchSuggestions
|
|
||||||
import com.discord.widgets.search.suggestions.`WidgetSearchSuggestions$configureUI$1`
|
|
||||||
import com.discord.widgets.search.suggestions.WidgetSearchSuggestionsAdapter
|
|
||||||
import com.franmontiel.persistentcookiejar.PersistentCookieJar
|
|
||||||
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
|
|
||||||
import moe.lava.awoocord.scout.parsing.UserIdNode
|
|
||||||
import moe.lava.awoocord.scout.ui.DatePickerFragment
|
|
||||||
import moe.lava.awoocord.scout.ui.ScoutResource
|
|
||||||
import moe.lava.awoocord.scout.ui.ScoutSearchStringProvider
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
import b.a.k.b as FormatUtils
|
|
||||||
|
|
||||||
private val WidgetSearchSuggestionsAdapter.FilterViewHolder.binding
|
|
||||||
by accessField<WidgetSearchSuggestionsItemSuggestionBinding>()
|
|
||||||
|
|
||||||
private val WidgetSearchSuggestionsAdapter.HeaderViewHolder.binding
|
|
||||||
by accessField<WidgetSearchSuggestionItemHeaderBinding>()
|
|
||||||
|
|
||||||
@AliucordPlugin
|
|
||||||
@Suppress("unused", "unchecked_cast")
|
|
||||||
class Scout : Plugin() {
|
|
||||||
lateinit var scoutRes: ScoutResource
|
|
||||||
lateinit var ssProvider: ScoutSearchStringProvider
|
|
||||||
lateinit var searchApi: SearchAPIInterface
|
|
||||||
|
|
||||||
var optionsExpanded = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
needsResources = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun load(context: Context) {
|
|
||||||
scoutRes = ScoutResource(resources!!)
|
|
||||||
ssProvider = ScoutSearchStringProvider(context)
|
|
||||||
searchApi = buildSearchApi(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start(context: Context) {
|
|
||||||
extendFilterType()
|
|
||||||
extendHasAnswerOption()
|
|
||||||
extendSuggestionCategory()
|
|
||||||
fixFiltersKeying()
|
|
||||||
fixHasFilterSuggestion()
|
|
||||||
fixSearchPadding()
|
|
||||||
patchHasAnswerOption()
|
|
||||||
patchHasNode()
|
|
||||||
patchQuery()
|
|
||||||
patchQueryParser()
|
|
||||||
patchSearchUI(context)
|
|
||||||
patchThreadSupport()
|
|
||||||
patchUsernameDiscriminator()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop(context: Context) {
|
|
||||||
patcher.unpatchAll()
|
|
||||||
resetFilterType()
|
|
||||||
resetHasAnswerOption()
|
|
||||||
resetSuggestionCategory()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new custom search API implementation, for the extra `min_id` param in search queries
|
|
||||||
private fun buildSearchApi(context: Context): SearchAPIInterface {
|
|
||||||
val appHeadersProvider = AppHeadersProvider.INSTANCE
|
|
||||||
val requiredHeadersInterceptor = RequiredHeadersInterceptor(appHeadersProvider)
|
|
||||||
val persistentCookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context))
|
|
||||||
val restAPIBuilder = RestAPIBuilder(BuildConfig.HOST_API, persistentCookieJar)
|
|
||||||
|
|
||||||
return RestAPIBuilder.`build$default`(
|
|
||||||
restAPIBuilder,
|
|
||||||
SearchAPIInterface::class.java,
|
|
||||||
false,
|
|
||||||
0L,
|
|
||||||
listOf(requiredHeadersInterceptor),
|
|
||||||
"client_base",
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
102,
|
|
||||||
null
|
|
||||||
) as SearchAPIInterface
|
|
||||||
}
|
|
||||||
|
|
||||||
private var origFilterTypes: Array<FilterType>? = null
|
|
||||||
// Creates new pseudo-values of the `FilterType` enum for date filters
|
|
||||||
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
|
|
||||||
private fun extendFilterType() {
|
|
||||||
val cls = FilterType::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<FilterType>
|
|
||||||
origFilterTypes = origFilterTypes ?: values
|
|
||||||
var nextIdx = values.size
|
|
||||||
|
|
||||||
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.filters = arrayOf(SORT, AUTHOR_TYPE, EXCLUDE) + FilterTypeExtension.dates
|
|
||||||
FilterTypeExtension.values = arrayOf(EXPAND) + FilterTypeExtension.filters
|
|
||||||
|
|
||||||
val newValues = values.toMutableList()
|
|
||||||
newValues.addAll(FilterTypeExtension.values)
|
|
||||||
field.set(null, newValues.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resetFilterType() {
|
|
||||||
if (origFilterTypes == null)
|
|
||||||
return logger.error("No unpatched filter types?", null)
|
|
||||||
|
|
||||||
val cls = FilterType::class.java
|
|
||||||
val field = cls.getDeclaredField("\$VALUES")
|
|
||||||
field.isAccessible = true
|
|
||||||
field.set(null, origFilterTypes)
|
|
||||||
origFilterTypes = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private var origHasAnswerOptions: Array<HasAnswerOption>? = null
|
|
||||||
// Creates new pseudo-values of the `HasAnswerOption` enum for poll and forwarded filters
|
|
||||||
@Suppress("LocalVariableName", "AssignedValueIsNeverRead")
|
|
||||||
private fun extendHasAnswerOption() {
|
|
||||||
val cls = HasAnswerOption::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<HasAnswerOption>
|
|
||||||
origHasAnswerOptions = origHasAnswerOptions ?: values
|
|
||||||
var nextIdx = values.size
|
|
||||||
|
|
||||||
val POLL = constructor.newInstance("POLL", nextIdx++, "poll") as HasAnswerOption
|
|
||||||
val SNAPSHOT = constructor.newInstance("SNAPSHOT", nextIdx++, "snapshot") as HasAnswerOption
|
|
||||||
HasAnswerOptionExtension.POLL = POLL
|
|
||||||
HasAnswerOptionExtension.SNAPSHOT = SNAPSHOT
|
|
||||||
HasAnswerOptionExtension.values = arrayOf(POLL, SNAPSHOT)
|
|
||||||
|
|
||||||
val newValues = values.toMutableList()
|
|
||||||
newValues.addAll(HasAnswerOptionExtension.values)
|
|
||||||
field.set(null, newValues.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resetHasAnswerOption() {
|
|
||||||
if (origHasAnswerOptions == null)
|
|
||||||
return logger.error("No unpatched 'has' options?", null)
|
|
||||||
|
|
||||||
val cls = HasAnswerOption::class.java
|
|
||||||
val field = cls.getDeclaredField("\$VALUES")
|
|
||||||
field.isAccessible = true
|
|
||||||
field.set(null, origHasAnswerOptions)
|
|
||||||
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() {
|
|
||||||
patcher.instead<WidgetSearchSuggestionsAdapter.Companion>(
|
|
||||||
"getFilterItem",
|
|
||||||
FilterSuggestion::class.java,
|
|
||||||
) { (_, suggestion: FilterSuggestion) ->
|
|
||||||
SingleTypePayload(suggestion, suggestion.filterType.name, 2) // 2 = WidgetSearchSuggestionsAdapter.TYPE_FILTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// YES DISCORD TYPO'ED THIS HAHAHAHAHAHAFAUHFAIUFHAIFBHUKFHYRISFSUOIRN
|
|
||||||
private fun fixHasFilterSuggestion() {
|
|
||||||
patcher.before<FilterSuggestion.Companion>(
|
|
||||||
"getStringRepresentation",
|
|
||||||
FilterType::class.java,
|
|
||||||
SearchStringProvider::class.java,
|
|
||||||
) { (param, filter: FilterType, provider: SearchStringProvider) ->
|
|
||||||
if (filter == FilterType.HAS) {
|
|
||||||
param.result = provider.hasFilterString + ":"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch out the gigantic padding in search results
|
|
||||||
private fun fixSearchPadding() {
|
|
||||||
patcher.after<WidgetSearchResults>("onViewBound", View::class.java) {
|
|
||||||
view?.run {
|
|
||||||
fitsSystemWindows = false
|
|
||||||
setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.after<WidgetSearchSuggestions>("onViewBound", View::class.java) {
|
|
||||||
// Being a bit sneaky and reset the expanded flag here
|
|
||||||
optionsExpanded = false
|
|
||||||
view?.run {
|
|
||||||
fitsSystemWindows = false
|
|
||||||
setPadding(paddingLeft, 16.dp, paddingRight, paddingBottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patches various methods that use HasAnswerOption to include our new options
|
|
||||||
private fun patchHasAnswerOption() {
|
|
||||||
patcher.before<HasAnswerOption.Companion>(
|
|
||||||
"getOptionFromString",
|
|
||||||
String::class.java,
|
|
||||||
SearchStringProvider::class.java
|
|
||||||
) { param ->
|
|
||||||
val str = param.args[0] as String
|
|
||||||
if (str == ssProvider.hasPollString)
|
|
||||||
param.result = HasAnswerOptionExtension.POLL
|
|
||||||
else if (str == ssProvider.hasForwardString)
|
|
||||||
param.result = HasAnswerOptionExtension.SNAPSHOT
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.before<HasAnswerOption>(
|
|
||||||
"getLocalizedInputText",
|
|
||||||
SearchStringProvider::class.java
|
|
||||||
) { param ->
|
|
||||||
if (this == HasAnswerOptionExtension.POLL)
|
|
||||||
param.result = ssProvider.hasPollString
|
|
||||||
else if (this == HasAnswerOptionExtension.SNAPSHOT)
|
|
||||||
param.result = ssProvider.hasForwardString
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.instead<QueryParser.Companion>(
|
|
||||||
"createHasAnswerRegex",
|
|
||||||
SearchStringProvider::class.java
|
|
||||||
) { param ->
|
|
||||||
val ossProvider = param.args[0] as SearchStringProvider
|
|
||||||
|
|
||||||
val matches = HasAnswerOption.values().joinToString("|") { it.getLocalizedInputText(ossProvider) }
|
|
||||||
"^\\s*($matches)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch to set icons
|
|
||||||
patcher.before<WidgetSearchSuggestionsAdapter.HasViewHolder>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.java,
|
|
||||||
MGRecyclerDataPayload::class.java,
|
|
||||||
) { param ->
|
|
||||||
val suggestion = (param.args[1] as SingleTypePayload<HasSuggestion>).data
|
|
||||||
val option = suggestion.hasAnswerOption
|
|
||||||
|
|
||||||
val resID = when (option) {
|
|
||||||
HasAnswerOptionExtension.POLL -> "baseline_poll_24"
|
|
||||||
HasAnswerOptionExtension.SNAPSHOT -> "baseline_forward_to_inbox_24"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
resID?.let {
|
|
||||||
val bindingField = this::class.java.getDeclaredField("binding")
|
|
||||||
bindingField.isAccessible = true
|
|
||||||
val binding = bindingField.get(this) as WidgetSearchSuggestionsItemHasBinding
|
|
||||||
|
|
||||||
binding.d.text = option.getLocalizedInputText(null)
|
|
||||||
binding.b.setOnClickListener {
|
|
||||||
WidgetSearchSuggestionsAdapter.HasViewHolder.`access$getAdapter$p`(this).onHasClicked.invoke(option)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.c.setImageDrawable(scoutRes.getDrawable(it))
|
|
||||||
|
|
||||||
param.result = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patcher.instead<SearchSuggestionEngine>(
|
|
||||||
"getHasSuggestions",
|
|
||||||
CharSequence::class.java,
|
|
||||||
FilterType::class.java,
|
|
||||||
SearchStringProvider::class.java,
|
|
||||||
) { (_, 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) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
private fun patchHasNode() {
|
|
||||||
patcher.instead<HasNode>("getValidFilters") {
|
|
||||||
setOf(FilterTypeExtension.EXCLUDE, FilterType.HAS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch updateQuery to either include or exclude our has option
|
|
||||||
patcher.instead<HasNode>(
|
|
||||||
"updateQuery",
|
|
||||||
SearchQuery.Builder::class.java,
|
|
||||||
SearchData::class.java,
|
|
||||||
FilterType::class.java,
|
|
||||||
) { param ->
|
|
||||||
val builder = param.args[0] as SearchQuery.Builder?
|
|
||||||
val filterType = param.args[2] as FilterType
|
|
||||||
|
|
||||||
checkNotNull(builder) { "queryBuilder" }
|
|
||||||
|
|
||||||
val field = HasNode::class.java.getDeclaredField("hasAnswerOption")
|
|
||||||
field.isAccessible = true
|
|
||||||
val opt = field.get(this) as HasAnswerOption
|
|
||||||
|
|
||||||
if (filterType == FilterType.HAS)
|
|
||||||
builder.appendParam("has", opt.restParamValue)
|
|
||||||
else if (filterType == FilterTypeExtension.EXCLUDE)
|
|
||||||
builder.appendParam("has", "-" + opt.restParamValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patching the behaviour when the has suggestion is clicked
|
|
||||||
patcher.before<StoreSearchInput>(
|
|
||||||
"onHasClicked",
|
|
||||||
HasAnswerOption::class.java,
|
|
||||||
CharSequence::class.java,
|
|
||||||
CharSequence::class.java,
|
|
||||||
List::class.java,
|
|
||||||
) { param ->
|
|
||||||
val opt = param.args[0] as HasAnswerOption
|
|
||||||
val hasFilterText = param.args[1] as CharSequence
|
|
||||||
val filterAnswer = param.args[2] as CharSequence
|
|
||||||
val query = param.args[3] as List<QueryNode>
|
|
||||||
|
|
||||||
val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
|
|
||||||
"replaceAndPublish",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
List::class.java,
|
|
||||||
List::class.java
|
|
||||||
)
|
|
||||||
replaceAndPublish.isAccessible = true
|
|
||||||
|
|
||||||
val getAnswerReplacementStart = StoreSearchInput::class.java.getDeclaredMethod(
|
|
||||||
"getAnswerReplacementStart",
|
|
||||||
List::class.java,
|
|
||||||
)
|
|
||||||
getAnswerReplacementStart.isAccessible = true
|
|
||||||
|
|
||||||
val replacementIdx = getAnswerReplacementStart.invoke(this, query) as Int
|
|
||||||
val previousFilterText = query[replacementIdx]
|
|
||||||
val filterNode = if (previousFilterText.text == ssProvider.excludeFilterString)
|
|
||||||
FilterNode(FilterTypeExtension.EXCLUDE, ssProvider.excludeFilterString)
|
|
||||||
else
|
|
||||||
FilterNode(FilterType.HAS, hasFilterText)
|
|
||||||
|
|
||||||
replaceAndPublish.invoke(this, replacementIdx, listOf(filterNode, HasNode(opt, filterAnswer)), query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patches the search query to also insert `min_id`, required for searching "after:" and "during:"
|
|
||||||
private fun patchQuery() {
|
|
||||||
patcher.patch(
|
|
||||||
`SearchFetcher$getRestObservable$3`::class.java.getDeclaredMethod("call", Integer::class.java),
|
|
||||||
PreHook { param ->
|
|
||||||
val self = param.thisObject as `SearchFetcher$getRestObservable$3`<*, *>
|
|
||||||
val retryAttempts = param.args[0] as Int?
|
|
||||||
val params = self.`$searchQuery`.params
|
|
||||||
|
|
||||||
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())
|
|
||||||
else
|
|
||||||
maxID = listOf(it.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
param.result = if (self.`$searchTarget`.type == StoreSearch.SearchTarget.Type.GUILD)
|
|
||||||
searchApi.searchGuildMessages(
|
|
||||||
self.`$searchTarget`.id,
|
|
||||||
minID,
|
|
||||||
maxID,
|
|
||||||
params["author_id"],
|
|
||||||
params["mentions"],
|
|
||||||
params["channel_id"],
|
|
||||||
params["has"],
|
|
||||||
params["content"],
|
|
||||||
retryAttempts,
|
|
||||||
self.`$searchQuery`.includeNsfw,
|
|
||||||
listOf("timestamp"),
|
|
||||||
sortOrder,
|
|
||||||
authorType,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
searchApi.searchChannelMessages(
|
|
||||||
self.`$searchTarget`.id,
|
|
||||||
minID,
|
|
||||||
maxID,
|
|
||||||
params["author_id"],
|
|
||||||
params["mentions"],
|
|
||||||
params["has"],
|
|
||||||
params["content"],
|
|
||||||
retryAttempts,
|
|
||||||
self.`$searchQuery`.includeNsfw,
|
|
||||||
listOf("timestamp"),
|
|
||||||
sortOrder,
|
|
||||||
authorType,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch parser for date parsing
|
|
||||||
private fun patchQueryParser() {
|
|
||||||
patcher.after<QueryParser>(SearchStringProvider::class.java) {
|
|
||||||
// We need to access and insert into the rules before the rest
|
|
||||||
val field = Parser::class.java.getDeclaredField("rules").apply { isAccessible = true }
|
|
||||||
val rules = field.get(this) as ArrayList<Rule<Context, QueryNode, Any>>
|
|
||||||
rules.addAll(0, listOf(
|
|
||||||
UserIdNode.getUserIdRule(),
|
|
||||||
DateNode.getBeforeRule(ssProvider.beforeFilterString),
|
|
||||||
DateNode.getDuringRule(ssProvider.duringFilterString),
|
|
||||||
DateNode.getAfterRule(ssProvider.afterFilterString),
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// Patch needed to support the new filter types
|
|
||||||
patcher.before<StoreSearchInput>(
|
|
||||||
"onFilterClicked",
|
|
||||||
FilterType::class.java,
|
|
||||||
SearchStringProvider::class.java,
|
|
||||||
List::class.java,
|
|
||||||
) { param ->
|
|
||||||
val filter = param.args[0] as FilterType
|
|
||||||
if (filter !in FilterTypeExtension.values)
|
|
||||||
return@before // Exit if not an extended filter type
|
|
||||||
|
|
||||||
val replaceAndPublish = StoreSearchInput::class.java.getDeclaredMethod(
|
|
||||||
"replaceAndPublish",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
List::class.java,
|
|
||||||
List::class.java
|
|
||||||
)
|
|
||||||
replaceAndPublish.isAccessible = true
|
|
||||||
|
|
||||||
val getAnswerReplacementStart = StoreSearchInput::class.java.getDeclaredMethod(
|
|
||||||
"getAnswerReplacementStart",
|
|
||||||
List::class.java,
|
|
||||||
)
|
|
||||||
getAnswerReplacementStart.isAccessible = true
|
|
||||||
|
|
||||||
// Original implementation
|
|
||||||
val filterNode = FilterNode(filter, ssProvider.stringFor(filter))
|
|
||||||
val list = (param.args[2] as List<QueryNode>).toMutableList()
|
|
||||||
val lastIndex = if (list.isEmpty()) {
|
|
||||||
0
|
|
||||||
} else if (list.last() is ContentNode)
|
|
||||||
list.lastIndex
|
|
||||||
else
|
|
||||||
list.size
|
|
||||||
|
|
||||||
// Open a Date Picker
|
|
||||||
if (filter in FilterTypeExtension.dates) {
|
|
||||||
replaceAndPublish.invoke(this, lastIndex, listOf(filterNode), list)
|
|
||||||
DatePickerFragment.open(Utils.appActivity.supportFragmentManager) {
|
|
||||||
replaceAndPublish.invoke(this,
|
|
||||||
getAnswerReplacementStart.invoke(this, list),
|
|
||||||
listOf(filterNode, DateNode(it)),
|
|
||||||
list
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter == FilterTypeExtension.SORT)
|
|
||||||
replaceAndPublish.invoke(this,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch to set icons
|
|
||||||
@Suppress("ResourceType")
|
|
||||||
patcher.before<WidgetSearchSuggestionsAdapter.FilterViewHolder>(
|
|
||||||
"getIconDrawable",
|
|
||||||
Context::class.java,
|
|
||||||
FilterType::class.java
|
|
||||||
) { param ->
|
|
||||||
val type = param.args[1] as FilterType
|
|
||||||
val (isDiscord, resID) = when (type) {
|
|
||||||
FilterTypeExtension.BEFORE -> true to R.e.ic_history_white_24dp
|
|
||||||
FilterTypeExtension.DURING -> false to scoutRes.getDrawableId("baseline_clock_24")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
resID?.let {
|
|
||||||
val res = if (isDiscord) context.resources else resources!!
|
|
||||||
param.result = ResourcesCompat.getDrawable(res, it, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch for retrieving sample filter answer/placeholder
|
|
||||||
patcher.before<WidgetSearchSuggestionsAdapter.FilterViewHolder>(
|
|
||||||
"getAnswerText",
|
|
||||||
FilterType::class.java
|
|
||||||
) { param ->
|
|
||||||
val type = param.args[0] as FilterType
|
|
||||||
if (type in FilterTypeExtension.dates)
|
|
||||||
param.result = ssProvider.getIdentifier("search_answer_date")
|
|
||||||
if (type == FilterTypeExtension.SORT)
|
|
||||||
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
|
|
||||||
patcher.before<WidgetSearchSuggestionsAdapter.FilterViewHolder>(
|
|
||||||
"getFilterText",
|
|
||||||
FilterType::class.java
|
|
||||||
) { param ->
|
|
||||||
val type = param.args[0] as FilterType
|
|
||||||
val res = when (type) {
|
|
||||||
FilterTypeExtension.EXCLUDE -> ScoutResource.EXCLUDE_FILTER
|
|
||||||
FilterTypeExtension.BEFORE -> ssProvider.getIdentifier("search_filter_before")
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch formatting utils to use our custom lowercase strings
|
|
||||||
// This is called by FilterViewHolder.onConfigure, using the results from getAnswerText and getFilterText
|
|
||||||
patcher.patch(
|
|
||||||
FormatUtils::class.java.getDeclaredMethod(
|
|
||||||
"c",
|
|
||||||
Resources::class.java,
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
Array::class.java,
|
|
||||||
Function1::class.java
|
|
||||||
),
|
|
||||||
PreHook { param ->
|
|
||||||
val resID = param.args[1] as Int
|
|
||||||
val objArr = param.args[2] as Array<*>
|
|
||||||
val override = when (resID) {
|
|
||||||
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 {
|
|
||||||
param.result = FormatUtils.g(it, objArr.copyOf(), param.args[3] as b.a.k.`b$b`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Patch to manually configure expander, need to do this to update the suggestions widget
|
|
||||||
patcher.before<WidgetSearchSuggestionsAdapter.FilterViewHolder>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
MGRecyclerDataPayload::class.java,
|
|
||||||
) { (param, _: Int, payload: SingleTypePayload<FilterSuggestion>) ->
|
|
||||||
val suggestion = payload.data
|
|
||||||
if (suggestion.filterType != FilterTypeExtension.EXPAND) {
|
|
||||||
return@before
|
|
||||||
}
|
|
||||||
param.result = null
|
|
||||||
|
|
||||||
val sampleText = binding.b
|
|
||||||
val layout = binding.c
|
|
||||||
val filterText = binding.d
|
|
||||||
val icon = binding.e
|
|
||||||
layout.setOnClickListener {
|
|
||||||
val onFilter = adapter.onFilterClicked as `WidgetSearchSuggestions$configureUI$1`
|
|
||||||
val widget = onFilter.`this$0`
|
|
||||||
optionsExpanded = true
|
|
||||||
WidgetSearchSuggestions.Model.Companion!!.get(ContextSearchStringProvider(context)).z().subscribe {
|
|
||||||
WidgetSearchSuggestions.`access$configureUI`(widget, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sampleText.text = null
|
|
||||||
filterText.text = ssProvider.expandFilterString
|
|
||||||
val drawable = R.e.ic_chevron_right_primary_300_12dp
|
|
||||||
icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, drawable, null))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch to add our new filters into the initial suggestions
|
|
||||||
patcher.after<SearchSuggestionEngine>(
|
|
||||||
"getFilterSuggestions",
|
|
||||||
CharSequence::class.java,
|
|
||||||
SearchStringProvider::class.java,
|
|
||||||
Boolean::class.javaPrimitiveType!!,
|
|
||||||
) { (param, query: CharSequence) ->
|
|
||||||
val res = (param.result as List<SearchSuggestion>).toMutableList()
|
|
||||||
|
|
||||||
if (optionsExpanded || query != "") {
|
|
||||||
for (type in FilterTypeExtension.filters) {
|
|
||||||
val st = ssProvider.stringFor(type) + ":"
|
|
||||||
|
|
||||||
if (st.contains(query))
|
|
||||||
res.add(FilterSuggestion(type))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.add(FilterSuggestion(FilterTypeExtension.EXPAND))
|
|
||||||
}
|
|
||||||
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
|
|
||||||
private fun patchThreadSupport() {
|
|
||||||
// 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)
|
|
||||||
`QueryParser$Companion$getInAnswerRule$1`(compile, compile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch Search data model builder to also add in threads
|
|
||||||
patcher.before<SearchData.Builder>(
|
|
||||||
"buildForGuild",
|
|
||||||
Map::class.java,
|
|
||||||
Map::class.java,
|
|
||||||
Map::class.java,
|
|
||||||
Map::class.java
|
|
||||||
) { (
|
|
||||||
param,
|
|
||||||
/* members */ _: Map<Long, GuildMember>,
|
|
||||||
/* users*/ _: Map<Long, User>,
|
|
||||||
channels: Map<Long, Channel>,
|
|
||||||
permissions: Map<Long, Long>
|
|
||||||
) ->
|
|
||||||
val threads = StoreStream.getChannels().`getThreadsForGuildInternal$app_productionGoogleRelease`(
|
|
||||||
StoreStream.getGuildSelected().selectedGuildId
|
|
||||||
)
|
|
||||||
val mergedChannels = channels.toMutableMap()
|
|
||||||
val mergedPermissions = permissions.toMutableMap()
|
|
||||||
for (thread in threads) {
|
|
||||||
mergedChannels[thread.id] = thread
|
|
||||||
mergedPermissions[thread.id] = Permission.VIEW_CHANNEL
|
|
||||||
}
|
|
||||||
param.args[2] = mergedChannels
|
|
||||||
param.args[3] = mergedPermissions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post-process the name-id map to wrap the names in quotes if they have spaces
|
|
||||||
patcher.after<SearchData.Builder>(
|
|
||||||
"buildForGuild",
|
|
||||||
Map::class.java,
|
|
||||||
Map::class.java,
|
|
||||||
Map::class.java,
|
|
||||||
Map::class.java
|
|
||||||
) { param ->
|
|
||||||
val res = param.result as SearchData
|
|
||||||
val nameMap = res.channelNameIndex as HashMap<String, Long>
|
|
||||||
nameMap
|
|
||||||
.filter { (name) -> name.contains(" ") }
|
|
||||||
.forEach { (name, value) ->
|
|
||||||
val wrapped = "\"${name}\""
|
|
||||||
nameMap.remove(name)
|
|
||||||
nameMap[wrapped] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch the channel node to automatically insert quotes for names with spaces
|
|
||||||
patcher.before<ChannelNode>(String::class.java) { (param, name: String) ->
|
|
||||||
if (name.contains(" ") && !name.startsWith("\""))
|
|
||||||
param.args[0] = "\"${name}\""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch the search sorter to place threads last
|
|
||||||
patcher.before<`ChannelUtils$getSortByNameAndType$1`<*>>(
|
|
||||||
"compare",
|
|
||||||
Object::class.java, // ?? :sob:
|
|
||||||
Object::class.java,
|
|
||||||
) { (param, ch1: Channel?, ch2: Channel?) ->
|
|
||||||
if (ch1 == null || ch2 == null) return@before
|
|
||||||
|
|
||||||
// ChannelUtils.H <=> ChannelUtils.isThread
|
|
||||||
if (ChannelUtils.H(ch1) && !ChannelUtils.H(ch2)) {
|
|
||||||
param.result = 1
|
|
||||||
}
|
|
||||||
if (!ChannelUtils.H(ch1) && ChannelUtils.H(ch2)) {
|
|
||||||
param.result = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch search suggestions to set icon to thread icon if it is a thread
|
|
||||||
patcher.after<WidgetSearchSuggestionsAdapter.InChannelViewHolder>(
|
|
||||||
"onConfigure",
|
|
||||||
Int::class.javaPrimitiveType!!,
|
|
||||||
MGRecyclerDataPayload::class.java
|
|
||||||
) { (_, _: Int, payload: SingleTypePayload<ChannelSuggestion>) ->
|
|
||||||
StoreStream.getChannels().getChannel(payload.data.channelId)?.let {
|
|
||||||
if (ChannelUtils.H(it)) {
|
|
||||||
itemView.findViewById<ImageView>("search_suggestions_item_channel_icon")
|
|
||||||
.setImageDrawable(scoutRes.getDrawable("ic_thread_actually_white_24dp"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes the #0000 discriminator from usernames when searching
|
|
||||||
private fun patchUsernameDiscriminator() {
|
|
||||||
// Change the regex for the user rule
|
|
||||||
// Previously it matches something like <username>#<discrim>
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// Returns a new rule to support our optional second group (discriminator)
|
|
||||||
return@instead SimpleParserRule(regex) { matcher, _, obj ->
|
|
||||||
val username = matcher.group(3) ?: matcher.group(1)!!
|
|
||||||
val discrim = matcher.group(2)?.toInt() ?: 0
|
|
||||||
ParseSpec(UserNode(username, discrim), obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patches the node's string representation to add an @ and remove empty discriminators
|
|
||||||
patcher.after<UserNode>("getText") { param ->
|
|
||||||
param.result = "@" + (param.result as String).replace("#0000", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
package moe.lava.awoocord.scout.api
|
|
||||||
|
|
||||||
import com.discord.models.domain.ModelSearchResponse
|
|
||||||
import i0.f0.f
|
|
||||||
import i0.f0.s
|
|
||||||
import i0.f0.t
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
// io.f0.f = retrofit @GET
|
|
||||||
// io.f0.s = retrofit @Path
|
|
||||||
// io.f0.t = retrofit @Query
|
|
||||||
|
|
||||||
interface SearchAPIInterface {
|
|
||||||
@f("channels/{channelId}/messages/search")
|
|
||||||
fun searchChannelMessages(
|
|
||||||
@s("channelId") channelId: Long,
|
|
||||||
@t("min_id") minId: List<String>?,
|
|
||||||
@t("max_id") maxId: List<String>?,
|
|
||||||
@t("author_id") authorId: List<String>?,
|
|
||||||
@t("mentions") mentions: List<String>?,
|
|
||||||
@t("has") has: List<String>?,
|
|
||||||
@t("content") content: List<String>?,
|
|
||||||
@t("attempts") attempts: Int?,
|
|
||||||
@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")
|
|
||||||
fun searchGuildMessages(
|
|
||||||
@s("guildId") guildId: Long,
|
|
||||||
@t("min_id") minId: List<String>?,
|
|
||||||
@t("max_id") maxId: List<String>?,
|
|
||||||
@t("author_id") authorId: List<String>?,
|
|
||||||
@t("mentions") mentions: List<String>?,
|
|
||||||
@t("channel_id") channelId: List<String>?,
|
|
||||||
@t("has") has: List<String>?,
|
|
||||||
@t("content") content: List<String>?,
|
|
||||||
@t("attempts") attempts: Int?,
|
|
||||||
@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?>
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
@file:Suppress("EnumValuesSoftDeprecate")
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
package moe.lava.awoocord.scout.parsing
|
|
||||||
|
|
||||||
import com.discord.simpleast.core.parser.ParseSpec
|
|
||||||
import com.discord.utilities.SnowflakeUtils
|
|
||||||
import com.discord.utilities.search.network.SearchQuery
|
|
||||||
import com.discord.utilities.search.query.FilterType
|
|
||||||
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.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class DateNode(private val date: Long?, private val unparsed: String) : AnswerNode() {
|
|
||||||
|
|
||||||
constructor(unparsed: String) : this(fmt.parse(unparsed)?.time, unparsed)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val fmt = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
||||||
val regex: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}", Pattern.UNICODE_CASE)
|
|
||||||
fun getDateRule(): ParserRule {
|
|
||||||
return SimpleParserRule(regex) { matcher, _, obj ->
|
|
||||||
val match = matcher.group()
|
|
||||||
val date = fmt.parse(match)
|
|
||||||
val node = DateNode(date?.time, match)
|
|
||||||
ParseSpec(node, obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFilterRule(str: String, type: FilterType): ParserRule {
|
|
||||||
val regex = Pattern.compile("^\\s*?(${str}):", 64)
|
|
||||||
return SimpleParserRule(regex) { _, _, obj ->
|
|
||||||
ParseSpec(FilterNode(type, str), obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBeforeRule(str: String): ParserRule = getFilterRule(str, FilterTypeExtension.BEFORE)
|
|
||||||
fun getDuringRule(str: String): ParserRule = getFilterRule(str, FilterTypeExtension.DURING)
|
|
||||||
fun getAfterRule(str: String): ParserRule = getFilterRule(str, FilterTypeExtension.AFTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getValidFilters(): Set<FilterType> = FilterTypeExtension.dates.toSet()
|
|
||||||
override fun isValid(searchData: SearchData?): Boolean = date != null
|
|
||||||
override fun getText(): CharSequence = unparsed
|
|
||||||
|
|
||||||
private val snowflake: String?
|
|
||||||
get() = date?.let { SnowflakeUtils.fromTimestamp(date).toString() }
|
|
||||||
private val nextDaySnowflake: String?
|
|
||||||
get() = date?.let { SnowflakeUtils.fromTimestamp(date + 86_400_000).toString() }
|
|
||||||
|
|
||||||
override fun updateQuery(
|
|
||||||
builder: SearchQuery.Builder?,
|
|
||||||
searchData: SearchData?,
|
|
||||||
filterType: FilterType?
|
|
||||||
) {
|
|
||||||
checkNotNull(builder) { "queryBuilder" }
|
|
||||||
checkNotNull(date) { "date" }
|
|
||||||
when (filterType) {
|
|
||||||
FilterTypeExtension.BEFORE -> {
|
|
||||||
builder.appendParam("max_id", snowflake)
|
|
||||||
}
|
|
||||||
FilterTypeExtension.AFTER -> {
|
|
||||||
builder.appendParam("min_id", nextDaySnowflake)
|
|
||||||
}
|
|
||||||
FilterTypeExtension.DURING -> {
|
|
||||||
builder.appendParam("min_id", snowflake)
|
|
||||||
builder.appendParam("max_id", nextDaySnowflake)
|
|
||||||
}
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue