From 02ac3cb6525ace3412b87c1c453f8448ca55984c Mon Sep 17 00:00:00 2001 From: LavaDesu Date: Tue, 15 Jul 2025 17:53:33 +1000 Subject: [PATCH] feat(canary/ComponentsV2): init @ LavaDesu/Aliucord@8ee06ba700b2441ce19fdc061e87d82dc6724cba --- build.gradle.kts | 3 + canary/ComponentsV2/build.gradle.kts | 54 +++++ .../ComponentsV2/src/main/AndroidManifest.xml | 2 + .../com/aliucord/coreplugins/ComponentsV2.kt | 219 ++++++++++++++++++ .../componentsv2/BotUiComponentV2Entry.kt | 41 ++++ .../componentsv2/ComponentV2Type.kt | 63 +++++ .../componentsv2/PatchMessageItems.kt | 43 ++++ .../models/ContainerMessageComponent.kt | 41 ++++ .../models/MediaGalleryMessageComponent.kt | 33 +++ .../models/SectionMessageComponent.kt | 37 +++ .../models/SelectV2MessageComponent.kt | 48 ++++ .../models/SeparatorMessageComponent.kt | 32 +++ .../models/SpoilableMessageComponent.kt | 8 + .../models/TextDisplayMessageComponent.kt | 32 +++ .../models/ThumbnailMessageComponent.kt | 34 +++ .../componentsv2/selectsheet/SelectSheet.kt | 87 +++++++ .../selectsheet/SelectSheetAdapter.kt | 13 ++ .../selectsheet/SelectSheetItem.kt | 45 ++++ .../selectsheet/SelectSheetItemViewHolder.kt | 121 ++++++++++ .../selectsheet/SelectSheetViewModel.kt | 154 ++++++++++++ .../views/ContainerComponentView.kt | 92 ++++++++ .../views/MediaGalleryComponentView.kt | 131 +++++++++++ .../views/SectionComponentView.kt | 57 +++++ .../views/SelectV2ComponentView.kt | 85 +++++++ .../views/SeparatorComponentView.kt | 42 ++++ .../componentsv2/views/SpoilerView.kt | 121 ++++++++++ .../views/TextDisplayComponentView.kt | 86 +++++++ .../views/ThumbnailComponentView.kt | 95 ++++++++ .../kotlin/com/aliucord/utils/ViewUtils.kt | 41 ++++ .../api/botuikit/ChannelSelectComponent.kt | 14 ++ .../api/botuikit/ContainerComponent.kt | 12 + .../discord/api/botuikit/ContentComponent.kt | 7 + .../com/discord/api/botuikit/FileComponent.kt | 12 + .../api/botuikit/MediaGalleryComponent.kt | 9 + .../discord/api/botuikit/MediaGalleryItem.kt | 7 + .../botuikit/MentionableSelectComponent.kt | 14 ++ .../api/botuikit/RoleSelectComponent.kt | 14 ++ .../discord/api/botuikit/SectionComponent.kt | 15 ++ .../discord/api/botuikit/SelectV2Component.kt | 11 + .../api/botuikit/SelectV2DefaultValue.kt | 6 + .../api/botuikit/SelectV2DefaultValueType.kt | 7 + .../api/botuikit/SeparatorComponent.kt | 11 + .../api/botuikit/TextDisplayComponent.kt | 9 + .../api/botuikit/ThumbnailComponent.kt | 11 + .../discord/api/botuikit/UnfurledMediaItem.kt | 10 + .../api/botuikit/UserSelectComponent.kt | 14 ++ canary/LICENSE | 172 ++++++++++++++ settings.gradle.kts | 14 +- 48 files changed, 2223 insertions(+), 6 deletions(-) create mode 100644 canary/ComponentsV2/build.gradle.kts create mode 100644 canary/ComponentsV2/src/main/AndroidManifest.xml create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/ComponentsV2.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/BotUiComponentV2Entry.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/ComponentV2Type.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/PatchMessageItems.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/ContainerMessageComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/MediaGalleryMessageComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SectionMessageComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SelectV2MessageComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SeparatorMessageComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SpoilableMessageComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/TextDisplayMessageComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/ThumbnailMessageComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheet.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetAdapter.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetItem.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetItemViewHolder.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetViewModel.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/ContainerComponentView.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/MediaGalleryComponentView.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SectionComponentView.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SelectV2ComponentView.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SeparatorComponentView.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SpoilerView.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/TextDisplayComponentView.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/ThumbnailComponentView.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/aliucord/utils/ViewUtils.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ChannelSelectComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ContainerComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ContentComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/FileComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MediaGalleryComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MediaGalleryItem.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MentionableSelectComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/RoleSelectComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SectionComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2Component.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2DefaultValue.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2DefaultValueType.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SeparatorComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/TextDisplayComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ThumbnailComponent.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/UnfurledMediaItem.kt create mode 100644 canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/UserSelectComponent.kt create mode 100644 canary/LICENSE diff --git a/build.gradle.kts b/build.gradle.kts index 20ca4f1..57d4211 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ buildscript { google() mavenCentral() maven("https://maven.aliucord.com/snapshots") + gradlePluginPortal() // remove when gradle 8 maven("https://jitpack.io") } @@ -13,6 +14,8 @@ buildscript { classpath("com.android.tools.build:gradle:7.1.3") classpath("com.aliucord:gradle:main-SNAPSHOT") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21") +// classpath("com.gradleup.shadow:shadow-gradle-plugin:8.3.8") + classpath("com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:7.1.2") // For Gradle 7 compat } } diff --git a/canary/ComponentsV2/build.gradle.kts b/canary/ComponentsV2/build.gradle.kts new file mode 100644 index 0000000..aedad37 --- /dev/null +++ b/canary/ComponentsV2/build.gradle.kts @@ -0,0 +1,54 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +version = "7.15.0-8ee06ba" +description = "Beta backport of ComponentsV2" + +aliucord { + // Changelog of your plugin + changelog.set(""" + TODO {fixed} + ====================== + * File component + * SelectV2: searching + * SelectV2: showing selected items in chat list + + Changelog {added marginTop} + ====================== + # 7.15.0 + * Initial release >w< + """.trimIndent()) + + excludeFromUpdaterJson.set(false) +} + +//apply(plugin = "com.gradleup.shadow") +apply(plugin = "com.github.johnrengelman.shadow") // remove when gradle 8 + +val shadowDir = File(buildDir, "intermediates/shadowed") + +tasks.register("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("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) + } +} diff --git a/canary/ComponentsV2/src/main/AndroidManifest.xml b/canary/ComponentsV2/src/main/AndroidManifest.xml new file mode 100644 index 0000000..53e3c81 --- /dev/null +++ b/canary/ComponentsV2/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/ComponentsV2.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/ComponentsV2.kt new file mode 100644 index 0000000..467c460 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/ComponentsV2.kt @@ -0,0 +1,219 @@ +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.aliucord.utils.ReflectUtils +import com.discord.api.botuikit.* +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.botuikit.* +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.google.gson.stream.JsonReader +import com.lytefast.flexinput.R +import de.robv.android.xposed.XposedBridge + +@AliucordPlugin(requiresRestart = true) +@Suppress("unused") +class ComponentsV2 : Plugin() { + companion object { + /** 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 } + } + + override fun start(context: Context) { + XposedBridge.makeClassInheritable(BotUiComponentEntry::class.java) + ComponentV2Type.make() + patchGson() + // https://github.com/LSPosed/LSPlant/issues/41 + patchMessageItems(patcher) + + patcher.instead( + "toMessageLayoutComponent", + LayoutComponent::class.java, + Int::class.javaPrimitiveType!!, + List::class.java, + ComponentExperiments::class.java + ) { (_, layout: LayoutComponent, index: Int, components: List) -> + 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("configureView", ComponentActionListener::class.java, MessageComponent::class.java, ComponentView::class.java) + { (_, listener: ComponentActionListener, component: MessageComponent, view: ComponentView?) -> + view?.configure(component, this, listener) + } + + patcher.instead("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(WidgetChatListAdapter::class.java) + { + val rootLayout = itemView.findViewById(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( + "createActionMessageComponent", + ActionComponent::class.java, + Int::class.javaPrimitiveType!!, + ComponentStoreState::class.java, + ComponentExperiments::class.java, + ) { ( + _, + actionComponent: ActionComponent, + index: Int, + componentStoreState: ComponentStoreState, + ) -> + val interactionState: Map? = 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 + } + } + } + + override fun stop(context: Context) { + patcher.unpatchAll() + unpatchGson() + ComponentV2Type.unmake(logger) + } + + private fun patchGson() { + 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("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) + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/BotUiComponentV2Entry.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/BotUiComponentV2Entry.kt new file mode 100644 index 0000000..18a68b0 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/BotUiComponentV2Entry.kt @@ -0,0 +1,41 @@ +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, + 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, + val guildRoles: Map, + // val channelNames: Map, + ) + + 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() +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/ComponentV2Type.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/ComponentV2Type.kt new file mode 100644 index 0000000..79c35e0 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/ComponentV2Type.kt @@ -0,0 +1,63 @@ +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? = null + private var oldValues: Array? = 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 + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/PatchMessageItems.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/PatchMessageItems.kt new file mode 100644 index 0000000..6c12a46 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/PatchMessageItems.kt @@ -0,0 +1,43 @@ +package com.aliucord.coreplugins.componentsv2 + +import com.aliucord.api.PatcherAPI +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, + guildRoles: Map, + _blockedRelationships: Map?, + _referencedChannel: Channel?, + _threadStoreState: StoreThreadMessages.ThreadState?, + _message: Message, + state: StoreMessageState.State?, + _repliedMessages: Map?, + _isBlockedExpanded: Boolean, + _isMinimal: Boolean, + ) -> + @Suppress("UNCHECKED_CAST") + val result = (param.result as MutableList) + val meId = param.args[15] as Long + result.forEachIndexed { index, entry -> + if (entry is BotUiComponentEntry && ((entry.message.flags shr 15) and 1 == 1L)) { + val fields = BotUiComponentV2Entry.V2Fields(state, meId, channel, guildMembers, guildRoles) + result[index] = BotUiComponentV2Entry.fromV1(entry, fields) + } + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/ContainerMessageComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/ContainerMessageComponent.kt new file mode 100644 index 0000000..62213c7 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/ContainerMessageComponent.kt @@ -0,0 +1,41 @@ +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, + 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, + ): ContainerMessageComponent { + components.forEach { + if (it is MediaGalleryMessageComponent) + it.markedContained = true + } + return component.run { + ContainerMessageComponent( + type, + index, + id, + components, + accentColor, + spoiler, + ) + } + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/MediaGalleryMessageComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/MediaGalleryMessageComponent.kt new file mode 100644 index 0000000..350c17c --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/MediaGalleryMessageComponent.kt @@ -0,0 +1,33 @@ +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, + // 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, + ) + } + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SectionMessageComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SectionMessageComponent.kt new file mode 100644 index 0000000..6b189ea --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SectionMessageComponent.kt @@ -0,0 +1,37 @@ +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, + val accessory: MessageComponent?, +) : MessageComponent { + override fun getType() = type + override fun getIndex() = index + + companion object { + fun mergeToMessageComponent( + component: SectionComponent, + index: Int, + components: List, + ): SectionMessageComponent { + return component.run { + val realComponents = components.toMutableList() + val accessory = realComponents.removeAt(realComponents.lastIndex) + SectionMessageComponent( + type, + index, + id, + realComponents, + accessory, + ) + } + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SelectV2MessageComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SelectV2MessageComponent.kt new file mode 100644 index 0000000..3560f3f --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SelectV2MessageComponent.kt @@ -0,0 +1,48 @@ +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, + 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 + ) + } + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SeparatorMessageComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SeparatorMessageComponent.kt new file mode 100644 index 0000000..e055d36 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SeparatorMessageComponent.kt @@ -0,0 +1,32 @@ +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, + ) + } + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SpoilableMessageComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SpoilableMessageComponent.kt new file mode 100644 index 0000000..4219856 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/SpoilableMessageComponent.kt @@ -0,0 +1,8 @@ +package com.aliucord.coreplugins.componentsv2.models + +import com.discord.models.botuikit.MessageComponent + +interface SpoilableMessageComponent : MessageComponent { + val id: Int + val spoiler: Boolean +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/TextDisplayMessageComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/TextDisplayMessageComponent.kt new file mode 100644 index 0000000..aeb3e75 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/TextDisplayMessageComponent.kt @@ -0,0 +1,32 @@ +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 + ) + } + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/ThumbnailMessageComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/ThumbnailMessageComponent.kt new file mode 100644 index 0000000..d4bd625 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/models/ThumbnailMessageComponent.kt @@ -0,0 +1,34 @@ +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, + ) + } + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheet.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheet.kt new file mode 100644 index 0000000..05badd6 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheet.kt @@ -0,0 +1,87 @@ +@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS") + +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 + +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 = + b.a.k.b.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) + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetAdapter.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetAdapter.kt new file mode 100644 index 0000000..d63a58a --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetAdapter.kt @@ -0,0 +1,13 @@ +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(recycler) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MGRecyclerViewHolder<*, SelectSheetItem> { + return SelectSheetItemViewHolder(this) + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetItem.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetItem.kt new file mode 100644 index 0000000..414de8d --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetItem.kt @@ -0,0 +1,45 @@ +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) + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetItemViewHolder.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetItemViewHolder.kt new file mode 100644 index 0000000..700c95c --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetItemViewHolder.kt @@ -0,0 +1,121 @@ +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(Utils.getResId("widget_select_component_bottom_sheet_item", "layout"), adapter) { + + private val description = itemView.findViewById(Utils.getResId("select_component_sheet_item_description", "id"))!! + private val divider = itemView.findViewById(Utils.getResId("select_component_sheet_item_divider", "id"))!! + private val dividerWithIcon = itemView.findViewById(Utils.getResId("select_component_sheet_item_divider_icon", "id"))!! + private val icon = itemView.findViewById(Utils.getResId("select_component_sheet_item_icon", "id"))!! + private val checkbox = itemView.findViewById(Utils.getResId("select_component_sheet_item_selected", "id"))!! + private val title = itemView.findViewById(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 + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetViewModel.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetViewModel.kt new file mode 100644 index 0000000..624ee63 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/selectsheet/SelectSheetViewModel.kt @@ -0,0 +1,154 @@ +@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS") + +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, + 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() + 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() + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/ContainerComponentView.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/ContainerComponentView.kt new file mode 100644 index 0000000..3bcdf4a --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/ContainerComponentView.kt @@ -0,0 +1,92 @@ +@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 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 { + 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) + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/MediaGalleryComponentView.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/MediaGalleryComponentView.kt new file mode 100644 index 0000000..fc48d4c --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/MediaGalleryComponentView.kt @@ -0,0 +1,131 @@ +@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.ComponentsV2 +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 { + 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>? = 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>() + 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 = ComponentsV2.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() + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SectionComponentView.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SectionComponentView.kt new file mode 100644 index 0000000..750c28b --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SectionComponentView.kt @@ -0,0 +1,57 @@ +@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 { + 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)) + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SelectV2ComponentView.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SelectV2ComponentView.kt new file mode 100644 index 0000000..79026ba --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SelectV2ComponentView.kt @@ -0,0 +1,85 @@ +@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 { + 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) + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SeparatorComponentView.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SeparatorComponentView.kt new file mode 100644 index 0000000..855e7ef --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SeparatorComponentView.kt @@ -0,0 +1,42 @@ +@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 { + 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) + } + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SpoilerView.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SpoilerView.kt new file mode 100644 index 0000000..24ba988 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/SpoilerView.kt @@ -0,0 +1,121 @@ +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, + ) { + 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 + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/TextDisplayComponentView.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/TextDisplayComponentView.kt new file mode 100644 index 0000000..8db35ca --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/TextDisplayComponentView.kt @@ -0,0 +1,86 @@ +@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 { + 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? + 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); + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/ThumbnailComponentView.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/ThumbnailComponentView.kt new file mode 100644 index 0000000..1c043dd --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/coreplugins/componentsv2/views/ThumbnailComponentView.kt @@ -0,0 +1,95 @@ +@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 { + 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) + } +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/aliucord/utils/ViewUtils.kt b/canary/ComponentsV2/src/main/kotlin/com/aliucord/utils/ViewUtils.kt new file mode 100644 index 0000000..2ed27e1 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/aliucord/utils/ViewUtils.kt @@ -0,0 +1,41 @@ +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.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.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() +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ChannelSelectComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ChannelSelectComponent.kt new file mode 100644 index 0000000..05ec6a2 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ChannelSelectComponent.kt @@ -0,0 +1,14 @@ +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?, + override val minValues: Int, + override val maxValues: Int, + override val disabled: Boolean, +) : SelectV2Component() { + override fun getType() = type +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ContainerComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ContainerComponent.kt new file mode 100644 index 0000000..a52183b --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ContainerComponent.kt @@ -0,0 +1,12 @@ +package com.discord.api.botuikit + +data class ContainerComponent( + private val type: ComponentType, + val id: Int, + val components: List, + @b.i.d.p.b("accent_color") val accentColor: Int?, + val spoiler: Boolean, +): LayoutComponent() { + override fun getType() = type + override fun a() = components +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ContentComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ContentComponent.kt new file mode 100644 index 0000000..f1e520b --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ContentComponent.kt @@ -0,0 +1,7 @@ +package com.discord.api.botuikit + +import java.io.Serializable + +abstract class ContentComponent : LayoutComponent(), Serializable { + final override fun a(): List = listOf() +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/FileComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/FileComponent.kt new file mode 100644 index 0000000..14b7701 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/FileComponent.kt @@ -0,0 +1,12 @@ +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 +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MediaGalleryComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MediaGalleryComponent.kt new file mode 100644 index 0000000..f14a0d2 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MediaGalleryComponent.kt @@ -0,0 +1,9 @@ +package com.discord.api.botuikit + +data class MediaGalleryComponent( + private val type: ComponentType, + val id: Int, + val items: List, +) : ContentComponent() { + override fun getType() = type +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MediaGalleryItem.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MediaGalleryItem.kt new file mode 100644 index 0000000..3cd1390 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MediaGalleryItem.kt @@ -0,0 +1,7 @@ +package com.discord.api.botuikit + +data class MediaGalleryItem( + val media: UnfurledMediaItem, + val description: String?, + val spoiler: Boolean, +) diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MentionableSelectComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MentionableSelectComponent.kt new file mode 100644 index 0000000..7e76023 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/MentionableSelectComponent.kt @@ -0,0 +1,14 @@ +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?, + override val minValues: Int, + override val maxValues: Int, + override val disabled: Boolean, +) : SelectV2Component() { + override fun getType() = type +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/RoleSelectComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/RoleSelectComponent.kt new file mode 100644 index 0000000..eca7f18 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/RoleSelectComponent.kt @@ -0,0 +1,14 @@ +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?, + override val minValues: Int, + override val maxValues: Int, + override val disabled: Boolean, +) : SelectV2Component() { + override fun getType() = type +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SectionComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SectionComponent.kt new file mode 100644 index 0000000..3175618 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SectionComponent.kt @@ -0,0 +1,15 @@ +package com.discord.api.botuikit + +data class SectionComponent( + private val type: ComponentType, + val id: Int, + val components: List, + 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 +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2Component.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2Component.kt new file mode 100644 index 0000000..4c22334 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2Component.kt @@ -0,0 +1,11 @@ +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? + abstract val minValues: Int + abstract val maxValues: Int + abstract val disabled: Boolean +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2DefaultValue.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2DefaultValue.kt new file mode 100644 index 0000000..c6f6992 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2DefaultValue.kt @@ -0,0 +1,6 @@ +package com.discord.api.botuikit + +data class SelectV2DefaultValue( + val id: Long, + val type: SelectV2DefaultValueType, +) diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2DefaultValueType.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2DefaultValueType.kt new file mode 100644 index 0000000..2caae0e --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SelectV2DefaultValueType.kt @@ -0,0 +1,7 @@ +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, +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SeparatorComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SeparatorComponent.kt new file mode 100644 index 0000000..b773f49 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/SeparatorComponent.kt @@ -0,0 +1,11 @@ +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 = listOf() +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/TextDisplayComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/TextDisplayComponent.kt new file mode 100644 index 0000000..c8d190d --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/TextDisplayComponent.kt @@ -0,0 +1,9 @@ +package com.discord.api.botuikit + +data class TextDisplayComponent( + private val type: ComponentType, + val id: Int, + val content: String, +) : ContentComponent() { + override fun getType() = type +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ThumbnailComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ThumbnailComponent.kt new file mode 100644 index 0000000..7fbea89 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/ThumbnailComponent.kt @@ -0,0 +1,11 @@ +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 +} diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/UnfurledMediaItem.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/UnfurledMediaItem.kt new file mode 100644 index 0000000..d9bdc8d --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/UnfurledMediaItem.kt @@ -0,0 +1,10 @@ +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?, +) diff --git a/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/UserSelectComponent.kt b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/UserSelectComponent.kt new file mode 100644 index 0000000..fc0a370 --- /dev/null +++ b/canary/ComponentsV2/src/main/kotlin/com/discord/api/botuikit/UserSelectComponent.kt @@ -0,0 +1,14 @@ +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?, + override val minValues: Int, + override val maxValues: Int, + override val disabled: Boolean, +) : SelectV2Component() { + override fun getType() = type +} diff --git a/canary/LICENSE b/canary/LICENSE new file mode 100644 index 0000000..95230e3 --- /dev/null +++ b/canary/LICENSE @@ -0,0 +1,172 @@ +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 " 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. diff --git a/settings.gradle.kts b/settings.gradle.kts index ec25265..a8ead21 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,15 +1,17 @@ rootProject.name = "Awoocord" -// This file sets what projects are included. Every time you add a new project, you must add it -// to the includes below. +val canaryPlugins = arrayOf("ComponentsV2") -// Plugins are included like this include( "AlignThreads", - "Scout" + "Scout", + *canaryPlugins, ) rootProject.children.forEach { - // Change kotlin to java if you'd rather use java - it.projectDir = file("plugins/${it.name}") + val isCanary = it.name in canaryPlugins + val dir = if (isCanary) "canary" else "plugins" + val name = it.name + if (isCanary) it.name += "-Beta" + it.projectDir = file("${dir}/${name}") }