From e45ab543b5b94c67b67d58883aed4b74905e7984 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 15 Oct 2023 22:10:38 +0800 Subject: [PATCH 001/107] feat: add initial support for Nexus.R (React) --- .../command/core/NexusCommandEventCore.kt | 9 + .../command/facade/NexusCommandEvent.kt | 13 ++ .../nexus/features/command/react/React.kt | 189 ++++++++++++++++++ src/test/java/commands/TestCommand.kt | 27 +++ 4 files changed, 238 insertions(+) create mode 100644 src/main/java/pw/mihou/nexus/features/command/react/React.kt create mode 100644 src/test/java/commands/TestCommand.kt diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index 0ae6eb7f..10f7752f 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -8,6 +8,7 @@ import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.features.command.facade.NexusCommand import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.messages.NexusMessage import java.time.Instant @@ -19,6 +20,14 @@ import java.util.function.Function class NexusCommandEventCore(override val event: SlashCommandCreateEvent, override val command: NexusCommand) : NexusCommandEvent { private val store: MutableMap = HashMap() var updater: AtomicReference?> = AtomicReference(null) + override fun R(ephemeral: Boolean, react: React.() -> Unit) { + autoDefer(ephemeral) { + val r = React(this) + react(r) + + return@autoDefer r.view() + } + } override fun store(): MutableMap = store diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt index 33a02deb..517ca0be 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt @@ -10,6 +10,7 @@ import pw.mihou.nexus.Nexus import pw.mihou.nexus.Nexus.sharding import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore.execute import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore.middlewares +import pw.mihou.nexus.features.command.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.commons.NexusInteractionEvent import pw.mihou.nexus.features.messages.NexusMessage @@ -49,6 +50,18 @@ interface NexusCommandEvent : NexusInteractionEvent Unit) + /** * Gets the immediate response builder for this command and adds the * [MessageFlag.EPHEMERAL] flag ahead of time. diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt new file mode 100644 index 00000000..a5e87213 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -0,0 +1,189 @@ +package pw.mihou.nexus.features.command.react + +import org.javacord.api.entity.message.component.ActionRow +import org.javacord.api.entity.message.component.ButtonBuilder +import org.javacord.api.entity.message.component.ButtonStyle +import org.javacord.api.entity.message.component.LowLevelComponent +import org.javacord.api.entity.message.embed.EmbedBuilder +import org.javacord.api.event.interaction.ButtonClickEvent +import org.javacord.api.listener.GloballyAttachableListener +import org.javacord.api.listener.interaction.ButtonClickListener +import pw.mihou.nexus.core.assignment.NexusUuidAssigner +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.messages.NexusMessage + +class React(private val ev: NexusCommandEvent) { + private var message: NexusMessage = NexusMessage() + fun view() = message + + fun render(component: Component.() -> Unit) { + val element = Component() + component(element) + + message = element.render(ev) + } + + class Component { + private var embeds: MutableList = mutableListOf() + private var contents: String? = null + private var components: MutableList = mutableListOf() + private var listeners: MutableList = mutableListOf() + + fun render(event: NexusCommandEvent): NexusMessage { + listeners.forEach { event.api.addListener(it) } + return NexusMessage.with { + this.removeAllEmbeds() + this.addEmbeds(embeds) + + if (contents != null) { + this.setContent(contents) + } + components.chunked(3).map { ActionRow.of(it) }.forEach { this.addComponents(it) } + } + } + + fun Embed(embed: Embed.() -> Unit) { + val element = Embed() + embed(element) + + embeds.add(element.view()) + } + + fun Text(text: Text.() -> Unit) { + val element = Text() + text(element) + + contents = element.view() + } + + fun Button(style: ButtonStyle = ButtonStyle.PRIMARY, + label: String, + emoji: String? = null, + disabled: Boolean = false, + onClick: ((event: ButtonClickEvent) -> Unit)? = {}) { + val button = ButtonBuilder() + button.setStyle(style) + button.setLabel(label) + + if (emoji != null) { + button.setEmoji(emoji) + } + + button.setDisabled(disabled) + + val uuid = NexusUuidAssigner.request() + button.setCustomId(uuid) + + if (onClick != null) { + listeners += ButtonClickListener { + if (it.buttonInteraction.customId != uuid) { + return@ButtonClickListener + } + + onClick(it) + } + } + + components += button + } + + fun UrlButton(label: String, url: String, emoji: String? = null) { + val button = ButtonBuilder() + button.setStyle(ButtonStyle.LINK) + button.setLabel(label) + button.setUrl(url) + + components += button + } + + inner class Text: TextStyles { + private var content: String = "" + fun view() = content + fun Body(vararg nodes: String) { + content = nodes.joinToString() + } + } + + inner class Embed: TextStyles { + private val embed = EmbedBuilder() + fun view() = embed + + fun Title(text: String) { + embed.setTitle(text) + } + + fun Body(vararg nodes: String) { + embed.setDescription(nodes.joinToString()) + } + fun Field(name: String, vararg nodes: String) { + embed.addField(name, nodes.joinToString()) + } + fun Image(url: String) { + embed.setImage(url) + } + fun Thumbnail(url: String) { + embed.setThumbnail(url) + } + } + + interface TextStyles { + private fun renderTextStyles(bold: Boolean = false, underline: Boolean = false, italic: Boolean = false): Pair { + var prefix = "" + var suffix = "" + + if (bold) { + prefix += "**" + suffix += "**" + if (italic) { + prefix += "*" + suffix += "*" + } + } + + if (underline) { + prefix += "__" + suffix += "__" + } + + if (italic && !bold) { + prefix += "*" + suffix += "*" + } + + return prefix to suffix + } + + fun p(text: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic) + return prefix + text + suffix + } + + fun br(): String = "\n" + + fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic) + return "$prefix[$text]($href)$suffix" + } + + fun h1(text: String): String { + return "# $text" + } + fun h2(text: String): String { + return "## $text" + } + fun h3(text: String): String { + return "### $text" + } + fun ul(vararg nodes: String): String { + return nodes.joinToString("\n") { "* $it" } + } + fun ol(vararg nodes: String): String { + var text = "" + for ((index, node) in nodes.withIndex()) { + text += "${index + 1}. $node" + } + return text + } + } + } +} diff --git a/src/test/java/commands/TestCommand.kt b/src/test/java/commands/TestCommand.kt new file mode 100644 index 00000000..e8377400 --- /dev/null +++ b/src/test/java/commands/TestCommand.kt @@ -0,0 +1,27 @@ +package commands + +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler + +class TestCommand: NexusHandler { + val name = "react" + val description = "A test regarding React" + override fun onEvent(event: NexusCommandEvent) { + event.R { + render { + Embed { + Title("R.Embeds") + Body( + p("Hello World", bold = true, underline = true), + br(), + p("This is a little experiment over how this would look DX-wise. Discord message components that will also support states."), + link("Test @ Nexus", "https://github.com/ShindouMihou/Nexus") + ) + } + Button(label = "Click to be DM'd") { event -> + event.interaction.user.sendMessage("Hi") + } + } + } + } +} \ No newline at end of file From 5a0c2055ecd60af9c0c7c8eb198671ac6c192b3e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 15 Oct 2023 22:11:25 +0800 Subject: [PATCH 002/107] feat: fix errors caused by a minor change --- src/main/java/pw/mihou/nexus/features/command/react/React.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index a5e87213..dd8e1654 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -84,7 +84,7 @@ class React(private val ev: NexusCommandEvent) { } } - components += button + components += button.build() } fun UrlButton(label: String, url: String, emoji: String? = null) { @@ -93,7 +93,7 @@ class React(private val ev: NexusCommandEvent) { button.setLabel(label) button.setUrl(url) - components += button + components += button.build() } inner class Text: TextStyles { From 4d80112f70711deee15b36bd1667c87411372c75 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 15 Oct 2023 22:12:17 +0800 Subject: [PATCH 003/107] feat: set ephemeral to false by default --- .../pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt | 2 +- src/test/java/commands/{TestCommand.kt => ReactiveTest.kt} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/test/java/commands/{TestCommand.kt => ReactiveTest.kt} (96%) diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt index 517ca0be..18076d0d 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt @@ -60,7 +60,7 @@ interface NexusCommandEvent : NexusInteractionEvent Unit) + fun R(ephemeral: Boolean = false, react: React.() -> Unit) /** * Gets the immediate response builder for this command and adds the diff --git a/src/test/java/commands/TestCommand.kt b/src/test/java/commands/ReactiveTest.kt similarity index 96% rename from src/test/java/commands/TestCommand.kt rename to src/test/java/commands/ReactiveTest.kt index e8377400..9f61dc4c 100644 --- a/src/test/java/commands/TestCommand.kt +++ b/src/test/java/commands/ReactiveTest.kt @@ -3,7 +3,7 @@ package commands import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.command.facade.NexusHandler -class TestCommand: NexusHandler { +class ReactiveTest: NexusHandler { val name = "react" val description = "A test regarding React" override fun onEvent(event: NexusCommandEvent) { From 6271bfedd230427e39b96f51b2caef6f74edc0cb Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 15 Oct 2023 22:19:16 +0800 Subject: [PATCH 004/107] feat: properly implement Nexus.R for `NexusMiddlewareEventCore` --- .../nexus/features/command/core/NexusMiddlewareEventCore.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt index 2989e49b..8b94ece4 100644 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt @@ -5,6 +5,7 @@ import pw.mihou.nexus.features.command.facade.NexusCommand import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent import pw.mihou.nexus.features.command.interceptors.core.NexusMiddlewareGateCore +import pw.mihou.nexus.features.command.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.messages.NexusMessage import java.util.concurrent.CompletableFuture @@ -13,6 +14,10 @@ import java.util.function.Function class NexusMiddlewareEventCore(private val _event: NexusCommandEvent, private val gate: NexusMiddlewareGateCore): NexusMiddlewareEvent { override val event: SlashCommandCreateEvent get() = _event.event override val command: NexusCommand get() = _event.command + override fun R(ephemeral: Boolean, react: React.() -> Unit) { + _event.R(ephemeral, react) + } + override fun store(): MutableMap = _event.store() override fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture { return _event.autoDefer(ephemeral, response) From 5a9031e4482fe2e3f9732b5bd933a6f49c238740 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 15 Oct 2023 22:24:38 +0800 Subject: [PATCH 005/107] feat: fix join to string --- .../java/pw/mihou/nexus/features/command/react/React.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index dd8e1654..7fbb2fe8 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -100,7 +100,7 @@ class React(private val ev: NexusCommandEvent) { private var content: String = "" fun view() = content fun Body(vararg nodes: String) { - content = nodes.joinToString() + content = nodes.joinToString("") } } @@ -113,10 +113,10 @@ class React(private val ev: NexusCommandEvent) { } fun Body(vararg nodes: String) { - embed.setDescription(nodes.joinToString()) + embed.setDescription(nodes.joinToString("")) } fun Field(name: String, vararg nodes: String) { - embed.addField(name, nodes.joinToString()) + embed.addField(name, nodes.joinToString("")) } fun Image(url: String) { embed.setImage(url) From 2989110772ad09cdf8952661f89bcda8bfabb950 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 15 Oct 2023 22:26:03 +0800 Subject: [PATCH 006/107] feat: add additional properties to Nexus.R Embed --- .../java/pw/mihou/nexus/features/command/react/React.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 7fbb2fe8..72e19e07 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -11,6 +11,8 @@ import org.javacord.api.listener.interaction.ButtonClickListener import pw.mihou.nexus.core.assignment.NexusUuidAssigner import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.messages.NexusMessage +import java.awt.Color +import java.time.Instant class React(private val ev: NexusCommandEvent) { private var message: NexusMessage = NexusMessage() @@ -124,6 +126,12 @@ class React(private val ev: NexusCommandEvent) { fun Thumbnail(url: String) { embed.setThumbnail(url) } + fun Color(color: Color) { + embed.setColor(color) + } + fun Timestamp(timestamp: Instant) { + embed.setTimestamp(timestamp) + } } interface TextStyles { From e5d95d6f58df869069f21a764a11ddbc2f141b37 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 15 Oct 2023 22:35:55 +0800 Subject: [PATCH 007/107] feat: fix ordered list --- src/main/java/pw/mihou/nexus/features/command/react/React.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 72e19e07..4d3aef86 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -188,7 +188,7 @@ class React(private val ev: NexusCommandEvent) { fun ol(vararg nodes: String): String { var text = "" for ((index, node) in nodes.withIndex()) { - text += "${index + 1}. $node" + text += "${index + 1}. $node\n" } return text } From fa5f1d323818b9a4300ea549f23e43130f69d76e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:13:16 +0800 Subject: [PATCH 008/107] feat: add support for `writable` --- .../command/core/NexusCommandEventCore.kt | 9 +- .../command/facade/NexusCommandEvent.kt | 2 +- .../nexus/features/command/react/React.kt | 118 +++++++++++++++++- src/test/java/commands/ReactiveTest.kt | 2 + 4 files changed, 122 insertions(+), 9 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index 10f7752f..f22e6ad1 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -20,12 +20,15 @@ import java.util.function.Function class NexusCommandEventCore(override val event: SlashCommandCreateEvent, override val command: NexusCommand) : NexusCommandEvent { private val store: MutableMap = HashMap() var updater: AtomicReference?> = AtomicReference(null) - override fun R(ephemeral: Boolean, react: React.() -> Unit) { - autoDefer(ephemeral) { - val r = React(this) + override fun R(ephemeral: Boolean, react: React.() -> Unit): CompletableFuture { + val r = React(this) + return autoDefer(ephemeral) { react(r) return@autoDefer r.view() + }.thenApply { + r.__private__message = it + return@thenApply it } } diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt index 18076d0d..695b3ce5 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt @@ -60,7 +60,7 @@ interface NexusCommandEvent : NexusInteractionEvent Unit) + fun R(ephemeral: Boolean = false, react: React.() -> Unit): CompletableFuture /** * Gets the immediate response builder for this command and adds the diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 4d3aef86..90a7e79f 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -1,5 +1,7 @@ package pw.mihou.nexus.features.command.react +import org.javacord.api.DiscordApi +import org.javacord.api.entity.message.MessageUpdater import org.javacord.api.entity.message.component.ActionRow import org.javacord.api.entity.message.component.ButtonBuilder import org.javacord.api.entity.message.component.ButtonStyle @@ -8,21 +10,110 @@ import org.javacord.api.entity.message.embed.EmbedBuilder import org.javacord.api.event.interaction.ButtonClickEvent import org.javacord.api.listener.GloballyAttachableListener import org.javacord.api.listener.interaction.ButtonClickListener +import org.javacord.api.util.event.ListenerManager +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.core.assignment.NexusUuidAssigner import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.messages.NexusMessage import java.awt.Color import java.time.Instant +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock -class React(private val ev: NexusCommandEvent) { +typealias Subscription = (oldValue: T, newValue: T) -> Unit +typealias Unsubscribe = () -> Unit + +class React(private val api: DiscordApi) { private var message: NexusMessage = NexusMessage() + private var unsubscribe: Unsubscribe = {} + private var component: (Component.() -> Unit)? = null + + private var debounceTask: Cancellable? = null + private var mutex = ReentrantLock() + + internal var __private__message: NexusAutoResponse? = null + fun view() = message fun render(component: Component.() -> Unit) { + val element = apply(component) + + val (unsubscribe, message) = element.render(api) + this.message = message + this.unsubscribe = unsubscribe + } + + private fun apply(component: Component.() -> Unit): Component { + this.component = component val element = Component() component(element) + return element + } + + fun writable(value: T): Writable { + val element = Writable(value) + element.subscribe { _, _ -> + if (!mutex.tryLock()) return@subscribe + val component = this.component ?: return@subscribe + if (debounceTask == null) { + debounceTask = Nexus.launch.scheduler.launch(250) { + this.unsubscribe() + + debounceTask = null + + val msg = __private__message + + val message = msg?.getOrRequestMessage()?.join() + if (message != null) { + val updater = message.createUpdater() + val view = apply(component) + this.unsubscribe = view.render(updater, api) + updater.replaceMessage() + } + } + } + mutex.unlock() + } + return element + } + + class Writable(value: T) { + private val subscribers: MutableList> = mutableListOf() + private val _value: AtomicReference = AtomicReference(value) + fun set(value: T) { + val oldValue = _value.get() + _value.set(value) + + subscribers.forEach { it(oldValue, value) } + } + fun getAndUpdate(updater: (T) -> T) { + val oldValue = _value.get() + _value.getAndUpdate(updater) + + val value = _value.get() + subscribers.forEach { it(oldValue, value) } + } + fun get(): T = _value.get() + fun subscribe(subscription: Subscription): Unsubscribe { + subscribers.add(subscription) + return { subscribers.remove(subscription) } + } + override fun toString(): String { + return _value.get().toString() + } + + override fun hashCode(): Int { + return _value.get().hashCode() + } - message = element.render(ev) + override fun equals(other: Any?): Boolean { + val value = _value.get() + if (other == null && value == null) return true + if (other == null) return false + return value == other + } } class Component { @@ -31,9 +122,25 @@ class React(private val ev: NexusCommandEvent) { private var components: MutableList = mutableListOf() private var listeners: MutableList = mutableListOf() - fun render(event: NexusCommandEvent): NexusMessage { - listeners.forEach { event.api.addListener(it) } - return NexusMessage.with { + private fun attachListeners(api: DiscordApi): Unsubscribe { + val listenerManagers = listeners.map { api.addListener(it) } + return { listenerManagers.forEach { managers -> managers.forEach { it.remove() } } } + } + + fun render(api: DiscordApi): Pair { + return attachListeners(api) to NexusMessage.with { + this.removeAllEmbeds() + this.addEmbeds(embeds) + + if (contents != null) { + this.setContent(contents) + } + components.chunked(3).map { ActionRow.of(it) }.forEach { this.addComponents(it) } + } + } + + fun render(updater: MessageUpdater, api: DiscordApi): Unsubscribe { + updater.apply { this.removeAllEmbeds() this.addEmbeds(embeds) @@ -42,6 +149,7 @@ class React(private val ev: NexusCommandEvent) { } components.chunked(3).map { ActionRow.of(it) }.forEach { this.addComponents(it) } } + return attachListeners(api) } fun Embed(embed: Embed.() -> Unit) { diff --git a/src/test/java/commands/ReactiveTest.kt b/src/test/java/commands/ReactiveTest.kt index 9f61dc4c..67b1a294 100644 --- a/src/test/java/commands/ReactiveTest.kt +++ b/src/test/java/commands/ReactiveTest.kt @@ -8,6 +8,7 @@ class ReactiveTest: NexusHandler { val description = "A test regarding React" override fun onEvent(event: NexusCommandEvent) { event.R { + val clicks = writable(0) render { Embed { Title("R.Embeds") @@ -20,6 +21,7 @@ class ReactiveTest: NexusHandler { } Button(label = "Click to be DM'd") { event -> event.interaction.user.sendMessage("Hi") + clicks.getAndUpdate { it + 1 } } } } From 55382b3bcdafd9ce0fb056ad8ad811b89bb3cbac Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:16:12 +0800 Subject: [PATCH 009/107] feat: fix `NexusMiddlewareEventCore` not implementing properly --- .../nexus/features/command/core/NexusMiddlewareEventCore.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt index 8b94ece4..0cd7b225 100644 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt @@ -1,6 +1,7 @@ package pw.mihou.nexus.features.command.core import org.javacord.api.event.interaction.SlashCommandCreateEvent +import pw.mihou.nexus.Nexus import pw.mihou.nexus.features.command.facade.NexusCommand import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent @@ -14,8 +15,8 @@ import java.util.function.Function class NexusMiddlewareEventCore(private val _event: NexusCommandEvent, private val gate: NexusMiddlewareGateCore): NexusMiddlewareEvent { override val event: SlashCommandCreateEvent get() = _event.event override val command: NexusCommand get() = _event.command - override fun R(ephemeral: Boolean, react: React.() -> Unit) { - _event.R(ephemeral, react) + override fun R(ephemeral: Boolean, react: React.() -> Unit): CompletableFuture { + return _event.R(ephemeral, react) } override fun store(): MutableMap = _event.store() From 2137faca08a1edf2ceb3ad23b9ed96de66d88012 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:18:04 +0800 Subject: [PATCH 010/107] feat: fix build error from type mismatch --- .../mihou/nexus/features/command/core/NexusCommandEventCore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index f22e6ad1..4f9a47b0 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -21,7 +21,7 @@ class NexusCommandEventCore(override val event: SlashCommandCreateEvent, overrid private val store: MutableMap = HashMap() var updater: AtomicReference?> = AtomicReference(null) override fun R(ephemeral: Boolean, react: React.() -> Unit): CompletableFuture { - val r = React(this) + val r = React(this.api) return autoDefer(ephemeral) { react(r) From 02dd7c238fcedf0df767a8f71f1370a37e2e581a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:24:34 +0800 Subject: [PATCH 011/107] feat: cancel previous debounce instead --- .../nexus/features/command/react/React.kt | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 90a7e79f..e959b2c5 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -57,21 +57,20 @@ class React(private val api: DiscordApi) { element.subscribe { _, _ -> if (!mutex.tryLock()) return@subscribe val component = this.component ?: return@subscribe - if (debounceTask == null) { - debounceTask = Nexus.launch.scheduler.launch(250) { - this.unsubscribe() + debounceTask?.cancel(false) + debounceTask = Nexus.launch.scheduler.launch(250) { + this.unsubscribe() - debounceTask = null + debounceTask = null - val msg = __private__message + val msg = __private__message - val message = msg?.getOrRequestMessage()?.join() - if (message != null) { - val updater = message.createUpdater() - val view = apply(component) - this.unsubscribe = view.render(updater, api) - updater.replaceMessage() - } + val message = msg?.getOrRequestMessage()?.join() + if (message != null) { + val updater = message.createUpdater() + val view = apply(component) + this.unsubscribe = view.render(updater, api) + updater.replaceMessage() } } mutex.unlock() From 4b7a8fffcabbf566f9f6060232884ccf77522053 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:25:49 +0800 Subject: [PATCH 012/107] feat: don't re-request the message twice --- .../nexus/features/command/core/NexusCommandEventCore.kt | 2 +- .../java/pw/mihou/nexus/features/command/react/React.kt | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index 4f9a47b0..62822300 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -27,7 +27,7 @@ class NexusCommandEventCore(override val event: SlashCommandCreateEvent, overrid return@autoDefer r.view() }.thenApply { - r.__private__message = it + r.__private__message = it.getOrRequestMessage().join() return@thenApply it } } diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index e959b2c5..dc2ba398 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -1,6 +1,7 @@ package pw.mihou.nexus.features.command.react import org.javacord.api.DiscordApi +import org.javacord.api.entity.message.Message import org.javacord.api.entity.message.MessageUpdater import org.javacord.api.entity.message.component.ActionRow import org.javacord.api.entity.message.component.ButtonBuilder @@ -33,7 +34,7 @@ class React(private val api: DiscordApi) { private var debounceTask: Cancellable? = null private var mutex = ReentrantLock() - internal var __private__message: NexusAutoResponse? = null + internal var __private__message: Message? = null fun view() = message @@ -63,9 +64,7 @@ class React(private val api: DiscordApi) { debounceTask = null - val msg = __private__message - - val message = msg?.getOrRequestMessage()?.join() + val message = __private__message if (message != null) { val updater = message.createUpdater() val view = apply(component) From 3ab70aca98ed9ef6122628c93c63be4c6d872026 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:33:29 +0800 Subject: [PATCH 013/107] feat: run subscribers async --- src/main/java/pw/mihou/nexus/features/command/react/React.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index dc2ba398..6c7f6044 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -84,14 +84,14 @@ class React(private val api: DiscordApi) { val oldValue = _value.get() _value.set(value) - subscribers.forEach { it(oldValue, value) } + subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } } fun getAndUpdate(updater: (T) -> T) { val oldValue = _value.get() _value.getAndUpdate(updater) val value = _value.get() - subscribers.forEach { it(oldValue, value) } + subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } } fun get(): T = _value.get() fun subscribe(subscription: Subscription): Unsubscribe { From 4c6bf0373312270c6bfcf6411c260f3483b5349d Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:45:22 +0800 Subject: [PATCH 014/107] feat: add support for `time` format --- .../pw/mihou/nexus/features/command/react/React.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 6c7f6044..bdd25c19 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -298,6 +298,18 @@ class React(private val api: DiscordApi) { } return text } + fun time(instant: Instant, format: TimeFormat = TimeFormat.RELATIVE) = "" + fun time(instant: Writable, format: TimeFormat = TimeFormat.RELATIVE) = time(instant.get(), format) } } } + +enum class TimeFormat(val suffix: String) { + SHORT_TIME("t"), + LONG_TIME("T"), + SHORT_DATE("d"), + LONG_DATE("D"), + LONG_DATE_WITH_SHORT_TIME("f"), + LONG_DATE_WITH_DAY_OF_WEEK_AND_SHORT_TIME("F"), + RELATIVE("R"); +} \ No newline at end of file From 19681ad513a1fe763b9212ca429ee0e139187400 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:50:10 +0800 Subject: [PATCH 015/107] feat: set debounce millis to a configurable value --- .../java/pw/mihou/nexus/features/command/react/React.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index bdd25c19..29ede25d 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -11,12 +11,9 @@ import org.javacord.api.entity.message.embed.EmbedBuilder import org.javacord.api.event.interaction.ButtonClickEvent import org.javacord.api.listener.GloballyAttachableListener import org.javacord.api.listener.interaction.ButtonClickListener -import org.javacord.api.util.event.ListenerManager import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.core.assignment.NexusUuidAssigner -import pw.mihou.nexus.features.command.facade.NexusCommandEvent -import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.messages.NexusMessage import java.awt.Color import java.time.Instant @@ -36,6 +33,10 @@ class React(private val api: DiscordApi) { internal var __private__message: Message? = null + companion object { + var debounceMillis = 250L + } + fun view() = message fun render(component: Component.() -> Unit) { @@ -59,7 +60,7 @@ class React(private val api: DiscordApi) { if (!mutex.tryLock()) return@subscribe val component = this.component ?: return@subscribe debounceTask?.cancel(false) - debounceTask = Nexus.launch.scheduler.launch(250) { + debounceTask = Nexus.launch.scheduler.launch(debounceMillis) { this.unsubscribe() debounceTask = null From bfb3f9448be4e4d9b827e8ae16f9eec093c0bdfa Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:53:45 +0800 Subject: [PATCH 016/107] feat: clear the uuids after re-render --- .../pw/mihou/nexus/features/command/react/React.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 29ede25d..adeed64e 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -120,10 +120,15 @@ class React(private val api: DiscordApi) { private var contents: String? = null private var components: MutableList = mutableListOf() private var listeners: MutableList = mutableListOf() + private var uuids: MutableList = mutableListOf() private fun attachListeners(api: DiscordApi): Unsubscribe { val listenerManagers = listeners.map { api.addListener(it) } - return { listenerManagers.forEach { managers -> managers.forEach { it.remove() } } } + return { + listenerManagers.forEach { managers -> managers.forEach { it.remove() } } + uuids.forEach { NexusUuidAssigner.deny(it) } + uuids.clear() + } } fun render(api: DiscordApi): Pair { @@ -167,6 +172,7 @@ class React(private val api: DiscordApi) { fun Button(style: ButtonStyle = ButtonStyle.PRIMARY, label: String, + customId: String? = null, emoji: String? = null, disabled: Boolean = false, onClick: ((event: ButtonClickEvent) -> Unit)? = {}) { @@ -180,7 +186,11 @@ class React(private val api: DiscordApi) { button.setDisabled(disabled) - val uuid = NexusUuidAssigner.request() + val uuid = customId ?: run { + val id = NexusUuidAssigner.request() + uuids.add(id) + return@run id + } button.setCustomId(uuid) if (onClick != null) { From 56a29ad76ae5d2bf56b54401018f7e51dc15a84c Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:55:46 +0800 Subject: [PATCH 017/107] feat: add support for inlining fields --- src/main/java/pw/mihou/nexus/features/command/react/React.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index adeed64e..44b9d81a 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -234,8 +234,8 @@ class React(private val api: DiscordApi) { fun Body(vararg nodes: String) { embed.setDescription(nodes.joinToString("")) } - fun Field(name: String, vararg nodes: String) { - embed.addField(name, nodes.joinToString("")) + fun Field(name: String, inline: Boolean = false, vararg nodes: String) { + embed.addField(name, nodes.joinToString(""), inline) } fun Image(url: String) { embed.setImage(url) From fd171520b50cd36ff87c637f73e3aa3c5a3ea1d5 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 01:59:10 +0800 Subject: [PATCH 018/107] feat: add overloads for `Image` and `Thumbnail` --- .../nexus/features/command/react/React.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 44b9d81a..5baf5f27 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -1,6 +1,7 @@ package pw.mihou.nexus.features.command.react import org.javacord.api.DiscordApi +import org.javacord.api.entity.Icon import org.javacord.api.entity.message.Message import org.javacord.api.entity.message.MessageUpdater import org.javacord.api.entity.message.component.ActionRow @@ -16,6 +17,9 @@ import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.core.assignment.NexusUuidAssigner import pw.mihou.nexus.features.messages.NexusMessage import java.awt.Color +import java.awt.image.BufferedImage +import java.io.File +import java.io.InputStream import java.time.Instant import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock @@ -240,9 +244,57 @@ class React(private val api: DiscordApi) { fun Image(url: String) { embed.setImage(url) } + fun Image(image: Icon) { + embed.setImage(image) + } + fun Image(image: File) { + embed.setImage(image) + } + fun Image(image: InputStream) { + embed.setImage(image) + } + fun Image(image: InputStream, fileType: String) { + embed.setImage(image, fileType) + } + fun Image(image: ByteArray) { + embed.setImage(image) + } + fun Image(image: ByteArray, fileType: String) { + embed.setImage(image, fileType) + } + fun Image(image: BufferedImage) { + embed.setImage(image) + } + fun Image(image: BufferedImage, fileType: String) { + embed.setImage(image, fileType) + } fun Thumbnail(url: String) { embed.setThumbnail(url) } + fun Thumbnail(thumbnail: Icon) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: File) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: InputStream) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: InputStream, fileType: String) { + embed.setThumbnail(thumbnail, fileType) + } + fun Thumbnail(thumbnail: ByteArray) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: ByteArray, fileType: String) { + embed.setThumbnail(thumbnail, fileType) + } + fun Thumbnail(thumbnail: BufferedImage) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: BufferedImage, fileType: String) { + embed.setThumbnail(thumbnail, fileType) + } fun Color(color: Color) { embed.setColor(color) } From ec943ebc12bdb3cf46ad3299c14fdaf4a66fd2db Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 02:01:57 +0800 Subject: [PATCH 019/107] feat: add `Footer` --- .../nexus/features/command/react/React.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 5baf5f27..6188cdea 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -301,6 +301,36 @@ class React(private val api: DiscordApi) { fun Timestamp(timestamp: Instant) { embed.setTimestamp(timestamp) } + fun Footer(text: String) { + embed.setFooter(text) + } + fun Footer(text: String, iconUrl: String) { + embed.setFooter(text, iconUrl) + } + fun Footer(text: String, icon: Icon) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: File) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: InputStream) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: InputStream, fileType: String) { + embed.setFooter(text, icon, fileType) + } + fun Footer(text: String, icon: ByteArray) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: ByteArray, fileType: String) { + embed.setFooter(text, icon, fileType) + } + fun Footer(text: String, icon: BufferedImage) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: BufferedImage, fileType: String) { + embed.setFooter(text, icon, fileType) + } } interface TextStyles { From 12f63f6569b25964651f80276a4f70ec6c69a279 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 02:05:09 +0800 Subject: [PATCH 020/107] feat: add `Author` and overloads --- .../nexus/features/command/react/React.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 6188cdea..fedb0b7a 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -3,12 +3,14 @@ package pw.mihou.nexus.features.command.react import org.javacord.api.DiscordApi import org.javacord.api.entity.Icon import org.javacord.api.entity.message.Message +import org.javacord.api.entity.message.MessageAuthor import org.javacord.api.entity.message.MessageUpdater import org.javacord.api.entity.message.component.ActionRow import org.javacord.api.entity.message.component.ButtonBuilder import org.javacord.api.entity.message.component.ButtonStyle import org.javacord.api.entity.message.component.LowLevelComponent import org.javacord.api.entity.message.embed.EmbedBuilder +import org.javacord.api.entity.user.User import org.javacord.api.event.interaction.ButtonClickEvent import org.javacord.api.listener.GloballyAttachableListener import org.javacord.api.listener.interaction.ButtonClickListener @@ -331,6 +333,42 @@ class React(private val api: DiscordApi) { fun Footer(text: String, icon: BufferedImage, fileType: String) { embed.setFooter(text, icon, fileType) } + fun Author(name: String) { + embed.setAuthor(name) + } + fun Author(author: MessageAuthor) { + embed.setAuthor(author) + } + fun Author(author: User) { + embed.setAuthor(author) + } + fun Author(name: String, url: String, iconUrl: String) { + embed.setAuthor(name, url, iconUrl) + } + fun Author(name: String, url: String, icon: Icon) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: File) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: InputStream) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: InputStream, fileType: String) { + embed.setAuthor(name, url, icon, fileType) + } + fun Author(name: String, url: String, icon: ByteArray) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: ByteArray, fileType: String) { + embed.setAuthor(name, url, icon, fileType) + } + fun Author(name: String, url: String, icon: BufferedImage) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: BufferedImage, fileType: String) { + embed.setAuthor(name, url, icon, fileType) + } } interface TextStyles { From af44b47253ceee9767b20894853dc287a5925844 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 02:08:22 +0800 Subject: [PATCH 021/107] feat: add support for `strikethrough` --- .../nexus/features/command/react/React.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index fedb0b7a..39a64f32 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -372,7 +372,8 @@ class React(private val api: DiscordApi) { } interface TextStyles { - private fun renderTextStyles(bold: Boolean = false, underline: Boolean = false, italic: Boolean = false): Pair { + private fun renderTextStyles(bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, + strikethrough: Boolean = false): Pair { var prefix = "" var suffix = "" @@ -395,18 +396,25 @@ class React(private val api: DiscordApi) { suffix += "*" } + if (strikethrough) { + prefix += "~~" + suffix += "~~" + } + return prefix to suffix } - fun p(text: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false): String { - val (prefix, suffix) = renderTextStyles(bold, underline, italic) + fun p(text: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, + strikethrough: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough) return prefix + text + suffix } fun br(): String = "\n" - fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false): String { - val (prefix, suffix) = renderTextStyles(bold, underline, italic) + fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, + strikethrough: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough) return "$prefix[$text]($href)$suffix" } From cce050dff8354c497f62d872fb4904dc0b6d945f Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 02:09:34 +0800 Subject: [PATCH 022/107] feat: add support for `spoiler` --- .../mihou/nexus/features/command/react/React.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 39a64f32..7b6dde4e 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -373,7 +373,7 @@ class React(private val api: DiscordApi) { interface TextStyles { private fun renderTextStyles(bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false): Pair { + strikethrough: Boolean = false, spoiler: Boolean): Pair { var prefix = "" var suffix = "" @@ -401,20 +401,25 @@ class React(private val api: DiscordApi) { suffix += "~~" } + if (spoiler) { + prefix += "`" + suffix += "`" + } + return prefix to suffix } fun p(text: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false): String { - val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough) + strikethrough: Boolean = false, spoiler: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) return prefix + text + suffix } fun br(): String = "\n" fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false): String { - val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough) + strikethrough: Boolean = false, spoiler: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) return "$prefix[$text]($href)$suffix" } From f7c4f299ed36fca89ccb75320f3c8c2b777a2d30 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 02:10:38 +0800 Subject: [PATCH 023/107] feat: add support for `codeblock` --- src/main/java/pw/mihou/nexus/features/command/react/React.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 7b6dde4e..3310b7b8 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -442,6 +442,9 @@ class React(private val api: DiscordApi) { } return text } + fun codeblock(language: String, vararg nodes: String): String { + return "```$language\n${nodes.joinToString("")}\n```" + } fun time(instant: Instant, format: TimeFormat = TimeFormat.RELATIVE) = "" fun time(instant: Writable, format: TimeFormat = TimeFormat.RELATIVE) = time(instant.get(), format) } From d39bd7c9fa1e2896c6cbb650dfcf0e0030363985 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 02:12:06 +0800 Subject: [PATCH 024/107] feat: add support for `blockquote` --- src/main/java/pw/mihou/nexus/features/command/react/React.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 3310b7b8..94d519b9 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -445,6 +445,9 @@ class React(private val api: DiscordApi) { fun codeblock(language: String, vararg nodes: String): String { return "```$language\n${nodes.joinToString("")}\n```" } + fun blockquote(vararg nodes: String): String { + return nodes.joinToString("\n") { "> $it"} + } fun time(instant: Instant, format: TimeFormat = TimeFormat.RELATIVE) = "" fun time(instant: Writable, format: TimeFormat = TimeFormat.RELATIVE) = time(instant.get(), format) } From b66c37ea8bdffc41b2c9defafc6a51b01f139c2e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 07:08:08 +0800 Subject: [PATCH 025/107] feat: add `expand` function to allow using writables outside --- .../java/pw/mihou/nexus/features/command/react/React.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 94d519b9..5c9fe0b5 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -62,7 +62,11 @@ class React(private val api: DiscordApi) { fun writable(value: T): Writable { val element = Writable(value) - element.subscribe { _, _ -> + return expand(element) + } + + fun expand(writable: Writable): Writable { + writable.subscribe { _, _ -> if (!mutex.tryLock()) return@subscribe val component = this.component ?: return@subscribe debounceTask?.cancel(false) @@ -81,7 +85,7 @@ class React(private val api: DiscordApi) { } mutex.unlock() } - return element + return writable } class Writable(value: T) { From 279895c60662ba291a47cd70f2ceae87251967b1 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 09:20:20 +0800 Subject: [PATCH 026/107] feat: add support for delegation --- .../pw/mihou/nexus/features/command/react/React.kt | 10 +++++++++- src/test/java/commands/ReactiveTest.kt | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 5c9fe0b5..93715386 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -25,11 +25,13 @@ import java.io.InputStream import java.time.Instant import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock +import kotlin.properties.Delegates +import kotlin.reflect.KProperty typealias Subscription = (oldValue: T, newValue: T) -> Unit typealias Unsubscribe = () -> Unit -class React(private val api: DiscordApi) { +class React internal constructor(private val api: DiscordApi) { private var message: NexusMessage = NexusMessage() private var unsubscribe: Unsubscribe = {} private var component: (Component.() -> Unit)? = null @@ -91,6 +93,12 @@ class React(private val api: DiscordApi) { class Writable(value: T) { private val subscribers: MutableList> = mutableListOf() private val _value: AtomicReference = AtomicReference(value) + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + return _value.get() + } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + set(value) + } fun set(value: T) { val oldValue = _value.get() _value.set(value) diff --git a/src/test/java/commands/ReactiveTest.kt b/src/test/java/commands/ReactiveTest.kt index 67b1a294..d11a15ac 100644 --- a/src/test/java/commands/ReactiveTest.kt +++ b/src/test/java/commands/ReactiveTest.kt @@ -8,7 +8,7 @@ class ReactiveTest: NexusHandler { val description = "A test regarding React" override fun onEvent(event: NexusCommandEvent) { event.R { - val clicks = writable(0) + var clicks by writable(0) render { Embed { Title("R.Embeds") @@ -21,7 +21,7 @@ class ReactiveTest: NexusHandler { } Button(label = "Click to be DM'd") { event -> event.interaction.user.sendMessage("Hi") - clicks.getAndUpdate { it + 1 } + clicks += 1 } } } From f8bc6a59b796d4570537c1da3eaca277ed5a4dd9 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 10:54:04 +0800 Subject: [PATCH 027/107] feat: organize code for `React` --- .../command/core/NexusCommandEventCore.kt | 2 +- .../nexus/features/command/react/React.kt | 327 +----------------- .../features/command/react/elements/Button.kt | 46 +++ .../features/command/react/elements/Embed.kt | 162 +++++++++ .../features/command/react/elements/Text.kt | 19 + .../command/react/elements/UrlButton.kt | 14 + .../command/react/styles/TextStyles.kt | 85 +++++ .../command/react/styles/TimeFormat.kt | 11 + 8 files changed, 345 insertions(+), 321 deletions(-) create mode 100644 src/main/java/pw/mihou/nexus/features/command/react/elements/Button.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/react/elements/Embed.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/react/elements/Text.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/react/elements/UrlButton.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/react/styles/TextStyles.kt create mode 100644 src/main/java/pw/mihou/nexus/features/command/react/styles/TimeFormat.kt diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index 62822300..bb84d386 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -27,7 +27,7 @@ class NexusCommandEventCore(override val event: SlashCommandCreateEvent, overrid return@autoDefer r.view() }.thenApply { - r.__private__message = it.getOrRequestMessage().join() + r.resultingMessage = it.getOrRequestMessage().join() return@thenApply it } } diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 93715386..3764d718 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -1,31 +1,18 @@ package pw.mihou.nexus.features.command.react import org.javacord.api.DiscordApi -import org.javacord.api.entity.Icon import org.javacord.api.entity.message.Message -import org.javacord.api.entity.message.MessageAuthor import org.javacord.api.entity.message.MessageUpdater import org.javacord.api.entity.message.component.ActionRow -import org.javacord.api.entity.message.component.ButtonBuilder -import org.javacord.api.entity.message.component.ButtonStyle import org.javacord.api.entity.message.component.LowLevelComponent import org.javacord.api.entity.message.embed.EmbedBuilder -import org.javacord.api.entity.user.User -import org.javacord.api.event.interaction.ButtonClickEvent import org.javacord.api.listener.GloballyAttachableListener -import org.javacord.api.listener.interaction.ButtonClickListener import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.core.assignment.NexusUuidAssigner import pw.mihou.nexus.features.messages.NexusMessage -import java.awt.Color -import java.awt.image.BufferedImage -import java.io.File -import java.io.InputStream -import java.time.Instant import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock -import kotlin.properties.Delegates import kotlin.reflect.KProperty typealias Subscription = (oldValue: T, newValue: T) -> Unit @@ -39,7 +26,7 @@ class React internal constructor(private val api: DiscordApi) { private var debounceTask: Cancellable? = null private var mutex = ReentrantLock() - internal var __private__message: Message? = null + internal var resultingMessage: Message? = null companion object { var debounceMillis = 250L @@ -77,7 +64,7 @@ class React internal constructor(private val api: DiscordApi) { debounceTask = null - val message = __private__message + val message = resultingMessage if (message != null) { val updater = message.createUpdater() val view = apply(component) @@ -134,11 +121,11 @@ class React internal constructor(private val api: DiscordApi) { } class Component { - private var embeds: MutableList = mutableListOf() - private var contents: String? = null - private var components: MutableList = mutableListOf() - private var listeners: MutableList = mutableListOf() - private var uuids: MutableList = mutableListOf() + internal var embeds: MutableList = mutableListOf() + internal var contents: String? = null + internal var components: MutableList = mutableListOf() + internal var listeners: MutableList = mutableListOf() + internal var uuids: MutableList = mutableListOf() private fun attachListeners(api: DiscordApi): Unsubscribe { val listenerManagers = listeners.map { api.addListener(it) } @@ -173,305 +160,5 @@ class React internal constructor(private val api: DiscordApi) { } return attachListeners(api) } - - fun Embed(embed: Embed.() -> Unit) { - val element = Embed() - embed(element) - - embeds.add(element.view()) - } - - fun Text(text: Text.() -> Unit) { - val element = Text() - text(element) - - contents = element.view() - } - - fun Button(style: ButtonStyle = ButtonStyle.PRIMARY, - label: String, - customId: String? = null, - emoji: String? = null, - disabled: Boolean = false, - onClick: ((event: ButtonClickEvent) -> Unit)? = {}) { - val button = ButtonBuilder() - button.setStyle(style) - button.setLabel(label) - - if (emoji != null) { - button.setEmoji(emoji) - } - - button.setDisabled(disabled) - - val uuid = customId ?: run { - val id = NexusUuidAssigner.request() - uuids.add(id) - return@run id - } - button.setCustomId(uuid) - - if (onClick != null) { - listeners += ButtonClickListener { - if (it.buttonInteraction.customId != uuid) { - return@ButtonClickListener - } - - onClick(it) - } - } - - components += button.build() - } - - fun UrlButton(label: String, url: String, emoji: String? = null) { - val button = ButtonBuilder() - button.setStyle(ButtonStyle.LINK) - button.setLabel(label) - button.setUrl(url) - - components += button.build() - } - - inner class Text: TextStyles { - private var content: String = "" - fun view() = content - fun Body(vararg nodes: String) { - content = nodes.joinToString("") - } - } - - inner class Embed: TextStyles { - private val embed = EmbedBuilder() - fun view() = embed - - fun Title(text: String) { - embed.setTitle(text) - } - - fun Body(vararg nodes: String) { - embed.setDescription(nodes.joinToString("")) - } - fun Field(name: String, inline: Boolean = false, vararg nodes: String) { - embed.addField(name, nodes.joinToString(""), inline) - } - fun Image(url: String) { - embed.setImage(url) - } - fun Image(image: Icon) { - embed.setImage(image) - } - fun Image(image: File) { - embed.setImage(image) - } - fun Image(image: InputStream) { - embed.setImage(image) - } - fun Image(image: InputStream, fileType: String) { - embed.setImage(image, fileType) - } - fun Image(image: ByteArray) { - embed.setImage(image) - } - fun Image(image: ByteArray, fileType: String) { - embed.setImage(image, fileType) - } - fun Image(image: BufferedImage) { - embed.setImage(image) - } - fun Image(image: BufferedImage, fileType: String) { - embed.setImage(image, fileType) - } - fun Thumbnail(url: String) { - embed.setThumbnail(url) - } - fun Thumbnail(thumbnail: Icon) { - embed.setThumbnail(thumbnail) - } - fun Thumbnail(thumbnail: File) { - embed.setThumbnail(thumbnail) - } - fun Thumbnail(thumbnail: InputStream) { - embed.setThumbnail(thumbnail) - } - fun Thumbnail(thumbnail: InputStream, fileType: String) { - embed.setThumbnail(thumbnail, fileType) - } - fun Thumbnail(thumbnail: ByteArray) { - embed.setThumbnail(thumbnail) - } - fun Thumbnail(thumbnail: ByteArray, fileType: String) { - embed.setThumbnail(thumbnail, fileType) - } - fun Thumbnail(thumbnail: BufferedImage) { - embed.setThumbnail(thumbnail) - } - fun Thumbnail(thumbnail: BufferedImage, fileType: String) { - embed.setThumbnail(thumbnail, fileType) - } - fun Color(color: Color) { - embed.setColor(color) - } - fun Timestamp(timestamp: Instant) { - embed.setTimestamp(timestamp) - } - fun Footer(text: String) { - embed.setFooter(text) - } - fun Footer(text: String, iconUrl: String) { - embed.setFooter(text, iconUrl) - } - fun Footer(text: String, icon: Icon) { - embed.setFooter(text, icon) - } - fun Footer(text: String, icon: File) { - embed.setFooter(text, icon) - } - fun Footer(text: String, icon: InputStream) { - embed.setFooter(text, icon) - } - fun Footer(text: String, icon: InputStream, fileType: String) { - embed.setFooter(text, icon, fileType) - } - fun Footer(text: String, icon: ByteArray) { - embed.setFooter(text, icon) - } - fun Footer(text: String, icon: ByteArray, fileType: String) { - embed.setFooter(text, icon, fileType) - } - fun Footer(text: String, icon: BufferedImage) { - embed.setFooter(text, icon) - } - fun Footer(text: String, icon: BufferedImage, fileType: String) { - embed.setFooter(text, icon, fileType) - } - fun Author(name: String) { - embed.setAuthor(name) - } - fun Author(author: MessageAuthor) { - embed.setAuthor(author) - } - fun Author(author: User) { - embed.setAuthor(author) - } - fun Author(name: String, url: String, iconUrl: String) { - embed.setAuthor(name, url, iconUrl) - } - fun Author(name: String, url: String, icon: Icon) { - embed.setAuthor(name, url, icon) - } - fun Author(name: String, url: String, icon: File) { - embed.setAuthor(name, url, icon) - } - fun Author(name: String, url: String, icon: InputStream) { - embed.setAuthor(name, url, icon) - } - fun Author(name: String, url: String, icon: InputStream, fileType: String) { - embed.setAuthor(name, url, icon, fileType) - } - fun Author(name: String, url: String, icon: ByteArray) { - embed.setAuthor(name, url, icon) - } - fun Author(name: String, url: String, icon: ByteArray, fileType: String) { - embed.setAuthor(name, url, icon, fileType) - } - fun Author(name: String, url: String, icon: BufferedImage) { - embed.setAuthor(name, url, icon) - } - fun Author(name: String, url: String, icon: BufferedImage, fileType: String) { - embed.setAuthor(name, url, icon, fileType) - } - } - - interface TextStyles { - private fun renderTextStyles(bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false, spoiler: Boolean): Pair { - var prefix = "" - var suffix = "" - - if (bold) { - prefix += "**" - suffix += "**" - if (italic) { - prefix += "*" - suffix += "*" - } - } - - if (underline) { - prefix += "__" - suffix += "__" - } - - if (italic && !bold) { - prefix += "*" - suffix += "*" - } - - if (strikethrough) { - prefix += "~~" - suffix += "~~" - } - - if (spoiler) { - prefix += "`" - suffix += "`" - } - - return prefix to suffix - } - - fun p(text: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false, spoiler: Boolean = false): String { - val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) - return prefix + text + suffix - } - - fun br(): String = "\n" - - fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false, spoiler: Boolean = false): String { - val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) - return "$prefix[$text]($href)$suffix" - } - - fun h1(text: String): String { - return "# $text" - } - fun h2(text: String): String { - return "## $text" - } - fun h3(text: String): String { - return "### $text" - } - fun ul(vararg nodes: String): String { - return nodes.joinToString("\n") { "* $it" } - } - fun ol(vararg nodes: String): String { - var text = "" - for ((index, node) in nodes.withIndex()) { - text += "${index + 1}. $node\n" - } - return text - } - fun codeblock(language: String, vararg nodes: String): String { - return "```$language\n${nodes.joinToString("")}\n```" - } - fun blockquote(vararg nodes: String): String { - return nodes.joinToString("\n") { "> $it"} - } - fun time(instant: Instant, format: TimeFormat = TimeFormat.RELATIVE) = "" - fun time(instant: Writable, format: TimeFormat = TimeFormat.RELATIVE) = time(instant.get(), format) - } } -} - -enum class TimeFormat(val suffix: String) { - SHORT_TIME("t"), - LONG_TIME("T"), - SHORT_DATE("d"), - LONG_DATE("D"), - LONG_DATE_WITH_SHORT_TIME("f"), - LONG_DATE_WITH_DAY_OF_WEEK_AND_SHORT_TIME("F"), - RELATIVE("R"); } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/react/elements/Button.kt b/src/main/java/pw/mihou/nexus/features/command/react/elements/Button.kt new file mode 100644 index 00000000..2776ab8c --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/react/elements/Button.kt @@ -0,0 +1,46 @@ +package pw.mihou.nexus.features.command.react.elements + +import org.javacord.api.entity.message.component.ButtonBuilder +import org.javacord.api.entity.message.component.ButtonStyle +import org.javacord.api.event.interaction.ButtonClickEvent +import org.javacord.api.listener.interaction.ButtonClickListener +import pw.mihou.nexus.core.assignment.NexusUuidAssigner +import pw.mihou.nexus.features.command.react.React + +fun React.Component.Button( + style: ButtonStyle = ButtonStyle.PRIMARY, + label: String, + customId: String? = null, + emoji: String? = null, + disabled: Boolean = false, + onClick: ((event: ButtonClickEvent) -> Unit)? = {} +) { + val button = ButtonBuilder() + button.setStyle(style) + button.setLabel(label) + + if (emoji != null) { + button.setEmoji(emoji) + } + + button.setDisabled(disabled) + + val uuid = customId ?: run { + val id = NexusUuidAssigner.request() + uuids.add(id) + return@run id + } + button.setCustomId(uuid) + + if (onClick != null) { + listeners += ButtonClickListener { + if (it.buttonInteraction.customId != uuid) { + return@ButtonClickListener + } + + onClick(it) + } + } + + components += button.build() +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/react/elements/Embed.kt b/src/main/java/pw/mihou/nexus/features/command/react/elements/Embed.kt new file mode 100644 index 00000000..11a2f9ee --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/react/elements/Embed.kt @@ -0,0 +1,162 @@ +package pw.mihou.nexus.features.command.react.elements + +import org.javacord.api.entity.Icon +import org.javacord.api.entity.message.MessageAuthor +import org.javacord.api.entity.message.embed.EmbedBuilder +import org.javacord.api.entity.user.User +import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.command.react.styles.TextStyles +import java.awt.Color +import java.awt.image.BufferedImage +import java.io.File +import java.io.InputStream +import java.time.Instant + +fun React.Component.Embed(embed: Embed.() -> Unit) { + val element = Embed() + embed(element) + + embeds.add(element.view()) +} + +class Embed: TextStyles { + private val embed = EmbedBuilder() + fun view() = embed + + fun Title(text: String) { + embed.setTitle(text) + } + + fun Body(vararg nodes: String) { + embed.setDescription(nodes.joinToString("")) + } + fun Field(name: String, inline: Boolean = false, vararg nodes: String) { + embed.addField(name, nodes.joinToString(""), inline) + } + fun Image(url: String) { + embed.setImage(url) + } + fun Image(image: Icon) { + embed.setImage(image) + } + fun Image(image: File) { + embed.setImage(image) + } + fun Image(image: InputStream) { + embed.setImage(image) + } + fun Image(image: InputStream, fileType: String) { + embed.setImage(image, fileType) + } + fun Image(image: ByteArray) { + embed.setImage(image) + } + fun Image(image: ByteArray, fileType: String) { + embed.setImage(image, fileType) + } + fun Image(image: BufferedImage) { + embed.setImage(image) + } + fun Image(image: BufferedImage, fileType: String) { + embed.setImage(image, fileType) + } + fun Thumbnail(url: String) { + embed.setThumbnail(url) + } + fun Thumbnail(thumbnail: Icon) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: File) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: InputStream) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: InputStream, fileType: String) { + embed.setThumbnail(thumbnail, fileType) + } + fun Thumbnail(thumbnail: ByteArray) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: ByteArray, fileType: String) { + embed.setThumbnail(thumbnail, fileType) + } + fun Thumbnail(thumbnail: BufferedImage) { + embed.setThumbnail(thumbnail) + } + fun Thumbnail(thumbnail: BufferedImage, fileType: String) { + embed.setThumbnail(thumbnail, fileType) + } + fun Color(color: Color) { + embed.setColor(color) + } + fun Timestamp(timestamp: Instant) { + embed.setTimestamp(timestamp) + } + fun Footer(text: String) { + embed.setFooter(text) + } + fun Footer(text: String, iconUrl: String) { + embed.setFooter(text, iconUrl) + } + fun Footer(text: String, icon: Icon) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: File) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: InputStream) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: InputStream, fileType: String) { + embed.setFooter(text, icon, fileType) + } + fun Footer(text: String, icon: ByteArray) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: ByteArray, fileType: String) { + embed.setFooter(text, icon, fileType) + } + fun Footer(text: String, icon: BufferedImage) { + embed.setFooter(text, icon) + } + fun Footer(text: String, icon: BufferedImage, fileType: String) { + embed.setFooter(text, icon, fileType) + } + fun Author(name: String) { + embed.setAuthor(name) + } + fun Author(author: MessageAuthor) { + embed.setAuthor(author) + } + fun Author(author: User) { + embed.setAuthor(author) + } + fun Author(name: String, url: String, iconUrl: String) { + embed.setAuthor(name, url, iconUrl) + } + fun Author(name: String, url: String, icon: Icon) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: File) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: InputStream) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: InputStream, fileType: String) { + embed.setAuthor(name, url, icon, fileType) + } + fun Author(name: String, url: String, icon: ByteArray) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: ByteArray, fileType: String) { + embed.setAuthor(name, url, icon, fileType) + } + fun Author(name: String, url: String, icon: BufferedImage) { + embed.setAuthor(name, url, icon) + } + fun Author(name: String, url: String, icon: BufferedImage, fileType: String) { + embed.setAuthor(name, url, icon, fileType) + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/react/elements/Text.kt b/src/main/java/pw/mihou/nexus/features/command/react/elements/Text.kt new file mode 100644 index 00000000..f96c357b --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/react/elements/Text.kt @@ -0,0 +1,19 @@ +package pw.mihou.nexus.features.command.react.elements + +import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.command.react.styles.TextStyles + +fun React.Component.Text(text: Text.() -> Unit) { + val element = Text() + text(element) + + contents = element.view() +} + +class Text: TextStyles { + private var content: String = "" + fun view() = content + fun Body(vararg nodes: String) { + content = nodes.joinToString("") + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/react/elements/UrlButton.kt b/src/main/java/pw/mihou/nexus/features/command/react/elements/UrlButton.kt new file mode 100644 index 00000000..ffb6af06 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/react/elements/UrlButton.kt @@ -0,0 +1,14 @@ +package pw.mihou.nexus.features.command.react.elements + +import org.javacord.api.entity.message.component.ButtonBuilder +import org.javacord.api.entity.message.component.ButtonStyle +import pw.mihou.nexus.features.command.react.React + +fun React.Component.UrlButton(label: String, url: String, emoji: String? = null) { + val button = ButtonBuilder() + button.setStyle(ButtonStyle.LINK) + button.setLabel(label) + button.setUrl(url) + + components += button.build() +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/command/react/styles/TextStyles.kt new file mode 100644 index 00000000..ff46420a --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/react/styles/TextStyles.kt @@ -0,0 +1,85 @@ +package pw.mihou.nexus.features.command.react.styles + +import pw.mihou.nexus.features.command.react.React +import java.time.Instant + +interface TextStyles { + private fun renderTextStyles(bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, + strikethrough: Boolean = false, spoiler: Boolean): Pair { + var prefix = "" + var suffix = "" + + if (bold) { + prefix += "**" + suffix += "**" + if (italic) { + prefix += "*" + suffix += "*" + } + } + + if (underline) { + prefix += "__" + suffix += "__" + } + + if (italic && !bold) { + prefix += "*" + suffix += "*" + } + + if (strikethrough) { + prefix += "~~" + suffix += "~~" + } + + if (spoiler) { + prefix += "`" + suffix += "`" + } + + return prefix to suffix + } + + fun p(text: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, + strikethrough: Boolean = false, spoiler: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) + return prefix + text + suffix + } + + fun br(): String = "\n" + + fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, + strikethrough: Boolean = false, spoiler: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) + return "$prefix[$text]($href)$suffix" + } + + fun h1(text: String): String { + return "# $text" + } + fun h2(text: String): String { + return "## $text" + } + fun h3(text: String): String { + return "### $text" + } + fun ul(vararg nodes: String): String { + return nodes.joinToString("\n") { "* $it" } + } + fun ol(vararg nodes: String): String { + var text = "" + for ((index, node) in nodes.withIndex()) { + text += "${index + 1}. $node\n" + } + return text + } + fun codeblock(language: String, vararg nodes: String): String { + return "```$language\n${nodes.joinToString("")}\n```" + } + fun blockquote(vararg nodes: String): String { + return nodes.joinToString("\n") { "> $it"} + } + fun time(instant: Instant, format: TimeFormat = TimeFormat.RELATIVE) = "" + fun time(instant: React.Writable, format: TimeFormat = TimeFormat.RELATIVE) = time(instant.get(), format) +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/command/react/styles/TimeFormat.kt b/src/main/java/pw/mihou/nexus/features/command/react/styles/TimeFormat.kt new file mode 100644 index 00000000..d08425f7 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/command/react/styles/TimeFormat.kt @@ -0,0 +1,11 @@ +package pw.mihou.nexus.features.command.react.styles + +enum class TimeFormat(val suffix: String) { + SHORT_TIME("t"), + LONG_TIME("T"), + SHORT_DATE("d"), + LONG_DATE("D"), + LONG_DATE_WITH_SHORT_TIME("f"), + LONG_DATE_WITH_DAY_OF_WEEK_AND_SHORT_TIME("F"), + RELATIVE("R"); +} \ No newline at end of file From b8bc0da29c00c8fafaf7addd7112fb3b12910652 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 10:54:57 +0800 Subject: [PATCH 028/107] feat: fix tests --- src/test/java/commands/ReactiveTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/commands/ReactiveTest.kt b/src/test/java/commands/ReactiveTest.kt index d11a15ac..4d4490b2 100644 --- a/src/test/java/commands/ReactiveTest.kt +++ b/src/test/java/commands/ReactiveTest.kt @@ -2,6 +2,8 @@ package commands import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.command.react.elements.Button +import pw.mihou.nexus.features.command.react.elements.Embed class ReactiveTest: NexusHandler { val name = "react" From c66f83cd307125d46faf98d47e37d3892b140b42 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 10:59:25 +0800 Subject: [PATCH 029/107] feat: add support for first and render subscriptions --- .../nexus/features/command/react/React.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 3764d718..dcc46b3b 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -18,8 +18,10 @@ import kotlin.reflect.KProperty typealias Subscription = (oldValue: T, newValue: T) -> Unit typealias Unsubscribe = () -> Unit +typealias RenderSubscription = () -> Unit + class React internal constructor(private val api: DiscordApi) { - private var message: NexusMessage = NexusMessage() + private var message: NexusMessage? = null private var unsubscribe: Unsubscribe = {} private var component: (Component.() -> Unit)? = null @@ -28,15 +30,32 @@ class React internal constructor(private val api: DiscordApi) { internal var resultingMessage: Message? = null + internal var firstRenderSubscribers = mutableListOf() + internal var renderSubscribers = mutableListOf() + companion object { var debounceMillis = 250L } - fun view() = message + fun view() = message ?: NexusMessage() + + fun onRender(subscription: RenderSubscription) { + renderSubscribers.add(subscription) + } + + fun onInitialRender(subscription: RenderSubscription) { + firstRenderSubscribers.add(subscription) + } fun render(component: Component.() -> Unit) { val element = apply(component) + if (message == null) { + firstRenderSubscribers.forEach { it() } + } + + renderSubscribers.forEach { it() } + val (unsubscribe, message) = element.render(api) this.message = message this.unsubscribe = unsubscribe From 1acd9350055d2faf39530cc97f3edea2540fd5bd Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 11:30:13 +0800 Subject: [PATCH 030/107] feat: fix `on:render` and `on:initialRener` happening after render --- src/main/java/pw/mihou/nexus/features/command/react/React.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index dcc46b3b..65191078 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -48,14 +48,14 @@ class React internal constructor(private val api: DiscordApi) { } fun render(component: Component.() -> Unit) { - val element = apply(component) - if (message == null) { firstRenderSubscribers.forEach { it() } } renderSubscribers.forEach { it() } + val element = apply(component) + val (unsubscribe, message) = element.render(api) this.message = message this.unsubscribe = unsubscribe From 34aa81176bd4c77e0f46e79772ffc41070bebd21 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 11:34:45 +0800 Subject: [PATCH 031/107] feat: fix re-rendering not working --- .../pw/mihou/nexus/features/command/react/React.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 65191078..5002049b 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -48,12 +48,6 @@ class React internal constructor(private val api: DiscordApi) { } fun render(component: Component.() -> Unit) { - if (message == null) { - firstRenderSubscribers.forEach { it() } - } - - renderSubscribers.forEach { it() } - val element = apply(component) val (unsubscribe, message) = element.render(api) @@ -64,6 +58,13 @@ class React internal constructor(private val api: DiscordApi) { private fun apply(component: Component.() -> Unit): Component { this.component = component val element = Component() + + if (message == null) { + firstRenderSubscribers.forEach { it() } + } + + renderSubscribers.forEach { it() } + component(element) return element } From e5ea3a0b4818aba58804ba45ac1607145ce2be99 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 18:49:13 +0800 Subject: [PATCH 032/107] feat: expand `React` to support message render mode --- .../command/core/NexusCommandEventCore.kt | 4 +- .../nexus/features/command/react/React.kt | 51 ++++++++++++++++--- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index bb84d386..b849af49 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -21,11 +21,11 @@ class NexusCommandEventCore(override val event: SlashCommandCreateEvent, overrid private val store: MutableMap = HashMap() var updater: AtomicReference?> = AtomicReference(null) override fun R(ephemeral: Boolean, react: React.() -> Unit): CompletableFuture { - val r = React(this.api) + val r = React(this.api, React.RenderMode.Interaction) return autoDefer(ephemeral) { react(r) - return@autoDefer r.view() + return@autoDefer r.message!! }.thenApply { r.resultingMessage = it.getOrRequestMessage().join() return@thenApply it diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/command/react/React.kt index 5002049b..3842a00f 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/command/react/React.kt @@ -2,6 +2,7 @@ package pw.mihou.nexus.features.command.react import org.javacord.api.DiscordApi import org.javacord.api.entity.message.Message +import org.javacord.api.entity.message.MessageBuilder import org.javacord.api.entity.message.MessageUpdater import org.javacord.api.entity.message.component.ActionRow import org.javacord.api.entity.message.component.LowLevelComponent @@ -20,8 +21,12 @@ typealias Unsubscribe = () -> Unit typealias RenderSubscription = () -> Unit -class React internal constructor(private val api: DiscordApi) { - private var message: NexusMessage? = null +class React internal constructor(private val api: DiscordApi, private val renderMode: RenderMode) { + internal var rendered: Boolean = false + + internal var message: NexusMessage? = null + internal var messageBuilder: MessageBuilder? = null + private var unsubscribe: Unsubscribe = {} private var component: (Component.() -> Unit)? = null @@ -37,7 +42,10 @@ class React internal constructor(private val api: DiscordApi) { var debounceMillis = 250L } - fun view() = message ?: NexusMessage() + internal enum class RenderMode { + Interaction, + Message + } fun onRender(subscription: RenderSubscription) { renderSubscribers.add(subscription) @@ -48,18 +56,30 @@ class React internal constructor(private val api: DiscordApi) { } fun render(component: Component.() -> Unit) { - val element = apply(component) + val element = apply(component) + + when(renderMode) { + RenderMode.Interaction -> { + val (unsubscribe, message) = element.render(api) + this.message = message + this.unsubscribe = unsubscribe + } + RenderMode.Message -> { + val builder = MessageBuilder() + val unsubscribe = element.render(builder, api) - val (unsubscribe, message) = element.render(api) - this.message = message - this.unsubscribe = unsubscribe + this.messageBuilder = builder + this.unsubscribe = unsubscribe + } + } + this.rendered = true } private fun apply(component: Component.() -> Unit): Component { this.component = component val element = Component() - if (message == null) { + if (!rendered) { firstRenderSubscribers.forEach { it() } } @@ -168,6 +188,8 @@ class React internal constructor(private val api: DiscordApi) { } } + // TODO: Reduce code duplication by using MessageBuilderBase (currently package-private) + // https://discord.com/channels/151037561152733184/151326093482262528/1163425854186065951 fun render(updater: MessageUpdater, api: DiscordApi): Unsubscribe { updater.apply { this.removeAllEmbeds() @@ -180,5 +202,18 @@ class React internal constructor(private val api: DiscordApi) { } return attachListeners(api) } + + fun render(builder: MessageBuilder, api: DiscordApi): Unsubscribe { + builder.apply { + this.removeAllEmbeds() + this.addEmbeds(embeds) + + if (contents != null) { + this.setContent(contents) + } + components.chunked(3).map { ActionRow.of(it) }.forEach { this.addComponents(it) } + } + return attachListeners(api) + } } } \ No newline at end of file From 8ccd8add30704f5a332f8c8d16ff4731142368e3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 18:56:29 +0800 Subject: [PATCH 033/107] feat: expand `React` to Context Menus and dedupe interaction events --- .../command/core/NexusCommandEventCore.kt | 63 +--------------- .../command/facade/NexusCommandEvent.kt | 23 ------ .../nexus/features/commons/Deferrable.kt | 72 +++++++++++++++++++ .../features/commons/NexusInteractionEvent.kt | 37 ++++++++++ .../contexts/NexusContextMenuEvent.kt | 20 +++++- 5 files changed, 131 insertions(+), 84 deletions(-) create mode 100644 src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index b849af49..a5b10478 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -10,6 +10,7 @@ import pw.mihou.nexus.features.command.facade.NexusCommand import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.command.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse +import pw.mihou.nexus.features.commons.Deferrable import pw.mihou.nexus.features.messages.NexusMessage import java.time.Instant import java.util.concurrent.CompletableFuture @@ -20,69 +21,11 @@ import java.util.function.Function class NexusCommandEventCore(override val event: SlashCommandCreateEvent, override val command: NexusCommand) : NexusCommandEvent { private val store: MutableMap = HashMap() var updater: AtomicReference?> = AtomicReference(null) - override fun R(ephemeral: Boolean, react: React.() -> Unit): CompletableFuture { - val r = React(this.api, React.RenderMode.Interaction) - return autoDefer(ephemeral) { - react(r) - - return@autoDefer r.message!! - }.thenApply { - r.resultingMessage = it.getOrRequestMessage().join() - return@thenApply it - } - } override fun store(): MutableMap = store - override fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture { - var task: Cancellable? = null - val deferredTaskRan = AtomicBoolean(false) - if (updater.get() == null) { - val timeUntil = Instant.now().toEpochMilli() - event.interaction.creationTimestamp - .minusMillis(Nexus.configuration.global.autoDeferAfterMilliseconds) - .toEpochMilli() - - task = Nexus.launch.scheduler.launch(timeUntil) { - if (updater.get() == null) { - respondLaterEphemerallyIf(ephemeral).exceptionally(ExceptionLogger.get()) - } - deferredTaskRan.set(true) - } - } - val future = CompletableFuture() - Nexus.launcher.launch { - try { - val message = response.apply(null) - if (!deferredTaskRan.get() && task != null) { - task.cancel(false) - } - val updater = updater.get() - if (updater == null) { - val responder = respondNow() - if (ephemeral) { - responder.setFlags(MessageFlag.EPHEMERAL) - } - message.into(responder).respond() - .thenAccept { r -> future.complete(NexusAutoResponse(r, null)) } - .exceptionally { exception -> - future.completeExceptionally(exception) - return@exceptionally null - } - } else { - val completedUpdater = updater.join() - message.into(completedUpdater).update() - .thenAccept { r -> future.complete(NexusAutoResponse(null, r)) } - .exceptionally { exception -> - future.completeExceptionally(exception) - return@exceptionally null - } - } - } catch (exception: Exception) { - future.completeExceptionally(exception) - } - } - return future - } + override fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture = + Deferrable.autoDefer(this, updater, ephemeral, response) override fun respondLater(): CompletableFuture { return updater.updateAndGet { interaction.respondLater() }!! diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt index 695b3ce5..c073921c 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt @@ -50,18 +50,6 @@ interface NexusCommandEvent : NexusInteractionEvent Unit): CompletableFuture - /** * Gets the immediate response builder for this command and adds the * [MessageFlag.EPHEMERAL] flag ahead of time. @@ -184,15 +172,4 @@ interface NexusCommandEvent : NexusInteractionEvent): CompletableFuture } diff --git a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt new file mode 100644 index 00000000..4e08d059 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt @@ -0,0 +1,72 @@ +package pw.mihou.nexus.features.commons + +import org.javacord.api.entity.message.MessageFlag +import org.javacord.api.interaction.Interaction +import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater +import org.javacord.api.util.logging.ExceptionLogger +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.configuration.modules.Cancellable +import pw.mihou.nexus.features.command.responses.NexusAutoResponse +import pw.mihou.nexus.features.messages.NexusMessage +import java.time.Instant +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Function + +object Deferrable { + internal fun autoDefer( + event: NexusInteractionEvent<*, *>, + updater: AtomicReference?>, + ephemeral: Boolean, + response: Function + ): CompletableFuture { + var task: Cancellable? = null + val deferredTaskRan = AtomicBoolean(false) + if (updater.get() == null) { + val timeUntil = Instant.now().toEpochMilli() - event.interaction.creationTimestamp + .minusMillis(Nexus.configuration.global.autoDeferAfterMilliseconds) + .toEpochMilli() + + task = Nexus.launch.scheduler.launch(timeUntil) { + if (updater.get() == null) { + event.respondLaterEphemerallyIf(ephemeral).exceptionally(ExceptionLogger.get()) + } + deferredTaskRan.set(true) + } + } + val future = CompletableFuture() + Nexus.launcher.launch { + try { + val message = response.apply(null) + if (!deferredTaskRan.get() && task != null) { + task.cancel(false) + } + val updater = updater.get() + if (updater == null) { + val responder = event.respondNow() + if (ephemeral) { + responder.setFlags(MessageFlag.EPHEMERAL) + } + message.into(responder).respond() + .thenAccept { r -> future.complete(NexusAutoResponse(r, null)) } + .exceptionally { exception -> + future.completeExceptionally(exception) + return@exceptionally null + } + } else { + val completedUpdater = updater.join() + message.into(completedUpdater).update() + .thenAccept { r -> future.complete(NexusAutoResponse(null, r)) } + .exceptionally { exception -> + future.completeExceptionally(exception) + return@exceptionally null + } + } + } catch (exception: Exception) { + future.completeExceptionally(exception) + } + } + return future + } +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt index e82c1b14..871b525c 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt @@ -12,8 +12,12 @@ import org.javacord.api.interaction.ApplicationCommandInteraction import org.javacord.api.interaction.Interaction import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater +import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.command.responses.NexusAutoResponse +import pw.mihou.nexus.features.messages.NexusMessage import java.util.* import java.util.concurrent.CompletableFuture +import java.util.function.Function import java.util.function.Predicate import kotlin.NoSuchElementException @@ -180,4 +184,37 @@ interface NexusInteractionEvent) = respondLaterEphemerallyIf(predicate.test(null)) + + /** + * Automatically answers either deferred or non-deferred based on circumstances, to configure the time that it should + * consider before deferring (this is based on time now - (interaction creation time - auto defer time)), you can + * modify [pw.mihou.nexus.configuration.modules.NexusGlobalConfiguration.autoDeferAfterMilliseconds]. + * + * @param ephemeral whether to respond ephemerally or not. + * @param response the response to send to Discord. + * @return the response from Discord. + */ + fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture + + /** + * An experimental feature to use the new Nexus.R rendering mechanism to render Discord messages + * with a syntax similar to a template engine. In future versions of this experiment, we plan on supporting + * more reactivity (i.e. supporting states via subscribe mechanism). + * + * This internally uses [autoDefer] to assist in sending the initial update response. In the reactive change, + * this will also include its own debounced message updating mechanism to allow for updating the message upon + * state changes. + */ + @JvmSynthetic + fun R(ephemeral: Boolean = false, react: React.() -> Unit): CompletableFuture { + val r = React(this.api, React.RenderMode.Interaction) + return autoDefer(ephemeral) { + react(r) + + return@autoDefer r.message!! + }.thenApply { + r.resultingMessage = it.getOrRequestMessage().join() + return@thenApply it + } + } } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt b/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt index 0dfd67f7..81e26948 100644 --- a/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt @@ -1,9 +1,27 @@ package pw.mihou.nexus.features.contexts +import org.javacord.api.entity.message.MessageFlag import org.javacord.api.event.interaction.ApplicationCommandEvent +import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater +import org.javacord.api.util.logging.ExceptionLogger +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.configuration.modules.Cancellable +import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.command.responses.NexusAutoResponse +import pw.mihou.nexus.features.commons.Deferrable import pw.mihou.nexus.features.commons.NexusInteractionEvent +import pw.mihou.nexus.features.messages.NexusMessage +import java.time.Instant +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Function class NexusContextMenuEvent( val contextMenu: NexusContextMenu, override val event: Event, override val interaction: Interaction -): NexusInteractionEvent \ No newline at end of file +): NexusInteractionEvent { + private var updater: AtomicReference?> = AtomicReference(null) + override fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture = + Deferrable.autoDefer(this, updater, ephemeral, response) +} \ No newline at end of file From aef88b7a540da72ce7e794ba2de6f353ede67d23 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 18:59:59 +0800 Subject: [PATCH 034/107] feat: expand `React` to messages --- .../features/commons/NexusInteractionEvent.kt | 10 ++++------ .../features/messages/ReactExtensions.kt | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt diff --git a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt index 871b525c..6805173a 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt @@ -198,12 +198,10 @@ interface NexusInteractionEvent Unit): CompletableFuture { diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt new file mode 100644 index 00000000..0f0e8f6c --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -0,0 +1,20 @@ +package pw.mihou.nexus.features.messages + +import org.javacord.api.entity.message.Message +import org.javacord.api.event.message.MessageCreateEvent +import pw.mihou.nexus.features.command.react.React +import java.util.concurrent.CompletableFuture + +/** + * An experimental feature to use the new Nexus.R rendering mechanism to render Discord messages + * with a syntax similar to a template engine that sports states (writable) that can easily update message + * upon state changes. + * @param react the entire procedure over how rendering the response works. + */ +@JvmSynthetic +fun MessageCreateEvent.R(react: React.() -> Unit): CompletableFuture { + val r = React(this.api, React.RenderMode.Interaction) + react(r) + + return r.messageBuilder!!.replyTo(message).send(channel) +} \ No newline at end of file From dfd169770724c12d224750193c7b908bdfc5e68f Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 19:04:34 +0800 Subject: [PATCH 035/107] feat: fix null pointer in `React` message --- .../java/pw/mihou/nexus/features/messages/ReactExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index 0f0e8f6c..3c840efa 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -13,7 +13,7 @@ import java.util.concurrent.CompletableFuture */ @JvmSynthetic fun MessageCreateEvent.R(react: React.() -> Unit): CompletableFuture { - val r = React(this.api, React.RenderMode.Interaction) + val r = React(this.api, React.RenderMode.Message) react(r) return r.messageBuilder!!.replyTo(message).send(channel) From d18991c94969a1127aa4d9ab2637bfc51ca982a7 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 19:07:17 +0800 Subject: [PATCH 036/107] feat: fix reactivity not working for messages --- .../java/pw/mihou/nexus/features/messages/ReactExtensions.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index 3c840efa..19c24e3c 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -16,5 +16,8 @@ fun MessageCreateEvent.R(react: React.() -> Unit): CompletableFuture { val r = React(this.api, React.RenderMode.Message) react(r) - return r.messageBuilder!!.replyTo(message).send(channel) + return r.messageBuilder!!.replyTo(message).send(channel).thenApply { + r.resultingMessage = it + return@thenApply it + } } \ No newline at end of file From 5a5079493af1302c510ab1cfdeefce24b0e69114 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 19:21:35 +0800 Subject: [PATCH 037/107] feat: move React out from `command` package React is no longer limited to commands, therefore, it is right to moveit out from commands package. --- .../nexus/features/command/core/NexusCommandEventCore.kt | 2 +- .../nexus/features/command/core/NexusMiddlewareEventCore.kt | 2 +- .../nexus/features/command/facade/NexusCommandEvent.kt | 2 +- .../mihou/nexus/features/commons/NexusInteractionEvent.kt | 2 +- .../mihou/nexus/features/contexts/NexusContextMenuEvent.kt | 2 +- .../pw/mihou/nexus/features/messages/ReactExtensions.kt | 2 +- .../pw/mihou/nexus/features/{command => }/react/React.kt | 2 +- .../nexus/features/{command => }/react/elements/Button.kt | 4 ++-- .../nexus/features/{command => }/react/elements/Embed.kt | 6 +++--- .../nexus/features/{command => }/react/elements/Text.kt | 6 +++--- .../features/{command => }/react/elements/UrlButton.kt | 4 ++-- .../nexus/features/{command => }/react/styles/TextStyles.kt | 4 ++-- .../nexus/features/{command => }/react/styles/TimeFormat.kt | 2 +- src/test/java/commands/ReactiveTest.kt | 4 ++-- 14 files changed, 22 insertions(+), 22 deletions(-) rename src/main/java/pw/mihou/nexus/features/{command => }/react/React.kt (99%) rename src/main/java/pw/mihou/nexus/features/{command => }/react/elements/Button.kt (91%) rename src/main/java/pw/mihou/nexus/features/{command => }/react/elements/Embed.kt (96%) rename src/main/java/pw/mihou/nexus/features/{command => }/react/elements/Text.kt (64%) rename src/main/java/pw/mihou/nexus/features/{command => }/react/elements/UrlButton.kt (77%) rename src/main/java/pw/mihou/nexus/features/{command => }/react/styles/TextStyles.kt (96%) rename src/main/java/pw/mihou/nexus/features/{command => }/react/styles/TimeFormat.kt (81%) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index a5b10478..ac131c8d 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -8,7 +8,7 @@ import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.features.command.facade.NexusCommand import pw.mihou.nexus.features.command.facade.NexusCommandEvent -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.commons.Deferrable import pw.mihou.nexus.features.messages.NexusMessage diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt index 0cd7b225..2ec96b38 100644 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt @@ -6,7 +6,7 @@ import pw.mihou.nexus.features.command.facade.NexusCommand import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent import pw.mihou.nexus.features.command.interceptors.core.NexusMiddlewareGateCore -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.messages.NexusMessage import java.util.concurrent.CompletableFuture diff --git a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt index c073921c..37ae1073 100755 --- a/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/command/facade/NexusCommandEvent.kt @@ -10,7 +10,7 @@ import pw.mihou.nexus.Nexus import pw.mihou.nexus.Nexus.sharding import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore.execute import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptorCore.middlewares -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.commons.NexusInteractionEvent import pw.mihou.nexus.features.messages.NexusMessage diff --git a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt index 6805173a..0d2a1188 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt @@ -12,7 +12,7 @@ import org.javacord.api.interaction.ApplicationCommandInteraction import org.javacord.api.interaction.Interaction import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.messages.NexusMessage import java.util.* diff --git a/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt b/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt index 81e26948..64040e24 100644 --- a/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt @@ -6,7 +6,7 @@ import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater import org.javacord.api.util.logging.ExceptionLogger import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.commons.Deferrable import pw.mihou.nexus.features.commons.NexusInteractionEvent diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index 19c24e3c..a7e2d0b9 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -2,7 +2,7 @@ package pw.mihou.nexus.features.messages import org.javacord.api.entity.message.Message import org.javacord.api.event.message.MessageCreateEvent -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React import java.util.concurrent.CompletableFuture /** diff --git a/src/main/java/pw/mihou/nexus/features/command/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt similarity index 99% rename from src/main/java/pw/mihou/nexus/features/command/react/React.kt rename to src/main/java/pw/mihou/nexus/features/react/React.kt index 3842a00f..267ddce1 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -1,4 +1,4 @@ -package pw.mihou.nexus.features.command.react +package pw.mihou.nexus.features.react import org.javacord.api.DiscordApi import org.javacord.api.entity.message.Message diff --git a/src/main/java/pw/mihou/nexus/features/command/react/elements/Button.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt similarity index 91% rename from src/main/java/pw/mihou/nexus/features/command/react/elements/Button.kt rename to src/main/java/pw/mihou/nexus/features/react/elements/Button.kt index 2776ab8c..e0510d54 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/elements/Button.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt @@ -1,11 +1,11 @@ -package pw.mihou.nexus.features.command.react.elements +package pw.mihou.nexus.features.react.elements import org.javacord.api.entity.message.component.ButtonBuilder import org.javacord.api.entity.message.component.ButtonStyle import org.javacord.api.event.interaction.ButtonClickEvent import org.javacord.api.listener.interaction.ButtonClickListener import pw.mihou.nexus.core.assignment.NexusUuidAssigner -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React fun React.Component.Button( style: ButtonStyle = ButtonStyle.PRIMARY, diff --git a/src/main/java/pw/mihou/nexus/features/command/react/elements/Embed.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt similarity index 96% rename from src/main/java/pw/mihou/nexus/features/command/react/elements/Embed.kt rename to src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt index 11a2f9ee..98eabea0 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/elements/Embed.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt @@ -1,11 +1,11 @@ -package pw.mihou.nexus.features.command.react.elements +package pw.mihou.nexus.features.react.elements import org.javacord.api.entity.Icon import org.javacord.api.entity.message.MessageAuthor import org.javacord.api.entity.message.embed.EmbedBuilder import org.javacord.api.entity.user.User -import pw.mihou.nexus.features.command.react.React -import pw.mihou.nexus.features.command.react.styles.TextStyles +import pw.mihou.nexus.features.react.React +import pw.mihou.nexus.features.react.styles.TextStyles import java.awt.Color import java.awt.image.BufferedImage import java.io.File diff --git a/src/main/java/pw/mihou/nexus/features/command/react/elements/Text.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt similarity index 64% rename from src/main/java/pw/mihou/nexus/features/command/react/elements/Text.kt rename to src/main/java/pw/mihou/nexus/features/react/elements/Text.kt index f96c357b..ba2dce9c 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/elements/Text.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt @@ -1,7 +1,7 @@ -package pw.mihou.nexus.features.command.react.elements +package pw.mihou.nexus.features.react.elements -import pw.mihou.nexus.features.command.react.React -import pw.mihou.nexus.features.command.react.styles.TextStyles +import pw.mihou.nexus.features.react.React +import pw.mihou.nexus.features.react.styles.TextStyles fun React.Component.Text(text: Text.() -> Unit) { val element = Text() diff --git a/src/main/java/pw/mihou/nexus/features/command/react/elements/UrlButton.kt b/src/main/java/pw/mihou/nexus/features/react/elements/UrlButton.kt similarity index 77% rename from src/main/java/pw/mihou/nexus/features/command/react/elements/UrlButton.kt rename to src/main/java/pw/mihou/nexus/features/react/elements/UrlButton.kt index ffb6af06..922fcb86 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/elements/UrlButton.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/UrlButton.kt @@ -1,8 +1,8 @@ -package pw.mihou.nexus.features.command.react.elements +package pw.mihou.nexus.features.react.elements import org.javacord.api.entity.message.component.ButtonBuilder import org.javacord.api.entity.message.component.ButtonStyle -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React fun React.Component.UrlButton(label: String, url: String, emoji: String? = null) { val button = ButtonBuilder() diff --git a/src/main/java/pw/mihou/nexus/features/command/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt similarity index 96% rename from src/main/java/pw/mihou/nexus/features/command/react/styles/TextStyles.kt rename to src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt index ff46420a..b1897705 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/styles/TextStyles.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt @@ -1,6 +1,6 @@ -package pw.mihou.nexus.features.command.react.styles +package pw.mihou.nexus.features.react.styles -import pw.mihou.nexus.features.command.react.React +import pw.mihou.nexus.features.react.React import java.time.Instant interface TextStyles { diff --git a/src/main/java/pw/mihou/nexus/features/command/react/styles/TimeFormat.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TimeFormat.kt similarity index 81% rename from src/main/java/pw/mihou/nexus/features/command/react/styles/TimeFormat.kt rename to src/main/java/pw/mihou/nexus/features/react/styles/TimeFormat.kt index d08425f7..ab55d7cb 100644 --- a/src/main/java/pw/mihou/nexus/features/command/react/styles/TimeFormat.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TimeFormat.kt @@ -1,4 +1,4 @@ -package pw.mihou.nexus.features.command.react.styles +package pw.mihou.nexus.features.react.styles enum class TimeFormat(val suffix: String) { SHORT_TIME("t"), diff --git a/src/test/java/commands/ReactiveTest.kt b/src/test/java/commands/ReactiveTest.kt index 4d4490b2..c0db73ef 100644 --- a/src/test/java/commands/ReactiveTest.kt +++ b/src/test/java/commands/ReactiveTest.kt @@ -2,8 +2,8 @@ package commands import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.command.facade.NexusHandler -import pw.mihou.nexus.features.command.react.elements.Button -import pw.mihou.nexus.features.command.react.elements.Embed +import pw.mihou.nexus.features.react.elements.Button +import pw.mihou.nexus.features.react.elements.Embed class ReactiveTest: NexusHandler { val name = "react" From 3012a74ff0ba0be238f3a1e5caaee1dff07aa06d Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 19:22:17 +0800 Subject: [PATCH 038/107] feat: ignore name shadowing in Deferrable --- src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt index 4e08d059..fa8a215c 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt @@ -42,7 +42,7 @@ object Deferrable { if (!deferredTaskRan.get() && task != null) { task.cancel(false) } - val updater = updater.get() + @Suppress("NAME_SHADOWING") val updater = updater.get() if (updater == null) { val responder = event.respondNow() if (ephemeral) { From a9f04a36c411d9140d78d92ce90c1884ce622653 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 19:22:42 +0800 Subject: [PATCH 039/107] feat: fix emoji not being applied with buttons --- .../java/pw/mihou/nexus/features/react/elements/UrlButton.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/UrlButton.kt b/src/main/java/pw/mihou/nexus/features/react/elements/UrlButton.kt index 922fcb86..07f34564 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/UrlButton.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/UrlButton.kt @@ -10,5 +10,9 @@ fun React.Component.UrlButton(label: String, url: String, emoji: String? = null) button.setLabel(label) button.setUrl(url) + if (emoji != null) { + button.setEmoji(emoji) + } + components += button.build() } \ No newline at end of file From 8a045020734475133bd02bfd29e5c6a3d16b32ed Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 19:46:25 +0800 Subject: [PATCH 040/107] feat: add select menu support --- .../pw/mihou/nexus/features/react/React.kt | 33 ++++++++++-- .../nexus/features/react/elements/Button.kt | 2 +- .../nexus/features/react/elements/Embed.kt | 5 +- .../features/react/elements/SelectMenu.kt | 53 +++++++++++++++++++ src/test/java/commands/ReactiveTest.kt | 13 +++++ 5 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 267ddce1..33437e9b 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -14,6 +14,7 @@ import pw.mihou.nexus.core.assignment.NexusUuidAssigner import pw.mihou.nexus.features.messages.NexusMessage import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock +import javax.swing.Action import kotlin.reflect.KProperty typealias Subscription = (oldValue: T, newValue: T) -> Unit @@ -176,6 +177,31 @@ class React internal constructor(private val api: DiscordApi, private val render } } + private fun chunkComponents(): List { + val actionRows = mutableListOf() + var lowLevelComponents = mutableListOf() + + for ((index, component) in components.withIndex()) { + if (component.isSelectMenu) { + actionRows += ActionRow.of(component) + } else { + if (lowLevelComponents.size >= 3) { + actionRows += ActionRow.of(lowLevelComponents) + lowLevelComponents = mutableListOf() + } + + lowLevelComponents += component + } + + if (index == components.size && lowLevelComponents.size <= 3) { + actionRows += ActionRow.of(lowLevelComponents) + lowLevelComponents = mutableListOf() + } + } + + return actionRows + } + fun render(api: DiscordApi): Pair { return attachListeners(api) to NexusMessage.with { this.removeAllEmbeds() @@ -184,7 +210,8 @@ class React internal constructor(private val api: DiscordApi, private val render if (contents != null) { this.setContent(contents) } - components.chunked(3).map { ActionRow.of(it) }.forEach { this.addComponents(it) } + + chunkComponents().forEach { this.addComponents(it) } } } @@ -198,7 +225,7 @@ class React internal constructor(private val api: DiscordApi, private val render if (contents != null) { this.setContent(contents) } - components.chunked(3).map { ActionRow.of(it) }.forEach { this.addComponents(it) } + chunkComponents().forEach { this.addComponents(it) } } return attachListeners(api) } @@ -211,7 +238,7 @@ class React internal constructor(private val api: DiscordApi, private val render if (contents != null) { this.setContent(contents) } - components.chunked(3).map { ActionRow.of(it) }.forEach { this.addComponents(it) } + chunkComponents().forEach { this.addComponents(it) } } return attachListeners(api) } diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt index e0510d54..aa6f41eb 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt @@ -13,7 +13,7 @@ fun React.Component.Button( customId: String? = null, emoji: String? = null, disabled: Boolean = false, - onClick: ((event: ButtonClickEvent) -> Unit)? = {} + onClick: ((event: ButtonClickEvent) -> Unit)? = null ) { val button = ButtonBuilder() button.setStyle(style) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt index 98eabea0..8d9a63fa 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt @@ -16,12 +16,11 @@ fun React.Component.Embed(embed: Embed.() -> Unit) { val element = Embed() embed(element) - embeds.add(element.view()) + embeds.add(element.embed) } class Embed: TextStyles { - private val embed = EmbedBuilder() - fun view() = embed + internal val embed = EmbedBuilder() fun Title(text: String) { embed.setTitle(text) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt b/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt new file mode 100644 index 00000000..59ed48ab --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt @@ -0,0 +1,53 @@ +package pw.mihou.nexus.features.react.elements + +import org.javacord.api.entity.channel.ChannelType +import org.javacord.api.entity.message.component.ComponentType +import org.javacord.api.entity.message.component.SelectMenuBuilder +import org.javacord.api.entity.message.component.SelectMenuOption +import org.javacord.api.event.interaction.SelectMenuChooseEvent +import org.javacord.api.listener.interaction.SelectMenuChooseListener +import pw.mihou.nexus.core.assignment.NexusUuidAssigner +import pw.mihou.nexus.features.react.React + +fun React.Component.SelectMenu( + componentType: ComponentType, + customId: String = NexusUuidAssigner.request(), + minimumValues: Int = 1, + maximumValues: Int = 1, + disabled: Boolean = false, + onSelect: ((event: SelectMenuChooseEvent) -> Unit)? = null, + selectMenu: SelectMenu.() -> Unit +) { + val element = SelectMenu( + SelectMenuBuilder(componentType, customId) + .setDisabled(disabled) + .setMaximumValues(maximumValues) + .setMinimumValues(minimumValues)) + selectMenu(element) + + if (onSelect != null) { + listeners += SelectMenuChooseListener { + if (it.selectMenuInteraction.customId != customId) { + return@SelectMenuChooseListener + } + + onSelect(it) + } + } + + components += element.selectMenu.build() +} + +class SelectMenu(internal val selectMenu: SelectMenuBuilder) { + fun ChannelType(type: ChannelType) { + selectMenu.addChannelType(type) + } + + fun Option(option: SelectMenuOption) { + selectMenu.addOption(option) + } + + fun Placeholder(text: String) { + selectMenu.setPlaceholder(text) + } +} \ No newline at end of file diff --git a/src/test/java/commands/ReactiveTest.kt b/src/test/java/commands/ReactiveTest.kt index c0db73ef..4d9e96a8 100644 --- a/src/test/java/commands/ReactiveTest.kt +++ b/src/test/java/commands/ReactiveTest.kt @@ -1,9 +1,11 @@ package commands +import org.javacord.api.entity.message.component.ComponentType import pw.mihou.nexus.features.command.facade.NexusCommandEvent import pw.mihou.nexus.features.command.facade.NexusHandler import pw.mihou.nexus.features.react.elements.Button import pw.mihou.nexus.features.react.elements.Embed +import pw.mihou.nexus.features.react.elements.SelectMenu class ReactiveTest: NexusHandler { val name = "react" @@ -25,6 +27,17 @@ class ReactiveTest: NexusHandler { event.interaction.user.sendMessage("Hi") clicks += 1 } + SelectMenu( + componentType = ComponentType.SELECT_MENU_STRING, + onSelect = { + it.selectMenuInteraction.acknowledge() + it.selectMenuInteraction.selectedChannels.forEach { channel -> + println("Channel selected: ${channel.name}") + } + } + ) { + ChannelType(org.javacord.api.entity.channel.ChannelType.SERVER_TEXT_CHANNEL) + } } } } From 09c0d0408b8ae0821623c981c3b67548a5d06c3a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 19:52:55 +0800 Subject: [PATCH 041/107] feat: fix components not being rendered --- src/main/java/pw/mihou/nexus/features/react/React.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 33437e9b..8621b053 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -193,7 +193,7 @@ class React internal constructor(private val api: DiscordApi, private val render lowLevelComponents += component } - if (index == components.size && lowLevelComponents.size <= 3) { + if (index == (components.size - 1) && lowLevelComponents.size <= 3) { actionRows += ActionRow.of(lowLevelComponents) lowLevelComponents = mutableListOf() } From a8110d60b72185759fa675c5709072ff3e4353b9 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 20:11:04 +0800 Subject: [PATCH 042/107] feat: fix duplicate action row --- src/main/java/pw/mihou/nexus/features/react/React.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 8621b053..464b05bb 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -184,6 +184,7 @@ class React internal constructor(private val api: DiscordApi, private val render for ((index, component) in components.withIndex()) { if (component.isSelectMenu) { actionRows += ActionRow.of(component) + continue } else { if (lowLevelComponents.size >= 3) { actionRows += ActionRow.of(lowLevelComponents) From 186e58d66e3cd36b21f9841648d9d2f678c5d717 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 20:17:49 +0800 Subject: [PATCH 043/107] feat: fix action rows not being appended --- src/main/java/pw/mihou/nexus/features/react/React.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 464b05bb..880ec913 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -200,6 +200,10 @@ class React internal constructor(private val api: DiscordApi, private val render } } + if (lowLevelComponents.isNotEmpty() && lowLevelComponents.size <= 3) { + actionRows += ActionRow.of(lowLevelComponents) + } + return actionRows } From 810d5756973c10deb190b0b48755d5fe6695670a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 20:23:41 +0800 Subject: [PATCH 044/107] feat: fix text styles --- .../pw/mihou/nexus/features/react/styles/TextStyles.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt index b1897705..bcef1da1 100644 --- a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt @@ -20,22 +20,22 @@ interface TextStyles { if (underline) { prefix += "__" - suffix += "__" + suffix = "__$suffix" } if (italic && !bold) { prefix += "*" - suffix += "*" + suffix = "*$suffix" } if (strikethrough) { prefix += "~~" - suffix += "~~" + suffix = "~~$suffix" } if (spoiler) { prefix += "`" - suffix += "`" + suffix = "`$suffix" } return prefix to suffix From a0515a5630cc8bc05bb83bb2116220f9e62bbad4 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Mon, 16 Oct 2023 22:33:48 +0800 Subject: [PATCH 045/107] feat: reduce debounceMillis --- src/main/java/pw/mihou/nexus/features/react/React.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 880ec913..370b7b5c 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -14,7 +14,6 @@ import pw.mihou.nexus.core.assignment.NexusUuidAssigner import pw.mihou.nexus.features.messages.NexusMessage import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock -import javax.swing.Action import kotlin.reflect.KProperty typealias Subscription = (oldValue: T, newValue: T) -> Unit @@ -40,7 +39,7 @@ class React internal constructor(private val api: DiscordApi, private val render internal var renderSubscribers = mutableListOf() companion object { - var debounceMillis = 250L + var debounceMillis = 25L } internal enum class RenderMode { From d9d8f29da1aec9d34ab28e188c81128cb626f794 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 10:56:52 +0800 Subject: [PATCH 046/107] feat: add preset text styles --- .../java/pw/mihou/nexus/features/react/styles/TextStyles.kt | 5 +++++ src/test/java/commands/ReactiveTest.kt | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt index bcef1da1..5ce52882 100644 --- a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt @@ -47,6 +47,11 @@ interface TextStyles { return prefix + text + suffix } + fun bold(text: String) = "**$text**" + fun italic(text: String) = "*$text*" + fun mark(text: String) = "`$text`" + fun del(text: String) = "~~$text~~" + fun br(): String = "\n" fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, diff --git a/src/test/java/commands/ReactiveTest.kt b/src/test/java/commands/ReactiveTest.kt index 4d9e96a8..0c33aee5 100644 --- a/src/test/java/commands/ReactiveTest.kt +++ b/src/test/java/commands/ReactiveTest.kt @@ -20,6 +20,8 @@ class ReactiveTest: NexusHandler { p("Hello World", bold = true, underline = true), br(), p("This is a little experiment over how this would look DX-wise. Discord message components that will also support states."), + br(), + bold("Rendered with Nexus."), link("Test @ Nexus", "https://github.com/ShindouMihou/Nexus") ) } From 7ba442816f00d5a4e95a4e369572d13bed9bc025 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 10:58:48 +0800 Subject: [PATCH 047/107] feat: add `SpacedBody` to reduce the amount of `br` used --- src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt | 3 +++ src/main/java/pw/mihou/nexus/features/react/elements/Text.kt | 3 +++ src/test/java/commands/ReactiveTest.kt | 4 +--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt index 8d9a63fa..391ee54e 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt @@ -29,6 +29,9 @@ class Embed: TextStyles { fun Body(vararg nodes: String) { embed.setDescription(nodes.joinToString("")) } + fun SpacedBody(vararg nodes: String) { + embed.setDescription(nodes.joinToString("\n")) + } fun Field(name: String, inline: Boolean = false, vararg nodes: String) { embed.addField(name, nodes.joinToString(""), inline) } diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt index ba2dce9c..b2d9f67d 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt @@ -16,4 +16,7 @@ class Text: TextStyles { fun Body(vararg nodes: String) { content = nodes.joinToString("") } + fun SpacedBody(vararg nodes: String) { + content = nodes.joinToString("\n") + } } \ No newline at end of file diff --git a/src/test/java/commands/ReactiveTest.kt b/src/test/java/commands/ReactiveTest.kt index 0c33aee5..cb272bfa 100644 --- a/src/test/java/commands/ReactiveTest.kt +++ b/src/test/java/commands/ReactiveTest.kt @@ -16,11 +16,9 @@ class ReactiveTest: NexusHandler { render { Embed { Title("R.Embeds") - Body( + SpacedBody( p("Hello World", bold = true, underline = true), - br(), p("This is a little experiment over how this would look DX-wise. Discord message components that will also support states."), - br(), bold("Rendered with Nexus."), link("Test @ Nexus", "https://github.com/ShindouMihou/Nexus") ) From 915608e60837164d11bed00d96e93e834dcf7137 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 21:57:59 +0800 Subject: [PATCH 048/107] feat: document the methods inside `React` --- .../pw/mihou/nexus/features/react/React.kt | 117 +++++++++++++++++- 1 file changed, 112 insertions(+), 5 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 370b7b5c..ff38ec4a 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -20,9 +20,10 @@ typealias Subscription = (oldValue: T, newValue: T) -> Unit typealias Unsubscribe = () -> Unit typealias RenderSubscription = () -> Unit +typealias ReactComponent = React.Component.() -> Unit class React internal constructor(private val api: DiscordApi, private val renderMode: RenderMode) { - internal var rendered: Boolean = false + private var rendered: Boolean = false internal var message: NexusMessage? = null internal var messageBuilder: MessageBuilder? = null @@ -35,10 +36,19 @@ class React internal constructor(private val api: DiscordApi, private val render internal var resultingMessage: Message? = null - internal var firstRenderSubscribers = mutableListOf() - internal var renderSubscribers = mutableListOf() + private var firstRenderSubscribers = mutableListOf() + private var renderSubscribers = mutableListOf() companion object { + /** + * Defines how long we should wait before proceeding to re-render the component, this is intended to ensure + * that all other states being changed in that period is applied as well, preventing multiple unnecessary re-renders + * which can be costly as we send HTTP requests to Discord. + * + * As a default, we recommend within 25ms to 250ms depending on how long the betweens of your state changes are, + * you can go even lower as long as the states are being changed immediately, otherwise, keeping it as default + * is recommended. + */ var debounceMillis = 25L } @@ -47,15 +57,30 @@ class React internal constructor(private val api: DiscordApi, private val render Message } + /** + * Subscribes a task to be ran whenever the component is being rendered, this happens on the initial render + * and re-renders but takes a lower priority than the ones in [onInitialRender]. + * @param subscription the subscription to execute on render. + */ fun onRender(subscription: RenderSubscription) { renderSubscribers.add(subscription) } + /** + * Subscribes a task to be ran whenever the component is being rendered. This happens first before the actual + * component being rendered, therefore, you can use this to load data before the component is actually rendered. + * @param subscription the subscription to execute on first render. + */ fun onInitialRender(subscription: RenderSubscription) { firstRenderSubscribers.add(subscription) } - fun render(component: Component.() -> Unit) { + /** + * Renders the given component, this will also be used to re-render the component onwards. Note that using two + * renders will result in the last executed render being used. + * @param component the component to render. + */ + fun render(component: ReactComponent) { val element = apply(component) when(renderMode) { @@ -89,11 +114,32 @@ class React internal constructor(private val api: DiscordApi, private val render return element } + /** + * Creates a [Writable] that can react to changes of the value, allowing you to re-render the message + * with the new states. Internally, this simply creates a [Writable] then adds a subscriber to re-render + * the message whenever the state changes (debounced by [React.debounceMillis] milliseconds). + * + * We recommend using [Writable]'s constructor and then using [React.expand] to add the subscriber whenever you need + * to create a [Writable] outside of the [React] scope. (View the source code of this method to see how it looks). + * + * To pass a [Writable], we recommend creating another variable that has the [Writable] itself as the value and another + * one that uses `by` and then passing the [Writable] instead. (Refer to the wiki for this, as function parameters are not mutable + * and delegated variables pass their [Writable.getValue] result, so changes cannot be listened). + * + * @param value the initial value. + * @return a [Writable] with a [Subscription] that will re-render the [ReactComponent] when the state changes. + */ fun writable(value: T): Writable { val element = Writable(value) return expand(element) } + /** + * Adds a [Subscription] that enables the [ReactComponent] to be re-rendered whenever the value of the [Writable] + * changes, this is what [writable] uses internally to react to changes. + * @param writable the writable to subscribe. + * @return the [Writable] with the re-render subscription attached. + */ fun expand(writable: Writable): Writable { writable.subscribe { _, _ -> if (!mutex.tryLock()) return@subscribe @@ -117,21 +163,62 @@ class React internal constructor(private val api: DiscordApi, private val render return writable } + /** + * Writable are the equivalent to state in React.js, or [writable] in Svelte (otherwise known as `$state` in Svelte Runes), + * these are simply properties that will execute subscribed tasks whenever the property changes, enabling reactivity. + * [React] uses this to support re-rendering the [ReactComponent] whenever a state changes, allowing developers to write + * incredibly reactive yet beautifully simple code that are similar to Svelte. + * + * We recommend using the constructor method to create a [Writable] for use cases outside of [React] scope, otherwise + * use the [writable] method to create a [Writable] inside of [React] scope which will have a re-render subscription. + * (Read the wiki for more information). + * + */ class Writable(value: T) { private val subscribers: MutableList> = mutableListOf() private val _value: AtomicReference = AtomicReference(value) + + /** + * Gets the value of this [Writable]. This is intended to be used for delegation. You may be looking for + * [get] instead which allows you to directly get the value. + */ operator fun getValue(thisRef: Any?, property: KProperty<*>): T { return _value.get() } + + /** + * Sets the value of this [Writable]. This is intended to be used for delegation. You may be looking for + * [set] or [getAndUpdate] instead which allows you to manipulate the [Writable]'s value. + */ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { set(value) } + + /** + * Manipulates the value of the [Writable]. + * This will run all the subscriptions asynchronously after the value has been changed, ensuring that + * all subscriptions are executed without interfering or delaying one another. + * + * When performing things such as increment, decrements, or anything that requires the current value, we + * recommend using [getAndUpdate] instead which will allow you to atomically update the value. + * + * @param value the new value of the [Writable]. + */ fun set(value: T) { val oldValue = _value.get() _value.set(value) subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } } + + /** + * Atomically updates the value of the [Writable]. This is recommended to use when manipulating the value of, say + * a numerical value, for instance, incrementing, decrementing, multiplying, etc. as this is performed atomically + * which stops a lot of thread-unsafety. + * + * Similar to [set], this executes all the subscriptions asynchronously. + * @param updater the updater to update the value of the [Writable]. + */ fun getAndUpdate(updater: (T) -> T) { val oldValue = _value.get() _value.getAndUpdate(updater) @@ -139,11 +226,27 @@ class React internal constructor(private val api: DiscordApi, private val render val value = _value.get() subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } } + + /** + * Gets the current value of the [Writable]. If you need to listen to changes to the value, + * use the [subscribe] method instead to subscribe to changes. + * + * @return the value of the [Writable]. + */ fun get(): T = _value.get() + + /** + * Subscribes to changes to the value of the [Writable]. This is ran asynchronously after the value has + * been changed. + * + * @param subscription the task to execute upon a change to the value is detected. + * @return an [Unsubscribe] method to unsubscribe the [Subscription]. + */ fun subscribe(subscription: Subscription): Unsubscribe { subscribers.add(subscription) return { subscribers.remove(subscription) } } + override fun toString(): String { return _value.get().toString() } @@ -160,7 +263,11 @@ class React internal constructor(private val api: DiscordApi, private val render } } - class Component { + /** + * An internal class of [React]. You do not need to touch this at all, and it is not recommended to even create + * this by yourself as it will do nothing. + */ + class Component internal constructor() { internal var embeds: MutableList = mutableListOf() internal var contents: String? = null internal var components: MutableList = mutableListOf() From 180b5207df9c8649352b2fee3bca2f2eb5000509 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 22:15:00 +0800 Subject: [PATCH 049/107] feat: document the different text styles --- .../nexus/features/react/styles/TextStyles.kt | 110 +++++++++++++++++- .../nexus/features/react/styles/TimeFormat.kt | 27 +++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt index 5ce52882..83a1787e 100644 --- a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt @@ -41,37 +41,116 @@ interface TextStyles { return prefix to suffix } + /** + * Renders a regular text content, unlike methods such as [bold], [italic], you can write this as unstyled + * or stack different stylings over the text through parameters. + * + * @param text the text to render. + * @param bold whether to make the text bold. + * @param underline whether to underline the text. + * @param italic whether to make the text italic. + * @param strikethrough whether to add a strikethrough to the text. + * @param spoiler whether to hide the text behind a spoiler. + */ fun p(text: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, strikethrough: Boolean = false, spoiler: Boolean = false): String { val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) return prefix + text + suffix } + /** + * Renders a bold text. We recommend using [p] when you want to stack different + * styling over one another. + * + * @param text the text to render. + */ fun bold(text: String) = "**$text**" + + /** + * Renders an italic text. We recommend using [p] when you want to stack different + * styling over one another. + * + * @param text the text to render. + */ fun italic(text: String) = "*$text*" + + /** + * Renders a spoiled (hidden) text. We recommend using [p] when you want to stack different + * styling over one another. + * + * @param text the text to render. + */ fun mark(text: String) = "`$text`" + + /** + * Renders a text with a strikethrough. We recommend using [p] when you want to stack different + * styling over one another. + * + * @param text the text to render. + */ fun del(text: String) = "~~$text~~" + /** + * Renders a next line, we recommend using `StyledBody` instead when next text has a next line. + */ fun br(): String = "\n" + /** + * Renders a link text that allows users to click and be redirected to the [href]. + * + * @param text the text to render. + * @param bold whether to make the text bold. + * @param underline whether to underline the text. + * @param italic whether to make the text italic. + * @param strikethrough whether to add a strikethrough to the text. + * @param spoiler whether to hide the text behind a spoiler. + */ fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, strikethrough: Boolean = false, spoiler: Boolean = false): String { val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) return "$prefix[$text]($href)$suffix" } + /** + * Renders a main title heading text. + * @param text the text to render. + */ fun h1(text: String): String { return "# $text" } + + /** + * Renders a subsection heading text. + * @param text the text to render. + */ fun h2(text: String): String { return "## $text" } + + /** + * Renders a sub-subsection heading text. + * @param text the text to render. + */ fun h3(text: String): String { return "### $text" } + + /** + * Renders an unordered list with the given nodes. This cannot be used to create nested + * unordered lists. + * + * @param nodes the nodes to include in the unordered list. + */ fun ul(vararg nodes: String): String { return nodes.joinToString("\n") { "* $it" } } + + /** + * Renders an ordered list with the given nodes. This cannot be used to create nested + * ordered lists. + * + * @param nodes the nodes to include in the unordered list. + */ fun ol(vararg nodes: String): String { var text = "" for ((index, node) in nodes.withIndex()) { @@ -79,12 +158,41 @@ interface TextStyles { } return text } + + /** + * Renders the texts inside a codeblock. + * + * @param language the language to use to highlight the nodes. + * @param nodes the nodes to include in the codeblock. + */ fun codeblock(language: String, vararg nodes: String): String { return "```$language\n${nodes.joinToString("")}\n```" } + + /** + * Renders the texts under a blockquote. + * + * @param nodes the texts to write inside a blockquote. + */ fun blockquote(vararg nodes: String): String { return nodes.joinToString("\n") { "> $it"} } + + /** + * Renders the [Instant] as a timestamp in Discord. By default, this uses the relative time format, but you + * can customize it to show different outputs. + * + * **Timestamp Formats**: + * 1. **Relative** ([TimeFormat.RELATIVE]): shows a relative time (e.g. 30 seconds ago) + * 2. **Short Time** ([TimeFormat.SHORT_TIME]): shows the time without the seconds (e.g. 10:07 PM) + * 3. **Long Time** ([TimeFormat.LONG_TIME]): shows the time with the seconds (e.g. 10:07:00 PM) + * 4. **Short Date** ([TimeFormat.SHORT_DATE]): shows the date with the month as a number and the year shortened (e.g. 10/17/23) + * 5. **Long Date** ([TimeFormat.LONG_DATE]): shows the date with the full month name and the full year (e.g. October 17, 2023) + * 6. **Long Date With Short Time** ([TimeFormat.LONG_DATE_WITH_SHORT_TIME]): shows the date with a short time (e.g. October 17, 2023 at 10:07 PM) + * 7. **Long Date With Day of Week and Short Time** ([TimeFormat.LONG_DATE_WITH_DAY_OF_WEEK_AND_SHORT_TIME]): shows the date with the short time and the day of the week (e.g. Tuesday, October 17, 2023 at 10:07 PM) + * + * @param instant the [Instant] to render as a timestamp. + * @param format the format of the timestamp. + */ fun time(instant: Instant, format: TimeFormat = TimeFormat.RELATIVE) = "" - fun time(instant: React.Writable, format: TimeFormat = TimeFormat.RELATIVE) = time(instant.get(), format) } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/styles/TimeFormat.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TimeFormat.kt index ab55d7cb..8414be02 100644 --- a/src/main/java/pw/mihou/nexus/features/react/styles/TimeFormat.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TimeFormat.kt @@ -1,11 +1,38 @@ package pw.mihou.nexus.features.react.styles enum class TimeFormat(val suffix: String) { + /** + * Shows the time without the seconds (e.g. 10:07 PM) + */ SHORT_TIME("t"), + + /** + * Shows the time with the seconds (e.g. 10:07:00 PM) + */ LONG_TIME("T"), + + /** + * Shows the date with the month as a number and the year shortened (e.g. 10/17/23) + */ SHORT_DATE("d"), + + /** + * Shows the date with the full month name and the full year (e.g. October 17, 2023) + */ LONG_DATE("D"), + + /** + * Shows the date with a short time (e.g. October 17, 2023 at 10:07 PM). + */ LONG_DATE_WITH_SHORT_TIME("f"), + + /** + * Shows the date with the short time and the day of the week (e.g. Tuesday, October 17, 2023 at 10:07 PM) + */ LONG_DATE_WITH_DAY_OF_WEEK_AND_SHORT_TIME("F"), + + /** + * Shows a relative time (e.g. 30 seconds ago). + */ RELATIVE("R"); } \ No newline at end of file From b0330a9dfe3ac9fd0e9a942d514fd177fc17fec0 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 22:18:31 +0800 Subject: [PATCH 050/107] feat: add additional documentation to the React class itself --- src/main/java/pw/mihou/nexus/features/react/React.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index ff38ec4a..dc572a7a 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -22,6 +22,11 @@ typealias Unsubscribe = () -> Unit typealias RenderSubscription = () -> Unit typealias ReactComponent = React.Component.() -> Unit +/** + * [React] is the React-Svelte inspired method of rendering (or sending) messages as response to various scenarios such + * as message commands, slash commands, context menus and different kind of magic. We recommend using the available `event.R` method + * instead as it is mostly designed to enable this to work for your situation. + */ class React internal constructor(private val api: DiscordApi, private val renderMode: RenderMode) { private var rendered: Boolean = false From f997db2332a992f2b5e11dbf094d4dc204799660 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 22:19:51 +0800 Subject: [PATCH 051/107] feat: use `CertainMessageEvent` over `MessageCreateEvent` to ensure support over all message events --- .../java/pw/mihou/nexus/features/messages/ReactExtensions.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index a7e2d0b9..75b0c715 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -1,7 +1,7 @@ package pw.mihou.nexus.features.messages import org.javacord.api.entity.message.Message -import org.javacord.api.event.message.MessageCreateEvent +import org.javacord.api.event.message.CertainMessageEvent import pw.mihou.nexus.features.react.React import java.util.concurrent.CompletableFuture @@ -9,10 +9,11 @@ import java.util.concurrent.CompletableFuture * An experimental feature to use the new Nexus.R rendering mechanism to render Discord messages * with a syntax similar to a template engine that sports states (writable) that can easily update message * upon state changes. + * * @param react the entire procedure over how rendering the response works. */ @JvmSynthetic -fun MessageCreateEvent.R(react: React.() -> Unit): CompletableFuture { +fun CertainMessageEvent.R(react: React.() -> Unit): CompletableFuture { val r = React(this.api, React.RenderMode.Message) react(r) From e4547941495fbafd28ffb97e7619972bab73e29b Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 22:37:14 +0800 Subject: [PATCH 052/107] feat: use `InteractionBase` to expand support for all interactions even buttons for `Nexus.R` (and also `autoDefer`). --- .../command/core/NexusCommandEventCore.kt | 2 +- .../nexus/features/commons/Deferrable.kt | 51 +++++++++++++++++-- .../features/commons/NexusInteractionEvent.kt | 17 +++---- .../contexts/NexusContextMenuEvent.kt | 2 +- .../pw/mihou/nexus/features/react/React.kt | 5 +- 5 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt index ac131c8d..882cfeb3 100755 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusCommandEventCore.kt @@ -25,7 +25,7 @@ class NexusCommandEventCore(override val event: SlashCommandCreateEvent, overrid override fun store(): MutableMap = store override fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture = - Deferrable.autoDefer(this, updater, ephemeral, response) + Deferrable.autoDefer(event.slashCommandInteraction, updater, ephemeral, response) override fun respondLater(): CompletableFuture { return updater.updateAndGet { interaction.respondLater() }!! diff --git a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt index fa8a215c..d99b41d4 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt @@ -2,12 +2,14 @@ package pw.mihou.nexus.features.commons import org.javacord.api.entity.message.MessageFlag import org.javacord.api.interaction.Interaction +import org.javacord.api.interaction.InteractionBase import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater import org.javacord.api.util.logging.ExceptionLogger import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.messages.NexusMessage +import pw.mihou.nexus.features.react.React import java.time.Instant import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean @@ -15,8 +17,8 @@ import java.util.concurrent.atomic.AtomicReference import java.util.function.Function object Deferrable { - internal fun autoDefer( - event: NexusInteractionEvent<*, *>, + internal fun autoDefer( + interaction: Interaction, updater: AtomicReference?>, ephemeral: Boolean, response: Function @@ -24,13 +26,13 @@ object Deferrable { var task: Cancellable? = null val deferredTaskRan = AtomicBoolean(false) if (updater.get() == null) { - val timeUntil = Instant.now().toEpochMilli() - event.interaction.creationTimestamp + val timeUntil = Instant.now().toEpochMilli() - interaction.creationTimestamp .minusMillis(Nexus.configuration.global.autoDeferAfterMilliseconds) .toEpochMilli() task = Nexus.launch.scheduler.launch(timeUntil) { if (updater.get() == null) { - event.respondLaterEphemerallyIf(ephemeral).exceptionally(ExceptionLogger.get()) + updater.updateAndGet { interaction.respondLater(ephemeral) }!!.exceptionally(ExceptionLogger.get()) } deferredTaskRan.set(true) } @@ -44,7 +46,7 @@ object Deferrable { } @Suppress("NAME_SHADOWING") val updater = updater.get() if (updater == null) { - val responder = event.respondNow() + val responder = interaction.createImmediateResponder() if (ephemeral) { responder.setFlags(MessageFlag.EPHEMERAL) } @@ -69,4 +71,43 @@ object Deferrable { } return future } +} + +/** + * Automatically answers either deferred or non-deferred based on circumstances, to configure the time that it should + * consider before deferring (this is based on time now - (interaction creation time - auto defer time)), you can + * modify [pw.mihou.nexus.configuration.modules.NexusGlobalConfiguration.autoDeferAfterMilliseconds]. + * + * For slash commands and context menus, we recommend using the Nexus event's methods instead which will enable better coordination + * with middlewares. + * + * @param ephemeral whether to respond ephemerally or not. + * @param response the response to send to Discord. + * @return the response from Discord. + */ +fun Interaction.autoDefer(ephemeral: Boolean, response: Function): CompletableFuture = + Deferrable.autoDefer(this, AtomicReference(null), ephemeral, response) + +/** + * An experimental feature to use the new Nexus.R rendering mechanism to render Discord messages + * with a syntax similar to a template engine that sports states (writables) that can easily update message + * upon state changes. + * + * This internally uses [autoDefer] to handle sending of the response, ensuring that we can handle long-running renders + * and others that may happen due to situations such as data fetching, etc. + * + * @param ephemeral whether to send the response as ephemeral or not. + * @param react the entire procedure over how rendering the response works. + */ +@JvmSynthetic +fun Interaction.R(ephemeral: Boolean, react: React.() -> Unit): CompletableFuture { + val r = React(this.api, React.RenderMode.Interaction) + return autoDefer(ephemeral) { + react(r) + + return@autoDefer r.message!! + }.thenApply { + r.resultingMessage = it.getOrRequestMessage().join() + return@thenApply it + } } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt index 0d2a1188..9dfb0445 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt @@ -200,19 +200,14 @@ interface NexusInteractionEvent Unit): CompletableFuture { - val r = React(this.api, React.RenderMode.Interaction) - return autoDefer(ephemeral) { - react(r) - - return@autoDefer r.message!! - }.thenApply { - r.resultingMessage = it.getOrRequestMessage().join() - return@thenApply it - } - } + fun R(ephemeral: Boolean = false, react: React.() -> Unit): CompletableFuture = + interaction.R(ephemeral, react) } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt b/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt index 64040e24..0e57b147 100644 --- a/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/contexts/NexusContextMenuEvent.kt @@ -23,5 +23,5 @@ class NexusContextMenuEvent { private var updater: AtomicReference?> = AtomicReference(null) override fun autoDefer(ephemeral: Boolean, response: Function): CompletableFuture = - Deferrable.autoDefer(this, updater, ephemeral, response) + Deferrable.autoDefer(event.interaction, updater, ephemeral, response) } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index dc572a7a..f3827e0a 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -24,8 +24,9 @@ typealias ReactComponent = React.Component.() -> Unit /** * [React] is the React-Svelte inspired method of rendering (or sending) messages as response to various scenarios such - * as message commands, slash commands, context menus and different kind of magic. We recommend using the available `event.R` method - * instead as it is mostly designed to enable this to work for your situation. + * as message commands, slash commands, context menus and different kind of magic. We recommend using the available + * `event.R` method instead as it is mostly designed to enable this to work for your situation, or instead use the + * available `interaction.R` method for interactions. */ class React internal constructor(private val api: DiscordApi, private val renderMode: RenderMode) { private var rendered: Boolean = false From f647e60ec591d0f23faeaf9d9d7263edb013fe23 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 22:38:37 +0800 Subject: [PATCH 053/107] feat: add support for `Messageable` --- .../features/messages/ReactExtensions.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index 75b0c715..60f903ca 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -1,6 +1,8 @@ package pw.mihou.nexus.features.messages +import org.javacord.api.DiscordApi import org.javacord.api.entity.message.Message +import org.javacord.api.entity.message.Messageable import org.javacord.api.event.message.CertainMessageEvent import pw.mihou.nexus.features.react.React import java.util.concurrent.CompletableFuture @@ -21,4 +23,22 @@ fun CertainMessageEvent.R(react: React.() -> Unit): CompletableFuture { r.resultingMessage = it return@thenApply it } +} + +/** + * An experimental feature to use the new Nexus.R rendering mechanism to render Discord messages + * with a syntax similar to a template engine that sports states (writable) that can easily update message + * upon state changes. + * + * @param react the entire procedure over how rendering the response works. + */ +@JvmSynthetic +fun Messageable.R(api: DiscordApi, react: React.() -> Unit): CompletableFuture { + val r = React(api, React.RenderMode.Message) + react(r) + + return r.messageBuilder!!.send(this).thenApply { + r.resultingMessage = it + return@thenApply it + } } \ No newline at end of file From e153ca967261e64889d0b9b6fb1c444a047ef2a7 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 17 Oct 2023 23:52:32 +0800 Subject: [PATCH 054/107] feat: add complete examples --- examples/nexus.r/ContextMenu.kt | 43 ++++++++++++ examples/nexus.r/Interaction.kt | 58 ++++++++++++++++ examples/nexus.r/MessageEvent.kt | 39 +++++++++++ examples/nexus.r/README.md | 30 +++++++++ examples/nexus.r/SlashCommand.kt | 39 +++++++++++ .../nexus.r/[1]_passing_state/PassingState.kt | 66 +++++++++++++++++++ examples/nexus.r/[1]_passing_state/README.md | 8 +++ examples/nexus.r/[2]_components/Component.kt | 60 +++++++++++++++++ examples/nexus.r/[2]_components/README.md | 15 +++++ 9 files changed, 358 insertions(+) create mode 100644 examples/nexus.r/ContextMenu.kt create mode 100644 examples/nexus.r/Interaction.kt create mode 100644 examples/nexus.r/MessageEvent.kt create mode 100644 examples/nexus.r/README.md create mode 100644 examples/nexus.r/SlashCommand.kt create mode 100644 examples/nexus.r/[1]_passing_state/PassingState.kt create mode 100644 examples/nexus.r/[1]_passing_state/README.md create mode 100644 examples/nexus.r/[2]_components/Component.kt create mode 100644 examples/nexus.r/[2]_components/README.md diff --git a/examples/nexus.r/ContextMenu.kt b/examples/nexus.r/ContextMenu.kt new file mode 100644 index 00000000..b219f967 --- /dev/null +++ b/examples/nexus.r/ContextMenu.kt @@ -0,0 +1,43 @@ +import org.javacord.api.event.interaction.MessageContextMenuCommandEvent +import org.javacord.api.interaction.MessageContextMenuInteraction +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.contexts.NexusContextMenuEvent +import pw.mihou.nexus.features.contexts.NexusMessageContextMenu +import pw.mihou.nexus.features.contexts.NexusUserContextMenu +import pw.mihou.nexus.features.messages.R +import pw.mihou.nexus.features.react.elements.Button +import pw.mihou.nexus.features.react.elements.Embed +import java.time.Instant + +/** + * Nexus.R supports basically everything, you can copy-paste the code from one event to another and it'll work + * the same regardless without much if any modification. In this example, we demonstrate how we can use Nexus.R + * to build amazingly simple yet reactive component. + * + * Our example will render a simple message with a button that increments the click count and re-render the + * message whenever a click is registered. + */ +object ContextMenu: NexusMessageContextMenu() { + val name = "react" + override fun onEvent(event: NexusContextMenuEvent) { + event.R { + var clicks by writable(0) + render { + Embed { + Title("Rendered with Nexus.R") + SpacedBody( + p("This message was rendered with Nexus.R."), + p("The button has been clicked ") + bold("$clicks times.") + ) + Color(java.awt.Color.YELLOW) + Timestamp(Instant.now()) + } + Button(label = "Click me!") { + it.buttonInteraction.acknowledge() + clicks += 1 + } + } + } + } +} \ No newline at end of file diff --git a/examples/nexus.r/Interaction.kt b/examples/nexus.r/Interaction.kt new file mode 100644 index 00000000..b798ed3f --- /dev/null +++ b/examples/nexus.r/Interaction.kt @@ -0,0 +1,58 @@ +import org.javacord.api.event.interaction.ButtonClickEvent +import org.javacord.api.event.interaction.MessageContextMenuCommandEvent +import org.javacord.api.event.interaction.SelectMenuChooseEvent +import org.javacord.api.interaction.Interaction +import org.javacord.api.interaction.MessageContextMenuInteraction +import org.javacord.api.listener.interaction.ButtonClickListener +import org.javacord.api.listener.interaction.SelectMenuChooseListener +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.commons.R +import pw.mihou.nexus.features.contexts.NexusContextMenuEvent +import pw.mihou.nexus.features.contexts.NexusMessageContextMenu +import pw.mihou.nexus.features.contexts.NexusUserContextMenu +import pw.mihou.nexus.features.messages.R +import pw.mihou.nexus.features.react.elements.Button +import pw.mihou.nexus.features.react.elements.Embed +import java.time.Instant + +/** + * Nexus.R supports basically everything, you can copy-paste the code from one event to another and it'll work + * the same regardless without much if any modification. In this example, we demonstrate how we can use Nexus.R + * to build amazingly simple yet reactive component. + * + * Our example will render a simple message with a button that increments the click count and re-render the + * message whenever a click is registered. + */ +object InteractionExample: ButtonClickListener, SelectMenuChooseListener { + private fun render(interaction: Interaction) { + interaction.R(ephemeral = false) { + var clicks by writable(0) + render { + Embed { + Title("Rendered with Nexus.R") + SpacedBody( + p("This message was rendered with Nexus.R."), + p("The button has been clicked ") + bold("$clicks times.") + ) + Color(java.awt.Color.YELLOW) + Timestamp(Instant.now()) + } + Button(label = "Click me!") { + it.buttonInteraction.acknowledge() + clicks += 1 + } + } + } + } + + override fun onButtonClick(event: ButtonClickEvent) { + render(event.interaction) + } + + override fun onSelectMenuChoose(event: SelectMenuChooseEvent) { + render(event.interaction) + } + + +} \ No newline at end of file diff --git a/examples/nexus.r/MessageEvent.kt b/examples/nexus.r/MessageEvent.kt new file mode 100644 index 00000000..7a25325c --- /dev/null +++ b/examples/nexus.r/MessageEvent.kt @@ -0,0 +1,39 @@ +import org.javacord.api.event.message.MessageCreateEvent +import org.javacord.api.listener.message.MessageCreateListener +import pw.mihou.nexus.features.messages.R +import pw.mihou.nexus.features.react.elements.Button +import pw.mihou.nexus.features.react.elements.Embed +import java.time.Instant + +/** + * Nexus.R supports basically everything, you can copy-paste the code from one event to another and it'll work + * the same regardless without much if any modification. In this example, we demonstrate how we can use Nexus.R + * to build amazingly simple yet reactive component. + * + * Our example will render a simple message with a button that increments the click count and re-render the + * message whenever a click is registered. + */ +object MessageEvent: MessageCreateListener { + override fun onMessageCreate(event: MessageCreateEvent) { + if (event.messageContent == "%nexus.r") { + event.R { + var clicks by writable(0) + render { + Embed { + Title("Rendered with Nexus.R") + SpacedBody( + p("This message was rendered with Nexus.R."), + p("The button has been clicked ") + bold("$clicks times.") + ) + Color(java.awt.Color.YELLOW) + Timestamp(Instant.now()) + } + Button(label = "Click me!") { + it.buttonInteraction.acknowledge() + clicks += 1 + } + } + } + } + } +} \ No newline at end of file diff --git a/examples/nexus.r/README.md b/examples/nexus.r/README.md new file mode 100644 index 00000000..f7dcc935 --- /dev/null +++ b/examples/nexus.r/README.md @@ -0,0 +1,30 @@ +# nexus.R + +React Native + Svelte for Discord Bots. Nexus.R is an innovative way to create and send responses or messages +to any text channel or interaction from slash commands to messages commands and even Users or text channels +with reactivity, enabling quick and simple re-rendering of messages upon state changes. + +### Features +- [x] Write-once, Use Everywhere + - Nexus.R allows you to easily reuse responses whether it was written for slash commands or + for messages with hardly any changes. +- [x] States in Discord Bot + - Nexus.R introduces the infamous states of JavaScript world into Discord bots, allowing you to create + messages that will re-render itself upon different state changes, just like a reactive website! +- [x] Webdev-like Feel + - In order to make Discord bot development more accessible to even web developers, we've made the + feature feel like writing web code (JSX) but simpler! + +### Table of Contents + +As Nexus.R is incredibly simple, you can read the examples and pretty much get a gist of how to use it. Here +are the examples we think that you should read first though: +1. [**Starting out with Nexus.R**](): It's all the same, except for how it's started. *Read which one you prefer to see.* + 2. [Slash Commands](SlashCommand.kt) + 3. [Context Menus](ContextMenu.kt) + 4. [Message Events i.e. Message Commands](MessageEvent.kt) + 5. [Interactions i.e. Buttons and Select Menus](Interaction.kt) +6. [**Passing State to Another Function**](%5B1%5D_passing_state): As states are not simply the same regular Kotlin properties, there needs a +little bit of a very tiny change when you want to pass state to another function outside of the `Nexus.R` scope. +7. [**Creating Components**](%5B2%5D_components): Reusing code is also important in coding, and components are just one way to reuse code +in Nexus.R, but there are drawbacks. It is recommended to read this after reading the above as it talks about important points in Nexus.R. \ No newline at end of file diff --git a/examples/nexus.r/SlashCommand.kt b/examples/nexus.r/SlashCommand.kt new file mode 100644 index 00000000..16eed130 --- /dev/null +++ b/examples/nexus.r/SlashCommand.kt @@ -0,0 +1,39 @@ +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.messages.R +import pw.mihou.nexus.features.react.elements.Button +import pw.mihou.nexus.features.react.elements.Embed +import java.time.Instant + +/** + * Nexus.R supports basically everything, you can copy-paste the code from one event to another and it'll work + * the same regardless without much if any modification. In this example, we demonstrate how we can use Nexus.R + * to build amazingly simple yet reactive component. + * + * Our example will render a simple message with a button that increments the click count and re-render the + * message whenever a click is registered. + */ +object SlashCommand: NexusHandler { + val name = "react" + val description = "Shows a demonstration of Nexus.R." + override fun onEvent(event: NexusCommandEvent) { + event.R { + var clicks by writable(0) + render { + Embed { + Title("Rendered with Nexus.R") + SpacedBody( + p("This message was rendered with Nexus.R."), + p("The button has been clicked ") + bold("$clicks times.") + ) + Color(java.awt.Color.YELLOW) + Timestamp(Instant.now()) + } + Button(label = "Click me!") { + it.buttonInteraction.acknowledge() + clicks += 1 + } + } + } + } +} \ No newline at end of file diff --git a/examples/nexus.r/[1]_passing_state/PassingState.kt b/examples/nexus.r/[1]_passing_state/PassingState.kt new file mode 100644 index 00000000..d437db4a --- /dev/null +++ b/examples/nexus.r/[1]_passing_state/PassingState.kt @@ -0,0 +1,66 @@ +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.react.React +import pw.mihou.nexus.features.react.elements.Button +import pw.mihou.nexus.features.react.elements.Embed +import java.time.Instant + +/** + * This example demonstrates how you pass [React.Writable] from the render function to another function. + * Unlike JavaScript frameworks, this isn't easily done as Kotlin maintains an immutable-only function argument, + * which means that we have to circumvent this by also maintaining the original [React.Writable] instance itself and + * passing that instead. + */ +object PassingState: NexusHandler { + val name = "react" + val description = "Shows a demonstration of Nexus.R." + override fun onEvent(event: NexusCommandEvent) { + event.R { + // As we need to maintain the original Writable instance, we need to separate it from the + // delegated property, this property won't have the native setter and related. + val clicksDelegate = writable(0) + var clicks by clicksDelegate + + render { + Embed { + Title("Rendered with Nexus.R") + SpacedBody( + p("This message was rendered with Nexus.R."), + p("The button has been clicked ") + bold("$clicks times.") + ) + Color(java.awt.Color.YELLOW) + Timestamp(Instant.now()) + } + Button(label = "Click me!") { + it.buttonInteraction.acknowledge() + increment(clicksDelegate) + } + Button(label = "Click this!") { + it.buttonInteraction.acknowledge() + clicks += 1 + } + Button(label = "Click also!") { + it.buttonInteraction.acknowledge() + incrementDelegated(clicksDelegate) + } + } + } + } + + /** + * This demonstrates using the [React.Writable] methods to manipulate the value of the [React.Writable], thereby + * causing a re-render. Generally, unless you perform a ton of manipulation, you can live with this. + */ + private fun increment(clicks: React.Writable) { + clicks.getAndUpdate { it + 1 } + } + + /** + * This demonstrates how you can also still delegate inside the method as well to maintain + * a similar feel to how a regular property would. + */ + private fun incrementDelegated(clicksDelegate: React.Writable) { + var clicks by clicksDelegate + clicks += 1 + } +} \ No newline at end of file diff --git a/examples/nexus.r/[1]_passing_state/README.md b/examples/nexus.r/[1]_passing_state/README.md new file mode 100644 index 00000000..fd63d131 --- /dev/null +++ b/examples/nexus.r/[1]_passing_state/README.md @@ -0,0 +1,8 @@ +# Passing States (Writables) + +As writables aren't simple properties, we can't just simply pass them to another function outside of the `Nexus.R` scope and expect it to work the same +especially since Kotlin doesn't support mutable function arguments. As such, in this example, we demonstrate how to properly pass writables from the +`Nexus.R` scope to another function by passing the Delegate class (`React.Writable`) itself. + +In the example, we also demonstrate how the `React.Writable` can then be delegated again inside the other function and +will still re-render the message when needed. \ No newline at end of file diff --git a/examples/nexus.r/[2]_components/Component.kt b/examples/nexus.r/[2]_components/Component.kt new file mode 100644 index 00000000..dcb221e3 --- /dev/null +++ b/examples/nexus.r/[2]_components/Component.kt @@ -0,0 +1,60 @@ +import org.javacord.api.event.message.MessageCreateEvent +import org.javacord.api.listener.message.MessageCreateListener +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.messages.R +import pw.mihou.nexus.features.react.React +import pw.mihou.nexus.features.react.elements.Button +import pw.mihou.nexus.features.react.elements.Embed +import java.time.Instant + +// Creating components can be done by simply creating an extension function over [React.Component], +// note once more that since we don't have access to the `Nexus.R` scope, we need to pass states like +// a regular function. Read the `[1]_passing_state` example if you haven't. +fun React.Component.Example(clicksDelegate: React.Writable) { + // Theoretically, you can create a [Writable] instance here, but it is not recommended + // as these are also called once again upon re-rendering. + var clicks by clicksDelegate + + Embed { + Title("Rendered with Nexus.R") + SpacedBody( + p("This message was rendered with Nexus.R."), + p("The button has been clicked ") + bold("$clicks times.") + ) + Color(java.awt.Color.YELLOW) + Timestamp(Instant.now()) + } + Button(label = "Click me!") { + it.buttonInteraction.acknowledge() + clicks += 1 + } +} + +// Once you've created the extension function, just simply call it on the `render` method. +// It is noted that you can't just simply create new Writable inside `render` as it is recreated +// again upon re-rendering. +object ComponentMessageExample: MessageCreateListener { + override fun onMessageCreate(event: MessageCreateEvent) { + event.R { + val clicksDelegate = writable(0) + render { + Example(clicksDelegate) + } + } + } +} + +// You can also do the same to slash commands, it doesn't matter as long as you have the parameters. +object ComponentSlashCommandExample: NexusHandler { + val name = "test" + val description = "An example of using components with Nexus.R" + override fun onEvent(event: NexusCommandEvent) { + event.R { + val clicksDelegate = writable(0) + render { + Example(clicksDelegate) + } + } + } +} \ No newline at end of file diff --git a/examples/nexus.r/[2]_components/README.md b/examples/nexus.r/[2]_components/README.md new file mode 100644 index 00000000..f047fd04 --- /dev/null +++ b/examples/nexus.r/[2]_components/README.md @@ -0,0 +1,15 @@ +# Components + +Reusing code is definitely one of the things that we'd like to do, and we can do so under +Nexus.R by simply creating an extension function over `React.Component` which would allow you to reuse +code easily. Although, it is noted that there are some drawbacks to this. + +### Drawbacks +1. You cannot simply create `Writable` as components are ran inside the `render` function which will re-render +the entire component, meaning that creating `Writable` inside `render` will result in the `Writable` being recreated instead. +2. Methods such as `onInitialRender` and `onRender` are not available. Components are simply functions ran inside `render` +which means anything inside cannot exist before the function itself is called which is also when the function happens to +render. + +Other than aforementioned drawbacks, components are an incredibly easy way to reuse code for cases such as +hybrid command bots i.e. bots that uses both slash and message commands. \ No newline at end of file From ec03a0bb1d6effe4b859f0792b666bfee295226d Mon Sep 17 00:00:00 2001 From: Miu Date: Tue, 17 Oct 2023 23:54:36 +0800 Subject: [PATCH 055/107] fix: `examples/nexus.r` readme --- examples/nexus.r/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/nexus.r/README.md b/examples/nexus.r/README.md index f7dcc935..2d0ce71b 100644 --- a/examples/nexus.r/README.md +++ b/examples/nexus.r/README.md @@ -20,11 +20,11 @@ with reactivity, enabling quick and simple re-rendering of messages upon state c As Nexus.R is incredibly simple, you can read the examples and pretty much get a gist of how to use it. Here are the examples we think that you should read first though: 1. [**Starting out with Nexus.R**](): It's all the same, except for how it's started. *Read which one you prefer to see.* - 2. [Slash Commands](SlashCommand.kt) - 3. [Context Menus](ContextMenu.kt) - 4. [Message Events i.e. Message Commands](MessageEvent.kt) - 5. [Interactions i.e. Buttons and Select Menus](Interaction.kt) -6. [**Passing State to Another Function**](%5B1%5D_passing_state): As states are not simply the same regular Kotlin properties, there needs a + - [Slash Commands](SlashCommand.kt) + - [Context Menus](ContextMenu.kt) + - [Message Events i.e. Message Commands](MessageEvent.kt) + - [Interactions i.e. Buttons and Select Menus](Interaction.kt) +2. [**Passing State to Another Function**](%5B1%5D_passing_state): As states are not simply the same regular Kotlin properties, there needs a little bit of a very tiny change when you want to pass state to another function outside of the `Nexus.R` scope. -7. [**Creating Components**](%5B2%5D_components): Reusing code is also important in coding, and components are just one way to reuse code -in Nexus.R, but there are drawbacks. It is recommended to read this after reading the above as it talks about important points in Nexus.R. \ No newline at end of file +3. [**Creating Components**](%5B2%5D_components): Reusing code is also important in coding, and components are just one way to reuse code +in Nexus.R, but there are drawbacks. It is recommended to read this after reading the above as it talks about important points in Nexus.R. From 4e75ca9d5b68ce72a42e8cd210e0410e76927438 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 18 Oct 2023 00:59:40 +0800 Subject: [PATCH 056/107] feat: add data fetching example --- examples/nexus.r/README.md | 3 +- .../nexus.r/[3]_data_fetching/DataFetching.kt | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 examples/nexus.r/[3]_data_fetching/DataFetching.kt diff --git a/examples/nexus.r/README.md b/examples/nexus.r/README.md index 2d0ce71b..ae9de65e 100644 --- a/examples/nexus.r/README.md +++ b/examples/nexus.r/README.md @@ -27,4 +27,5 @@ are the examples we think that you should read first though: 2. [**Passing State to Another Function**](%5B1%5D_passing_state): As states are not simply the same regular Kotlin properties, there needs a little bit of a very tiny change when you want to pass state to another function outside of the `Nexus.R` scope. 3. [**Creating Components**](%5B2%5D_components): Reusing code is also important in coding, and components are just one way to reuse code -in Nexus.R, but there are drawbacks. It is recommended to read this after reading the above as it talks about important points in Nexus.R. +4. [**Data Fetching**](%5B3%5D_data_fetching/DataFetching.kt): Data fetching is also an important factor in many bots, this is how you can load data +before rendering the message! diff --git a/examples/nexus.r/[3]_data_fetching/DataFetching.kt b/examples/nexus.r/[3]_data_fetching/DataFetching.kt new file mode 100644 index 00000000..b7842719 --- /dev/null +++ b/examples/nexus.r/[3]_data_fetching/DataFetching.kt @@ -0,0 +1,46 @@ +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.react.elements.Embed +import java.time.Instant + +// In this example, we'll demonstrate the different ways we can do data fetching via `onRender` and `onInitialRender`. +// Nexus.R uses `autoDefer` underneath, which means that you don't have to worry about calling `respondLater` or any +// of those mess, just take your time in grabbing that fresh data! +object DataFetching: NexusHandler { + val name = "react" + val description = "Shows a demonstration of Nexus.R." + override fun onEvent(event: NexusCommandEvent) { + event.R { + lateinit var qotd: String + + // event.R itself is actually a function, so you can initialize stuff here. + // but for demonstration purposes, we'll use `onInitialRender`, but just so you know, + // this is a 100% valid thing to do. + // + // qotd = "Lorem Ipsum Somethin' Magical!" + + onInitialRender { + // This is called way before `onRender` and `render` and can be where you can initialize + // data for the first time on render, although this is just the equivalent of doing it on `event.R` scope + // itself, but for people who prefer something like this, you can do this. + + // Imagine this is a data fetching thing. + qotd = "Lorem Ipsum Somethin' Magical!" + } + onRender { + // Anything that you want to do before the actual `render` is called. + // This is called again for re-rendering. + } + render { + Embed { + Title("Rendered with Nexus.R") + SpacedBody( + p("QOTD: $qotd"), + ) + Color(java.awt.Color.YELLOW) + Timestamp(Instant.now()) + } + } + } + } +} \ No newline at end of file From cf9a2b4fc22652326ccaf88c6e36ceab6dbedf5b Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 19 Oct 2023 15:37:56 +0800 Subject: [PATCH 057/107] feat: add numeric operator overrides using extension functions for `Writable` --- .../pw/mihou/nexus/features/react/React.kt | 4 ++ .../nexus/features/react/writable/Double.kt | 72 +++++++++++++++++++ .../nexus/features/react/writable/Float.kt | 72 +++++++++++++++++++ .../nexus/features/react/writable/Int.kt | 72 +++++++++++++++++++ .../nexus/features/react/writable/Long.kt | 72 +++++++++++++++++++ 5 files changed, 292 insertions(+) create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/Double.kt create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/Float.kt create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/Int.kt create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/Long.kt diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index f3827e0a..b5318a8b 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -360,4 +360,8 @@ class React internal constructor(private val api: DiscordApi, private val render return attachListeners(api) } } +} + +operator fun React.Writable.plusAssign(text: String) { + this.getAndUpdate { it + text } } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Double.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Double.kt new file mode 100644 index 00000000..4521482e --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Double.kt @@ -0,0 +1,72 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: Double): React.Writable { + this.getAndUpdate { it + number } + return this +} + +operator fun React.Writable.plusAssign(number: Double) { + this.plus(number) +} + +operator fun React.Writable.minus(number: Double): React.Writable { + this.getAndUpdate { it - number } + return this +} + +operator fun React.Writable.minusAssign(number: Double) { + this.minus(number) +} + +operator fun React.Writable.times(number: Double): React.Writable { + this.getAndUpdate { it * number } + return this +} + + +operator fun React.Writable.timesAssign(number: Double) { + this.times(number) +} + +operator fun React.Writable.div(number: Double): React.Writable { + this.getAndUpdate { it / number } + return this +} + +operator fun React.Writable.divAssign(number: Double) { + this.div(number) +} + + +operator fun React.Writable.rem(number: Double): React.Writable { + this.getAndUpdate { it % number } + return this +} + +operator fun React.Writable.remAssign(number: Double) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: Double): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1.0) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1.0) +} + +operator fun React.Writable.unaryPlus(): React.Writable { + this.getAndUpdate { +it } + return this +} + +operator fun React.Writable.unaryMinus(): React.Writable { + this.getAndUpdate { -it } + return this +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Float.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Float.kt new file mode 100644 index 00000000..00b84955 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Float.kt @@ -0,0 +1,72 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: Float): React.Writable { + this.getAndUpdate { it + number } + return this +} + +operator fun React.Writable.plusAssign(number: Float) { + this.plus(number) +} + +operator fun React.Writable.minus(number: Float): React.Writable { + this.getAndUpdate { it - number } + return this +} + +operator fun React.Writable.minusAssign(number: Float) { + this.minus(number) +} + +operator fun React.Writable.times(number: Float): React.Writable { + this.getAndUpdate { it * number } + return this +} + + +operator fun React.Writable.timesAssign(number: Float) { + this.times(number) +} + +operator fun React.Writable.div(number: Float): React.Writable { + this.getAndUpdate { it / number } + return this +} + +operator fun React.Writable.divAssign(number: Float) { + this.div(number) +} + + +operator fun React.Writable.rem(number: Float): React.Writable { + this.getAndUpdate { it % number } + return this +} + +operator fun React.Writable.remAssign(number: Float) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: Float): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1.0f) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1.0f) +} + +operator fun React.Writable.unaryPlus(): React.Writable { + this.getAndUpdate { +it } + return this +} + +operator fun React.Writable.unaryMinus(): React.Writable { + this.getAndUpdate { -it } + return this +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Int.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Int.kt new file mode 100644 index 00000000..34b77c86 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Int.kt @@ -0,0 +1,72 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: Int): React.Writable { + this.getAndUpdate { it + number } + return this +} + +operator fun React.Writable.plusAssign(number: Int) { + this.plus(number) +} + +operator fun React.Writable.minus(number: Int): React.Writable { + this.getAndUpdate { it - number } + return this +} + +operator fun React.Writable.minusAssign(number: Int) { + this.minus(number) +} + +operator fun React.Writable.times(number: Int): React.Writable { + this.getAndUpdate { it * number } + return this +} + + +operator fun React.Writable.timesAssign(number: Int) { + this.times(number) +} + +operator fun React.Writable.div(number: Int): React.Writable { + this.getAndUpdate { it / number } + return this +} + +operator fun React.Writable.divAssign(number: Int) { + this.div(number) +} + + +operator fun React.Writable.rem(number: Int): React.Writable { + this.getAndUpdate { it % number } + return this +} + +operator fun React.Writable.remAssign(number: Int) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: Int): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1) +} + +operator fun React.Writable.unaryPlus(): React.Writable { + this.getAndUpdate { +it } + return this +} + +operator fun React.Writable.unaryMinus(): React.Writable { + this.getAndUpdate { -it } + return this +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Long.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Long.kt new file mode 100644 index 00000000..9ccec90e --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Long.kt @@ -0,0 +1,72 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: Long): React.Writable { + this.getAndUpdate { it + number } + return this +} + +operator fun React.Writable.plusAssign(number: Long) { + this.plus(number) +} + +operator fun React.Writable.minus(number: Long): React.Writable { + this.getAndUpdate { it - number } + return this +} + +operator fun React.Writable.minusAssign(number: Long) { + this.minus(number) +} + +operator fun React.Writable.times(number: Long): React.Writable { + this.getAndUpdate { it * number } + return this +} + + +operator fun React.Writable.timesAssign(number: Long) { + this.times(number) +} + +operator fun React.Writable.div(number: Long): React.Writable { + this.getAndUpdate { it / number } + return this +} + +operator fun React.Writable.divAssign(number: Long) { + this.div(number) +} + + +operator fun React.Writable.rem(number: Long): React.Writable { + this.getAndUpdate { it % number } + return this +} + +operator fun React.Writable.remAssign(number: Long) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: Long): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1) +} + +operator fun React.Writable.unaryPlus(): React.Writable { + this.getAndUpdate { +it } + return this +} + +operator fun React.Writable.unaryMinus(): React.Writable { + this.getAndUpdate { -it } + return this +} \ No newline at end of file From ce1dc53a73b0f03c8b301d7ec213762cc2743beb Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 19 Oct 2023 15:41:05 +0800 Subject: [PATCH 058/107] feat: add string operator overrides using extension functions for `Writable` --- src/main/java/pw/mihou/nexus/features/react/React.kt | 7 ++----- .../pw/mihou/nexus/features/react/writable/String.kt | 11 +++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/String.kt diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index b5318a8b..19f1b7fc 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -89,12 +89,13 @@ class React internal constructor(private val api: DiscordApi, private val render fun render(component: ReactComponent) { val element = apply(component) - when(renderMode) { + when (renderMode) { RenderMode.Interaction -> { val (unsubscribe, message) = element.render(api) this.message = message this.unsubscribe = unsubscribe } + RenderMode.Message -> { val builder = MessageBuilder() val unsubscribe = element.render(builder, api) @@ -360,8 +361,4 @@ class React internal constructor(private val api: DiscordApi, private val render return attachListeners(api) } } -} - -operator fun React.Writable.plusAssign(text: String) { - this.getAndUpdate { it + text } } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/String.kt b/src/main/java/pw/mihou/nexus/features/react/writable/String.kt new file mode 100644 index 00000000..e63c76d3 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/String.kt @@ -0,0 +1,11 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(text: String): React.Writable { + this.getAndUpdate { it + text } + return this +} +operator fun React.Writable.plusAssign(text: String) { + this.plus(text) +} \ No newline at end of file From c21fee68b181028e7d914fbc8cb45468cd36e8e1 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 19 Oct 2023 15:48:24 +0800 Subject: [PATCH 059/107] feat: add more primitive numeric overloads for `Writable` --- .../nexus/features/react/writable/Byte.kt | 72 +++++++++++++++++++ .../nexus/features/react/writable/Short.kt | 72 +++++++++++++++++++ .../nexus/features/react/writable/UByte.kt | 62 ++++++++++++++++ .../nexus/features/react/writable/UInt.kt | 62 ++++++++++++++++ .../nexus/features/react/writable/ULong.kt | 62 ++++++++++++++++ 5 files changed, 330 insertions(+) create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/Byte.kt create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/Short.kt create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/UByte.kt create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/UInt.kt create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/ULong.kt diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Byte.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Byte.kt new file mode 100644 index 00000000..acf3a84d --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Byte.kt @@ -0,0 +1,72 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: Byte): React.Writable { + this.getAndUpdate { (it + number).toByte() } + return this +} + +operator fun React.Writable.plusAssign(number: Byte) { + this.plus(number) +} + +operator fun React.Writable.minus(number: Byte): React.Writable { + this.getAndUpdate { (it - number).toByte() } + return this +} + +operator fun React.Writable.minusAssign(number: Byte) { + this.minus(number) +} + +operator fun React.Writable.times(number: Byte): React.Writable { + this.getAndUpdate { (it * number).toByte() } + return this +} + + +operator fun React.Writable.timesAssign(number: Byte) { + this.times(number) +} + +operator fun React.Writable.div(number: Byte): React.Writable { + this.getAndUpdate { (it / number).toByte() } + return this +} + +operator fun React.Writable.divAssign(number: Byte) { + this.div(number) +} + + +operator fun React.Writable.rem(number: Byte): React.Writable { + this.getAndUpdate { (it % number).toByte() } + return this +} + +operator fun React.Writable.remAssign(number: Byte) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: Byte): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1.toByte()) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1.toByte()) +} + +operator fun React.Writable.unaryPlus(): React.Writable { + this.getAndUpdate { (+it).toByte() } + return this +} + +operator fun React.Writable.unaryMinus(): React.Writable { + this.getAndUpdate { (-it).toByte() } + return this +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Short.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Short.kt new file mode 100644 index 00000000..19c9583a --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Short.kt @@ -0,0 +1,72 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: Short): React.Writable { + this.getAndUpdate { (it + number).toShort() } + return this +} + +operator fun React.Writable.plusAssign(number: Short) { + this.plus(number) +} + +operator fun React.Writable.minus(number: Short): React.Writable { + this.getAndUpdate { (it - number).toShort() } + return this +} + +operator fun React.Writable.minusAssign(number: Short) { + this.minus(number) +} + +operator fun React.Writable.times(number: Short): React.Writable { + this.getAndUpdate { (it * number).toShort() } + return this +} + + +operator fun React.Writable.timesAssign(number: Short) { + this.times(number) +} + +operator fun React.Writable.div(number: Short): React.Writable { + this.getAndUpdate { (it / number).toShort() } + return this +} + +operator fun React.Writable.divAssign(number: Short) { + this.div(number) +} + + +operator fun React.Writable.rem(number: Short): React.Writable { + this.getAndUpdate { (it % number).toShort() } + return this +} + +operator fun React.Writable.remAssign(number: Short) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: Short): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1) +} + +operator fun React.Writable.unaryPlus(): React.Writable { + this.getAndUpdate { (+it).toShort() } + return this +} + +operator fun React.Writable.unaryMinus(): React.Writable { + this.getAndUpdate { (-it).toShort() } + return this +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/UByte.kt b/src/main/java/pw/mihou/nexus/features/react/writable/UByte.kt new file mode 100644 index 00000000..08ce0d10 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/UByte.kt @@ -0,0 +1,62 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: UByte): React.Writable { + this.getAndUpdate { (it + number).toUByte() } + return this +} + +operator fun React.Writable.plusAssign(number: UByte) { + this.plus(number) +} + +operator fun React.Writable.minus(number: UByte): React.Writable { + this.getAndUpdate { (it - number).toUByte() } + return this +} + +operator fun React.Writable.minusAssign(number: UByte) { + this.minus(number) +} + +operator fun React.Writable.times(number: UByte): React.Writable { + this.getAndUpdate { (it * number).toUByte() } + return this +} + + +operator fun React.Writable.timesAssign(number: UByte) { + this.times(number) +} + +operator fun React.Writable.div(number: UByte): React.Writable { + this.getAndUpdate { (it / number).toUByte() } + return this +} + +operator fun React.Writable.divAssign(number: UByte) { + this.div(number) +} + + +operator fun React.Writable.rem(number: UByte): React.Writable { + this.getAndUpdate { (it % number).toUByte() } + return this +} + +operator fun React.Writable.remAssign(number: UByte) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: UByte): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1.toUByte()) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1.toUByte()) +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/UInt.kt b/src/main/java/pw/mihou/nexus/features/react/writable/UInt.kt new file mode 100644 index 00000000..67d01e6f --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/UInt.kt @@ -0,0 +1,62 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: UInt): React.Writable { + this.getAndUpdate { it + number } + return this +} + +operator fun React.Writable.plusAssign(number: UInt) { + this.plus(number) +} + +operator fun React.Writable.minus(number: UInt): React.Writable { + this.getAndUpdate { it - number } + return this +} + +operator fun React.Writable.minusAssign(number: UInt) { + this.minus(number) +} + +operator fun React.Writable.times(number: UInt): React.Writable { + this.getAndUpdate { it * number } + return this +} + + +operator fun React.Writable.timesAssign(number: UInt) { + this.times(number) +} + +operator fun React.Writable.div(number: UInt): React.Writable { + this.getAndUpdate { it / number } + return this +} + +operator fun React.Writable.divAssign(number: UInt) { + this.div(number) +} + + +operator fun React.Writable.rem(number: UInt): React.Writable { + this.getAndUpdate { it % number } + return this +} + +operator fun React.Writable.remAssign(number: UInt) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: UInt): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1.toUInt()) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1.toUInt()) +} \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/ULong.kt b/src/main/java/pw/mihou/nexus/features/react/writable/ULong.kt new file mode 100644 index 00000000..e3eb380d --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/ULong.kt @@ -0,0 +1,62 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(number: ULong): React.Writable { + this.getAndUpdate { it + number } + return this +} + +operator fun React.Writable.plusAssign(number: ULong) { + this.plus(number) +} + +operator fun React.Writable.minus(number: ULong): React.Writable { + this.getAndUpdate { it - number } + return this +} + +operator fun React.Writable.minusAssign(number: ULong) { + this.minus(number) +} + +operator fun React.Writable.times(number: ULong): React.Writable { + this.getAndUpdate { it * number } + return this +} + + +operator fun React.Writable.timesAssign(number: ULong) { + this.times(number) +} + +operator fun React.Writable.div(number: ULong): React.Writable { + this.getAndUpdate { it / number } + return this +} + +operator fun React.Writable.divAssign(number: ULong) { + this.div(number) +} + + +operator fun React.Writable.rem(number: ULong): React.Writable { + this.getAndUpdate { it % number } + return this +} + +operator fun React.Writable.remAssign(number: ULong) { + this.rem(number) +} + +operator fun React.Writable.compareTo(number: ULong): Int { + return this.get().compareTo(number) +} + +operator fun React.Writable.dec(): React.Writable { + return minus(1.toULong()) +} + +operator fun React.Writable.inc(): React.Writable { + return plus(1.toULong()) +} \ No newline at end of file From b3c1b32f05516f7f2b8850ee37c7b108531f404b Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 19 Oct 2023 15:59:57 +0800 Subject: [PATCH 060/107] feat: add operators for List for `Writable` --- .../nexus/features/react/writable/List.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/pw/mihou/nexus/features/react/writable/List.kt diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/List.kt b/src/main/java/pw/mihou/nexus/features/react/writable/List.kt new file mode 100644 index 00000000..6892912c --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/writable/List.kt @@ -0,0 +1,31 @@ +package pw.mihou.nexus.features.react.writable + +import pw.mihou.nexus.features.react.React + +operator fun React.Writable.plus(element: T): React.Writable where List : MutableCollection { + this.getAndUpdate { + it += element + it + } + return this +} + +operator fun React.Writable.plusAssign(element: T) where List : MutableCollection { + this.plus(element) +} + +operator fun React.Writable.minus(element: T): React.Writable where List : MutableCollection { + this.getAndUpdate { + it.remove(element) + it + } + return this +} + +operator fun React.Writable.minusAssign(element: T) where List : MutableCollection { + this.plus(element) +} + +operator fun React.Writable.contains(element: T): Boolean where List : Collection { + return this.get().contains(element) +} \ No newline at end of file From 2dc0ac2f6b09c6f0150a122d1d6914e1475785d3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 19 Oct 2023 16:18:39 +0800 Subject: [PATCH 061/107] feat: add more operators for List for `Writable` --- .../pw/mihou/nexus/features/react/writable/List.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/List.kt b/src/main/java/pw/mihou/nexus/features/react/writable/List.kt index 6892912c..2f4f4cef 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/List.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/List.kt @@ -28,4 +28,14 @@ operator fun React.Writable.minusAssign(element: T) where List : operator fun React.Writable.contains(element: T): Boolean where List : Collection { return this.get().contains(element) +} + +@Suppress("UNCHECKED_CAST") +operator fun React.Writable.get(index: Int): T? where List : Collection { + return when(val collection = this.get()) { + is kotlin.collections.List<*> -> collection[index] as? T + is MutableList<*> -> collection[index] as? T + is Array<*> -> collection[index] as? T + else -> collection.withIndex().find { (i, _) -> i == index } as? T + } } \ No newline at end of file From e71e8957620d6ee48a5bd138f9fc176f7e56ff90 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 19 Oct 2023 23:20:37 +0800 Subject: [PATCH 062/107] feat: update the examples to use newer operator overloads --- examples/nexus.r/[1]_passing_state/PassingState.kt | 9 ++++++++- examples/nexus.r/[2]_components/Component.kt | 7 ++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/nexus.r/[1]_passing_state/PassingState.kt b/examples/nexus.r/[1]_passing_state/PassingState.kt index d437db4a..589013ce 100644 --- a/examples/nexus.r/[1]_passing_state/PassingState.kt +++ b/examples/nexus.r/[1]_passing_state/PassingState.kt @@ -3,6 +3,7 @@ import pw.mihou.nexus.features.command.facade.NexusHandler import pw.mihou.nexus.features.react.React import pw.mihou.nexus.features.react.elements.Button import pw.mihou.nexus.features.react.elements.Embed +import pw.mihou.nexus.features.react.writable.plusAssign import java.time.Instant /** @@ -52,7 +53,13 @@ object PassingState: NexusHandler { * causing a re-render. Generally, unless you perform a ton of manipulation, you can live with this. */ private fun increment(clicks: React.Writable) { - clicks.getAndUpdate { it + 1 } + // Newer version after a recent commit adding operator overloads over primitive types + // and lists. + clicks += 1 + + // Older method and more optimal for cases that are not supported with the operator overloads + // such as types that are not supported. + // clicks.getAndUpdate { it + 1 } } /** diff --git a/examples/nexus.r/[2]_components/Component.kt b/examples/nexus.r/[2]_components/Component.kt index dcb221e3..8bdbc751 100644 --- a/examples/nexus.r/[2]_components/Component.kt +++ b/examples/nexus.r/[2]_components/Component.kt @@ -6,16 +6,13 @@ import pw.mihou.nexus.features.messages.R import pw.mihou.nexus.features.react.React import pw.mihou.nexus.features.react.elements.Button import pw.mihou.nexus.features.react.elements.Embed +import pw.mihou.nexus.features.react.writable.plusAssign import java.time.Instant // Creating components can be done by simply creating an extension function over [React.Component], // note once more that since we don't have access to the `Nexus.R` scope, we need to pass states like // a regular function. Read the `[1]_passing_state` example if you haven't. -fun React.Component.Example(clicksDelegate: React.Writable) { - // Theoretically, you can create a [Writable] instance here, but it is not recommended - // as these are also called once again upon re-rendering. - var clicks by clicksDelegate - +fun React.Component.Example(clicks: React.Writable) { Embed { Title("Rendered with Nexus.R") SpacedBody( From 4518e575a1778227e430910ebaccf657bc15bf2f Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 19 Oct 2023 23:30:26 +0800 Subject: [PATCH 063/107] feat: add tiny dedupe --- .../java/pw/mihou/nexus/features/react/React.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 19f1b7fc..8b1018a0 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -215,7 +215,7 @@ class React internal constructor(private val api: DiscordApi, private val render val oldValue = _value.get() _value.set(value) - subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } + this.react(oldValue, value) } /** @@ -231,7 +231,7 @@ class React internal constructor(private val api: DiscordApi, private val render _value.getAndUpdate(updater) val value = _value.get() - subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } + this.react(oldValue, value) } /** @@ -254,6 +254,17 @@ class React internal constructor(private val api: DiscordApi, private val render return { subscribers.remove(subscription) } } + /** + * Reacts to the change and executes all the subscriptions that were subscribed at the + * time of execution. + * + * @param oldValue the old value. + * @param value the current value. + */ + internal fun react(oldValue: T, value: T) { + subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } + } + override fun toString(): String { return _value.get().toString() } From 25fb24719d1ad20b7efe7c1d3c714f0e3a90f086 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 21 Oct 2023 10:40:30 +0800 Subject: [PATCH 064/107] feat: allow `React` to update `Message` --- .../features/messages/ReactExtensions.kt | 19 +++++++++++++++++++ .../pw/mihou/nexus/features/react/React.kt | 14 +++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index 60f903ca..a292ddcf 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -41,4 +41,23 @@ fun Messageable.R(api: DiscordApi, react: React.() -> Unit): CompletableFuture Unit): CompletableFuture { + val r = React(api, React.RenderMode.Message) + react(r) + + r.resultingMessage = this + return r.messageUpdater!!.replaceMessage().thenApply { + r.resultingMessage = it + return@thenApply it + } } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 8b1018a0..89bcb60e 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -33,6 +33,7 @@ class React internal constructor(private val api: DiscordApi, private val render internal var message: NexusMessage? = null internal var messageBuilder: MessageBuilder? = null + internal var messageUpdater: MessageUpdater? = null private var unsubscribe: Unsubscribe = {} private var component: (Component.() -> Unit)? = null @@ -60,7 +61,8 @@ class React internal constructor(private val api: DiscordApi, private val render internal enum class RenderMode { Interaction, - Message + Message, + UpdateMessage } /** @@ -103,6 +105,16 @@ class React internal constructor(private val api: DiscordApi, private val render this.messageBuilder = builder this.unsubscribe = unsubscribe } + RenderMode.UpdateMessage -> { + if (resultingMessage == null) { + throw IllegalStateException("Updating a message with React needs `resultingMessage` to not be null.") + } + val updater = MessageUpdater(resultingMessage) + val unsubscribe = element.render(updater, api) + + this.messageUpdater = updater + this.unsubscribe = unsubscribe + } } this.rendered = true } From d336e01798c2c70683c4e921f49b53ba508e7d1b Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 21 Oct 2023 10:45:34 +0800 Subject: [PATCH 065/107] feat: rename `getAndUpdate` to `update` --- examples/nexus.r/[1]_passing_state/PassingState.kt | 2 +- examples/nexus.r/[2]_components/Component.kt | 2 +- .../java/pw/mihou/nexus/features/react/React.kt | 6 +++--- .../pw/mihou/nexus/features/react/writable/Byte.kt | 14 +++++++------- .../mihou/nexus/features/react/writable/Double.kt | 14 +++++++------- .../mihou/nexus/features/react/writable/Float.kt | 14 +++++++------- .../pw/mihou/nexus/features/react/writable/Int.kt | 14 +++++++------- .../pw/mihou/nexus/features/react/writable/List.kt | 4 ++-- .../pw/mihou/nexus/features/react/writable/Long.kt | 14 +++++++------- .../mihou/nexus/features/react/writable/Short.kt | 14 +++++++------- .../mihou/nexus/features/react/writable/String.kt | 2 +- .../mihou/nexus/features/react/writable/UByte.kt | 10 +++++----- .../pw/mihou/nexus/features/react/writable/UInt.kt | 10 +++++----- .../mihou/nexus/features/react/writable/ULong.kt | 10 +++++----- 14 files changed, 65 insertions(+), 65 deletions(-) diff --git a/examples/nexus.r/[1]_passing_state/PassingState.kt b/examples/nexus.r/[1]_passing_state/PassingState.kt index 589013ce..9214c1e8 100644 --- a/examples/nexus.r/[1]_passing_state/PassingState.kt +++ b/examples/nexus.r/[1]_passing_state/PassingState.kt @@ -59,7 +59,7 @@ object PassingState: NexusHandler { // Older method and more optimal for cases that are not supported with the operator overloads // such as types that are not supported. - // clicks.getAndUpdate { it + 1 } + // clicks.update { it + 1 } } /** diff --git a/examples/nexus.r/[2]_components/Component.kt b/examples/nexus.r/[2]_components/Component.kt index 8bdbc751..2a04bfc1 100644 --- a/examples/nexus.r/[2]_components/Component.kt +++ b/examples/nexus.r/[2]_components/Component.kt @@ -18,7 +18,7 @@ fun React.Component.Example(clicks: React.Writable) { SpacedBody( p("This message was rendered with Nexus.R."), p("The button has been clicked ") + bold("$clicks times.") - ) + )ge Color(java.awt.Color.YELLOW) Timestamp(Instant.now()) } diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 89bcb60e..a9a83e61 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -207,7 +207,7 @@ class React internal constructor(private val api: DiscordApi, private val render /** * Sets the value of this [Writable]. This is intended to be used for delegation. You may be looking for - * [set] or [getAndUpdate] instead which allows you to manipulate the [Writable]'s value. + * [set] or [update] instead which allows you to manipulate the [Writable]'s value. */ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { set(value) @@ -219,7 +219,7 @@ class React internal constructor(private val api: DiscordApi, private val render * all subscriptions are executed without interfering or delaying one another. * * When performing things such as increment, decrements, or anything that requires the current value, we - * recommend using [getAndUpdate] instead which will allow you to atomically update the value. + * recommend using [update] instead which will allow you to atomically update the value. * * @param value the new value of the [Writable]. */ @@ -238,7 +238,7 @@ class React internal constructor(private val api: DiscordApi, private val render * Similar to [set], this executes all the subscriptions asynchronously. * @param updater the updater to update the value of the [Writable]. */ - fun getAndUpdate(updater: (T) -> T) { + fun update(updater: (T) -> T) { val oldValue = _value.get() _value.getAndUpdate(updater) diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Byte.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Byte.kt index acf3a84d..ef0920ac 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/Byte.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Byte.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: Byte): React.Writable { - this.getAndUpdate { (it + number).toByte() } + this.update { (it + number).toByte() } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: Byte) { } operator fun React.Writable.minus(number: Byte): React.Writable { - this.getAndUpdate { (it - number).toByte() } + this.update { (it - number).toByte() } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: Byte) { } operator fun React.Writable.times(number: Byte): React.Writable { - this.getAndUpdate { (it * number).toByte() } + this.update { (it * number).toByte() } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: Byte) { } operator fun React.Writable.div(number: Byte): React.Writable { - this.getAndUpdate { (it / number).toByte() } + this.update { (it / number).toByte() } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: Byte) { operator fun React.Writable.rem(number: Byte): React.Writable { - this.getAndUpdate { (it % number).toByte() } + this.update { (it % number).toByte() } return this } @@ -62,11 +62,11 @@ operator fun React.Writable.inc(): React.Writable { } operator fun React.Writable.unaryPlus(): React.Writable { - this.getAndUpdate { (+it).toByte() } + this.update { (+it).toByte() } return this } operator fun React.Writable.unaryMinus(): React.Writable { - this.getAndUpdate { (-it).toByte() } + this.update { (-it).toByte() } return this } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Double.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Double.kt index 4521482e..cd77d8a8 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/Double.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Double.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: Double): React.Writable { - this.getAndUpdate { it + number } + this.update { it + number } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: Double) { } operator fun React.Writable.minus(number: Double): React.Writable { - this.getAndUpdate { it - number } + this.update { it - number } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: Double) { } operator fun React.Writable.times(number: Double): React.Writable { - this.getAndUpdate { it * number } + this.update { it * number } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: Double) { } operator fun React.Writable.div(number: Double): React.Writable { - this.getAndUpdate { it / number } + this.update { it / number } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: Double) { operator fun React.Writable.rem(number: Double): React.Writable { - this.getAndUpdate { it % number } + this.update { it % number } return this } @@ -62,11 +62,11 @@ operator fun React.Writable.inc(): React.Writable { } operator fun React.Writable.unaryPlus(): React.Writable { - this.getAndUpdate { +it } + this.update { +it } return this } operator fun React.Writable.unaryMinus(): React.Writable { - this.getAndUpdate { -it } + this.update { -it } return this } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Float.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Float.kt index 00b84955..325d95ef 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/Float.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Float.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: Float): React.Writable { - this.getAndUpdate { it + number } + this.update { it + number } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: Float) { } operator fun React.Writable.minus(number: Float): React.Writable { - this.getAndUpdate { it - number } + this.update { it - number } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: Float) { } operator fun React.Writable.times(number: Float): React.Writable { - this.getAndUpdate { it * number } + this.update { it * number } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: Float) { } operator fun React.Writable.div(number: Float): React.Writable { - this.getAndUpdate { it / number } + this.update { it / number } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: Float) { operator fun React.Writable.rem(number: Float): React.Writable { - this.getAndUpdate { it % number } + this.update { it % number } return this } @@ -62,11 +62,11 @@ operator fun React.Writable.inc(): React.Writable { } operator fun React.Writable.unaryPlus(): React.Writable { - this.getAndUpdate { +it } + this.update { +it } return this } operator fun React.Writable.unaryMinus(): React.Writable { - this.getAndUpdate { -it } + this.update { -it } return this } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Int.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Int.kt index 34b77c86..7b33e757 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/Int.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Int.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: Int): React.Writable { - this.getAndUpdate { it + number } + this.update { it + number } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: Int) { } operator fun React.Writable.minus(number: Int): React.Writable { - this.getAndUpdate { it - number } + this.update { it - number } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: Int) { } operator fun React.Writable.times(number: Int): React.Writable { - this.getAndUpdate { it * number } + this.update { it * number } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: Int) { } operator fun React.Writable.div(number: Int): React.Writable { - this.getAndUpdate { it / number } + this.update { it / number } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: Int) { operator fun React.Writable.rem(number: Int): React.Writable { - this.getAndUpdate { it % number } + this.update { it % number } return this } @@ -62,11 +62,11 @@ operator fun React.Writable.inc(): React.Writable { } operator fun React.Writable.unaryPlus(): React.Writable { - this.getAndUpdate { +it } + this.update { +it } return this } operator fun React.Writable.unaryMinus(): React.Writable { - this.getAndUpdate { -it } + this.update { -it } return this } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/List.kt b/src/main/java/pw/mihou/nexus/features/react/writable/List.kt index 2f4f4cef..50b78277 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/List.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/List.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(element: T): React.Writable where List : MutableCollection { - this.getAndUpdate { + this.update { it += element it } @@ -15,7 +15,7 @@ operator fun React.Writable.plusAssign(element: T) where List : } operator fun React.Writable.minus(element: T): React.Writable where List : MutableCollection { - this.getAndUpdate { + this.update { it.remove(element) it } diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Long.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Long.kt index 9ccec90e..7ccec7ac 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/Long.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Long.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: Long): React.Writable { - this.getAndUpdate { it + number } + this.update { it + number } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: Long) { } operator fun React.Writable.minus(number: Long): React.Writable { - this.getAndUpdate { it - number } + this.update { it - number } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: Long) { } operator fun React.Writable.times(number: Long): React.Writable { - this.getAndUpdate { it * number } + this.update { it * number } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: Long) { } operator fun React.Writable.div(number: Long): React.Writable { - this.getAndUpdate { it / number } + this.update { it / number } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: Long) { operator fun React.Writable.rem(number: Long): React.Writable { - this.getAndUpdate { it % number } + this.update { it % number } return this } @@ -62,11 +62,11 @@ operator fun React.Writable.inc(): React.Writable { } operator fun React.Writable.unaryPlus(): React.Writable { - this.getAndUpdate { +it } + this.update { +it } return this } operator fun React.Writable.unaryMinus(): React.Writable { - this.getAndUpdate { -it } + this.update { -it } return this } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/Short.kt b/src/main/java/pw/mihou/nexus/features/react/writable/Short.kt index 19c9583a..bd85fbf1 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/Short.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/Short.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: Short): React.Writable { - this.getAndUpdate { (it + number).toShort() } + this.update { (it + number).toShort() } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: Short) { } operator fun React.Writable.minus(number: Short): React.Writable { - this.getAndUpdate { (it - number).toShort() } + this.update { (it - number).toShort() } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: Short) { } operator fun React.Writable.times(number: Short): React.Writable { - this.getAndUpdate { (it * number).toShort() } + this.update { (it * number).toShort() } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: Short) { } operator fun React.Writable.div(number: Short): React.Writable { - this.getAndUpdate { (it / number).toShort() } + this.update { (it / number).toShort() } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: Short) { operator fun React.Writable.rem(number: Short): React.Writable { - this.getAndUpdate { (it % number).toShort() } + this.update { (it % number).toShort() } return this } @@ -62,11 +62,11 @@ operator fun React.Writable.inc(): React.Writable { } operator fun React.Writable.unaryPlus(): React.Writable { - this.getAndUpdate { (+it).toShort() } + this.update { (+it).toShort() } return this } operator fun React.Writable.unaryMinus(): React.Writable { - this.getAndUpdate { (-it).toShort() } + this.update { (-it).toShort() } return this } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/String.kt b/src/main/java/pw/mihou/nexus/features/react/writable/String.kt index e63c76d3..c5300675 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/String.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/String.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(text: String): React.Writable { - this.getAndUpdate { it + text } + this.update { it + text } return this } operator fun React.Writable.plusAssign(text: String) { diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/UByte.kt b/src/main/java/pw/mihou/nexus/features/react/writable/UByte.kt index 08ce0d10..5eadc6c3 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/UByte.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/UByte.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: UByte): React.Writable { - this.getAndUpdate { (it + number).toUByte() } + this.update { (it + number).toUByte() } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: UByte) { } operator fun React.Writable.minus(number: UByte): React.Writable { - this.getAndUpdate { (it - number).toUByte() } + this.update { (it - number).toUByte() } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: UByte) { } operator fun React.Writable.times(number: UByte): React.Writable { - this.getAndUpdate { (it * number).toUByte() } + this.update { (it * number).toUByte() } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: UByte) { } operator fun React.Writable.div(number: UByte): React.Writable { - this.getAndUpdate { (it / number).toUByte() } + this.update { (it / number).toUByte() } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: UByte) { operator fun React.Writable.rem(number: UByte): React.Writable { - this.getAndUpdate { (it % number).toUByte() } + this.update { (it % number).toUByte() } return this } diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/UInt.kt b/src/main/java/pw/mihou/nexus/features/react/writable/UInt.kt index 67d01e6f..1f81bed8 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/UInt.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/UInt.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: UInt): React.Writable { - this.getAndUpdate { it + number } + this.update { it + number } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: UInt) { } operator fun React.Writable.minus(number: UInt): React.Writable { - this.getAndUpdate { it - number } + this.update { it - number } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: UInt) { } operator fun React.Writable.times(number: UInt): React.Writable { - this.getAndUpdate { it * number } + this.update { it * number } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: UInt) { } operator fun React.Writable.div(number: UInt): React.Writable { - this.getAndUpdate { it / number } + this.update { it / number } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: UInt) { operator fun React.Writable.rem(number: UInt): React.Writable { - this.getAndUpdate { it % number } + this.update { it % number } return this } diff --git a/src/main/java/pw/mihou/nexus/features/react/writable/ULong.kt b/src/main/java/pw/mihou/nexus/features/react/writable/ULong.kt index e3eb380d..74a5766e 100644 --- a/src/main/java/pw/mihou/nexus/features/react/writable/ULong.kt +++ b/src/main/java/pw/mihou/nexus/features/react/writable/ULong.kt @@ -3,7 +3,7 @@ package pw.mihou.nexus.features.react.writable import pw.mihou.nexus.features.react.React operator fun React.Writable.plus(number: ULong): React.Writable { - this.getAndUpdate { it + number } + this.update { it + number } return this } @@ -12,7 +12,7 @@ operator fun React.Writable.plusAssign(number: ULong) { } operator fun React.Writable.minus(number: ULong): React.Writable { - this.getAndUpdate { it - number } + this.update { it - number } return this } @@ -21,7 +21,7 @@ operator fun React.Writable.minusAssign(number: ULong) { } operator fun React.Writable.times(number: ULong): React.Writable { - this.getAndUpdate { it * number } + this.update { it * number } return this } @@ -31,7 +31,7 @@ operator fun React.Writable.timesAssign(number: ULong) { } operator fun React.Writable.div(number: ULong): React.Writable { - this.getAndUpdate { it / number } + this.update { it / number } return this } @@ -41,7 +41,7 @@ operator fun React.Writable.divAssign(number: ULong) { operator fun React.Writable.rem(number: ULong): React.Writable { - this.getAndUpdate { it % number } + this.update { it % number } return this } From f2cf66356a98c26964afd0aa5960d3863f2c2ec0 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 21 Oct 2023 16:50:00 +0800 Subject: [PATCH 066/107] feat: fix example --- examples/nexus.r/[2]_components/Component.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/nexus.r/[2]_components/Component.kt b/examples/nexus.r/[2]_components/Component.kt index 2a04bfc1..8bdbc751 100644 --- a/examples/nexus.r/[2]_components/Component.kt +++ b/examples/nexus.r/[2]_components/Component.kt @@ -18,7 +18,7 @@ fun React.Component.Example(clicks: React.Writable) { SpacedBody( p("This message was rendered with Nexus.R."), p("The button has been clicked ") + bold("$clicks times.") - )ge + ) Color(java.awt.Color.YELLOW) Timestamp(Instant.now()) } From 236dca4ea28ec21915fd00dc6bddb9f206df859a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 21 Oct 2023 22:56:08 +0800 Subject: [PATCH 067/107] feat: clear `uuids` by replacing list instead. --- src/main/java/pw/mihou/nexus/features/react/React.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index a9a83e61..1bac25e8 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -309,7 +309,7 @@ class React internal constructor(private val api: DiscordApi, private val render return { listenerManagers.forEach { managers -> managers.forEach { it.remove() } } uuids.forEach { NexusUuidAssigner.deny(it) } - uuids.clear() + uuids = mutableListOf() } } From e28156b67c66fc96ac77337ca2a504a64e4a65a5 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 28 Oct 2023 16:20:57 +0800 Subject: [PATCH 068/107] feat: add support for derives in `Writable` and a new `ReadOnly` state. --- .../pw/mihou/nexus/features/react/React.kt | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 1bac25e8..0d50cc47 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -21,6 +21,7 @@ typealias Unsubscribe = () -> Unit typealias RenderSubscription = () -> Unit typealias ReactComponent = React.Component.() -> Unit +typealias Derive = (T) -> K /** * [React] is the React-Svelte inspired method of rendering (or sending) messages as response to various scenarios such @@ -182,6 +183,58 @@ class React internal constructor(private val api: DiscordApi, private val render return writable } + /** + * Creates a new [ReadOnly] state that has a value derived of this [Writable], which means that the value + * of the new [ReadOnly] state changes whenever the value of the current [Writable] changes. + * + * Note: This does not inherit the subscriptions of the [Writable]. + * + * Different from [Writable.derive] itself, this has the re-render subscription which will make changes to the + * origin [Writable] also signal the system to re-render. This is intended to be used for cases where the origin + * [Writable] is not subscribed to the re-render subscription, for some reason. + * + * @param modifier the action to do to mutate the value into the desired value. + * @return a new [ReadOnly] state that is derived from the current [Writable]. + */ + fun derive(writable: Writable, modifier: Derive) = writable.derive(modifier).apply { + expand(this.writable) + } + + /** + * [ReadOnly] is a state similar to [Writable], but instead, only the getters are exposed to the public. It's a + * simple wrapper around [Writable] and is used by [Writable.derive] to enable read-only states. As it is of + * no-use to external code, [ReadOnly] can only be created internally by Nexus. + */ + class ReadOnly internal constructor(value: T) { + internal val writable = Writable(value) + internal fun set(value: T) = writable.set(value) + + /** + * Gets the value of this [Writable]. This is intended to be used for delegation. You may be looking for + * [get] instead which allows you to directly get the value. + */ + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + return writable.get() + } + + /** + * Gets the current value of the [Writable]. If you need to listen to changes to the value, + * use the [subscribe] method instead to subscribe to changes. + * + * @return the value of the [Writable]. + */ + fun get(): T = writable.get() + + /** + * Subscribes to changes to the value of the [Writable]. This is ran asynchronously after the value has + * been changed. + * + * @param subscription the task to execute upon a change to the value is detected. + * @return an [Unsubscribe] method to unsubscribe the [Subscription]. + */ + fun subscribe(subscription: Subscription): Unsubscribe = writable.subscribe(subscription) + } + /** * Writable are the equivalent to state in React.js, or [writable] in Svelte (otherwise known as `$state` in Svelte Runes), * these are simply properties that will execute subscribed tasks whenever the property changes, enabling reactivity. @@ -277,6 +330,26 @@ class React internal constructor(private val api: DiscordApi, private val render subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } } + /** + * Creates a new [ReadOnly] state that has a value derived of this [Writable], which means that the value + * of the new [ReadOnly] state changes whenever the value of the current [Writable] changes. + * + * Note: This does not inherit the subscriptions of the [Writable], which means that subscriptions such as + * re-rendering is not inherited, but it's not as needed as the value + * + * @param modifier the action to do to mutate the value into the desired value. + * @return a new [ReadOnly] state that is derived from the current [Writable]. + */ + fun derive(modifier: Derive): ReadOnly { + val currentValue = get() + val state = ReadOnly(modifier(currentValue)) + + this.subscribe { _, newValue -> + state.set(modifier(newValue)) + } + return state + } + override fun toString(): String { return _value.get().toString() } From cfc8be6e58afb11107bdfd976afb55dceb42300e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 28 Oct 2023 16:24:39 +0800 Subject: [PATCH 069/107] feat: add conveinence components to `Button` in `React` --- .../nexus/features/react/elements/Button.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt index aa6f41eb..59c6bdba 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Button.kt @@ -7,6 +7,38 @@ import org.javacord.api.listener.interaction.ButtonClickListener import pw.mihou.nexus.core.assignment.NexusUuidAssigner import pw.mihou.nexus.features.react.React +fun React.Component.PrimaryButton( + label: String, + customId: String? = null, + emoji: String? = null, + disabled: Boolean = false, + onClick: ((event: ButtonClickEvent) -> Unit)? = null +) = Button(ButtonStyle.PRIMARY, label, customId, emoji, disabled, onClick) + +fun React.Component.SecondaryButton( + label: String, + customId: String? = null, + emoji: String? = null, + disabled: Boolean = false, + onClick: ((event: ButtonClickEvent) -> Unit)? = null +) = Button(ButtonStyle.SECONDARY, label, customId, emoji, disabled, onClick) + +fun React.Component.SuccessButton( + label: String, + customId: String? = null, + emoji: String? = null, + disabled: Boolean = false, + onClick: ((event: ButtonClickEvent) -> Unit)? = null +) = Button(ButtonStyle.SUCCESS, label, customId, emoji, disabled, onClick) + +fun React.Component.DangerButton( + label: String, + customId: String? = null, + emoji: String? = null, + disabled: Boolean = false, + onClick: ((event: ButtonClickEvent) -> Unit)? = null +) = Button(ButtonStyle.DANGER, label, customId, emoji, disabled, onClick) + fun React.Component.Button( style: ButtonStyle = ButtonStyle.PRIMARY, label: String, From 178a44dfea271348faf12a66f09ad83b83c38da5 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 29 Oct 2023 11:01:37 +0800 Subject: [PATCH 070/107] feat: add `useHideButtons` hook --- .../features/react/hooks/UseHideButtons.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt diff --git a/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt b/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt new file mode 100644 index 00000000..975a8b78 --- /dev/null +++ b/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt @@ -0,0 +1,21 @@ +package pw.mihou.nexus.features.react.hooks + +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.configuration.modules.Cancellable +import pw.mihou.nexus.features.react.React +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +fun React.useHideButtons(after: Duration = 10.minutes): React.Writable { + val showButtons = writable(true) + var removeButtons: Cancellable? = null + + onRender { + removeButtons?.cancel(true) + removeButtons = Nexus.launch.scheduler.launch(after.inWholeMilliseconds) { + showButtons.set(false) + } + } + + return showButtons +} \ No newline at end of file From 227e6a76b83c1243714b68e8ec8df60ff3a4d50f Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 29 Oct 2023 11:14:35 +0800 Subject: [PATCH 071/107] feat: add hooks example --- examples/nexus.r/README.md | 4 +- examples/nexus.r/[4]_hooks/Hooks.kt | 72 +++++++++++++++++++ .../features/react/hooks/UseHideButtons.kt | 10 +++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 examples/nexus.r/[4]_hooks/Hooks.kt diff --git a/examples/nexus.r/README.md b/examples/nexus.r/README.md index ae9de65e..853855f5 100644 --- a/examples/nexus.r/README.md +++ b/examples/nexus.r/README.md @@ -27,5 +27,5 @@ are the examples we think that you should read first though: 2. [**Passing State to Another Function**](%5B1%5D_passing_state): As states are not simply the same regular Kotlin properties, there needs a little bit of a very tiny change when you want to pass state to another function outside of the `Nexus.R` scope. 3. [**Creating Components**](%5B2%5D_components): Reusing code is also important in coding, and components are just one way to reuse code -4. [**Data Fetching**](%5B3%5D_data_fetching/DataFetching.kt): Data fetching is also an important factor in many bots, this is how you can load data -before rendering the message! +4. [**Data Fetching**](%5B3%5D_data_fetching/DataFetching.kt): Data fetching is also an important factor in many bots, this is how you can load data before rending the message! +5. [**Hooks**](%5B4%5D_hooks): Hooks are vital to be able to reuse a lot of code, unlike components, hooks are not affected by rerenders and is a great place to define writables and related! \ No newline at end of file diff --git a/examples/nexus.r/[4]_hooks/Hooks.kt b/examples/nexus.r/[4]_hooks/Hooks.kt new file mode 100644 index 00000000..e4cccaa1 --- /dev/null +++ b/examples/nexus.r/[4]_hooks/Hooks.kt @@ -0,0 +1,72 @@ +import pw.mihou.nexus.Nexus +import pw.mihou.nexus.configuration.modules.Cancellable +import pw.mihou.nexus.features.command.facade.NexusCommandEvent +import pw.mihou.nexus.features.command.facade.NexusHandler +import pw.mihou.nexus.features.messages.R +import pw.mihou.nexus.features.react.React +import pw.mihou.nexus.features.react.ReactComponent +import pw.mihou.nexus.features.react.elements.Button +import pw.mihou.nexus.features.react.elements.Embed +import pw.mihou.nexus.features.react.hooks.useHideButtons +import pw.mihou.nexus.features.react.writable.plusAssign +import java.time.Instant +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +// In this example, we'll demonstrate how to create reusable hooks in Nexus.R. Hooks are simply +// just extension functions over [React] itself and isn't affected through re-renders as it lives +// under [React]. You can even use it to pass components, in this example, we'll demonstrate using it +// to pass components. + +/** + * In here, we create an extension function over [React] which returns a [Triple] that contains the `showButtons`, `clicks` and + * the actual [ReactComponent] itself which will increment the clicks every time it is clicked. + */ +fun React.useClickButton(hideAfter: Duration = 10.minutes): Triple, React.Writable, ReactComponent> { + val showButtons = writable(true) + val clicks = writable(0) + + var removeButtons: Cancellable? = null + + onRender { + removeButtons?.cancel(true) + removeButtons = Nexus.launch.scheduler.launch(hideAfter.inWholeMilliseconds) { + showButtons.set(false) + } + } + + return Triple(showButtons, clicks) { + if (showButtons.get()) { + Button(label = "Click me!") { + it.buttonInteraction.acknowledge() + clicks += 1 + } + } + } +} + +object SlashCommand: NexusHandler { + val name = "react" + val description = "Shows a demonstration of Nexus.R." + override fun onEvent(event: NexusCommandEvent) { + event.R { + val (showButtonsDelegate, clicksDelegate, ClickButton) = useClickButton(10.minutes) + + var showButtons by showButtonsDelegate + var clicks by clicksDelegate + + render { + Embed { + Title("Rendered with Nexus.R") + SpacedBody( + p("This message was rendered with Nexus.R."), + p("The button has been clicked ") + bold("$clicks times.") + ) + Color(java.awt.Color.YELLOW) + Timestamp(Instant.now()) + } + ClickButton() + } + } + } +} diff --git a/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt b/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt index 975a8b78..aa8d1000 100644 --- a/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt +++ b/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt @@ -6,6 +6,16 @@ import pw.mihou.nexus.features.react.React import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes +/** + * [useHideButtons] is a hook that will give you a little [React.Writable] that will + * turn into [false] after the given [after] timestamp. + * + * This is useful for cases where you want to remove the buttons of a response after + * a given set of time. + * + * @param after the amount of time before hiding the buttons, defaults to 10 minutes. + * @return a [React.Writable] that will change to [false] after the given time. + */ fun React.useHideButtons(after: Duration = 10.minutes): React.Writable { val showButtons = writable(true) var removeButtons: Cancellable? = null From 345515452274f3bd770a386409b550665f774b0e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 29 Oct 2023 20:52:23 +0800 Subject: [PATCH 072/107] feat: add last defense try-catches on `Nexus.R` --- .../pw/mihou/nexus/features/react/React.kt | 109 +++++++++++------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 0d50cc47..677c4306 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -90,34 +90,38 @@ class React internal constructor(private val api: DiscordApi, private val render * @param component the component to render. */ fun render(component: ReactComponent) { - val element = apply(component) - - when (renderMode) { - RenderMode.Interaction -> { - val (unsubscribe, message) = element.render(api) - this.message = message - this.unsubscribe = unsubscribe - } + try { + val element = apply(component) + + when (renderMode) { + RenderMode.Interaction -> { + val (unsubscribe, message) = element.render(api) + this.message = message + this.unsubscribe = unsubscribe + } - RenderMode.Message -> { - val builder = MessageBuilder() - val unsubscribe = element.render(builder, api) + RenderMode.Message -> { + val builder = MessageBuilder() + val unsubscribe = element.render(builder, api) - this.messageBuilder = builder - this.unsubscribe = unsubscribe - } - RenderMode.UpdateMessage -> { - if (resultingMessage == null) { - throw IllegalStateException("Updating a message with React needs `resultingMessage` to not be null.") + this.messageBuilder = builder + this.unsubscribe = unsubscribe } - val updater = MessageUpdater(resultingMessage) - val unsubscribe = element.render(updater, api) + RenderMode.UpdateMessage -> { + if (resultingMessage == null) { + throw IllegalStateException("Updating a message with React needs `resultingMessage` to not be null.") + } + val updater = MessageUpdater(resultingMessage) + val unsubscribe = element.render(updater, api) - this.messageUpdater = updater - this.unsubscribe = unsubscribe + this.messageUpdater = updater + this.unsubscribe = unsubscribe + } } + this.rendered = true + } catch (err: Exception) { + Nexus.logger.error("An uncaught exception was received by Nexus.R's renderer with the following stacktrace.", err) } - this.rendered = true } private fun apply(component: Component.() -> Unit): Component { @@ -125,10 +129,22 @@ class React internal constructor(private val api: DiscordApi, private val render val element = Component() if (!rendered) { - firstRenderSubscribers.forEach { it() } + firstRenderSubscribers.forEach { + try { + it() + } catch (err: Exception) { + Nexus.logger.error("An uncaught exception was received by Nexus.R's initial render subscription dispatcher with the following stacktrace.", err) + } + } } - renderSubscribers.forEach { it() } + renderSubscribers.forEach { + try { + it() + } catch (err: Exception) { + Nexus.logger.error("An uncaught exception was received by Nexus.R's render subscription dispatcher with the following stacktrace.", err) + } + } component(element) return element @@ -162,23 +178,30 @@ class React internal constructor(private val api: DiscordApi, private val render */ fun expand(writable: Writable): Writable { writable.subscribe { _, _ -> - if (!mutex.tryLock()) return@subscribe - val component = this.component ?: return@subscribe - debounceTask?.cancel(false) - debounceTask = Nexus.launch.scheduler.launch(debounceMillis) { - this.unsubscribe() - - debounceTask = null - - val message = resultingMessage - if (message != null) { - val updater = message.createUpdater() - val view = apply(component) - this.unsubscribe = view.render(updater, api) - updater.replaceMessage() + try { + if (!mutex.tryLock()) return@subscribe + val component = this.component ?: return@subscribe + debounceTask?.cancel(false) + debounceTask = Nexus.launch.scheduler.launch(debounceMillis) { + this.unsubscribe() + + debounceTask = null + + val message = resultingMessage + if (message != null) { + val updater = message.createUpdater() + val view = apply(component) + this.unsubscribe = view.render(updater, api) + updater.replaceMessage().exceptionally { + Nexus.logger.error("Failed to re-render message using Nexus.R with the following stacktrace.", it) + return@exceptionally null + } + } } + mutex.unlock() + } catch (err: Exception) { + Nexus.logger.error("Failed to re-render message using Nexus.R with the following stacktrace.", err) } - mutex.unlock() } return writable } @@ -327,7 +350,13 @@ class React internal constructor(private val api: DiscordApi, private val render * @param value the current value. */ internal fun react(oldValue: T, value: T) { - subscribers.forEach { Nexus.launcher.launch { it(oldValue, value) } } + subscribers.forEach { Nexus.launcher.launch { + try { + it(oldValue, value) + } catch (err: Exception) { + Nexus.logger.error("An uncaught exception was received by Nexus.R's writable subscriptions with the following stacktrace.", err) + } + } } } /** From 3c2cda5d73029343b17b8f849aa395e02f030020 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 14:54:31 +0800 Subject: [PATCH 073/107] feat: remove pre-built hooks. Nexus' main library should be used to maintain the main functionality over extending it with additional things. --- .../features/react/hooks/UseHideButtons.kt | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt diff --git a/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt b/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt deleted file mode 100644 index aa8d1000..00000000 --- a/src/main/java/pw/mihou/nexus/features/react/hooks/UseHideButtons.kt +++ /dev/null @@ -1,31 +0,0 @@ -package pw.mihou.nexus.features.react.hooks - -import pw.mihou.nexus.Nexus -import pw.mihou.nexus.configuration.modules.Cancellable -import pw.mihou.nexus.features.react.React -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - -/** - * [useHideButtons] is a hook that will give you a little [React.Writable] that will - * turn into [false] after the given [after] timestamp. - * - * This is useful for cases where you want to remove the buttons of a response after - * a given set of time. - * - * @param after the amount of time before hiding the buttons, defaults to 10 minutes. - * @return a [React.Writable] that will change to [false] after the given time. - */ -fun React.useHideButtons(after: Duration = 10.minutes): React.Writable { - val showButtons = writable(true) - var removeButtons: Cancellable? = null - - onRender { - removeButtons?.cancel(true) - removeButtons = Nexus.launch.scheduler.launch(after.inWholeMilliseconds) { - showButtons.set(false) - } - } - - return showButtons -} \ No newline at end of file From b4a4893725bc9fefac004241b5b1e6c4154f05c7 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 31 Oct 2023 16:01:21 +0800 Subject: [PATCH 074/107] feat: add `addons` folder containing all other libraries for Nexus --- .gitmodules | 3 +++ addons/README.md | 14 ++++++++++++++ addons/nexus.entour | 1 + 3 files changed, 18 insertions(+) create mode 100644 .gitmodules create mode 100644 addons/README.md create mode 160000 addons/nexus.entour diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..65d0287d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "addons/nexus.entour"] + path = addons/nexus.entour + url = https://github.com/ShindouMihou/nexus.entour diff --git a/addons/README.md b/addons/README.md new file mode 100644 index 00000000..3789b44b --- /dev/null +++ b/addons/README.md @@ -0,0 +1,14 @@ +# Addons + +In order to keep the repository specific to the actual functionalities of the Nexus features, +other cool things that would be nice to have in Nexus, such as confirmation menus, etc. are stored into different +repositories and linked here through Git Submodule for others to see. + +Here are the list of each different add-on that extends upon Nexus' features: +- [`entour`](nexus.entour) is a set of handy built-in components, hooks and tools for Nexus.R. This contains many +different things for develoeprs who uses Nexus.R such as Confirmation Menus. + +### Contributing + +If you want to add your repository into this list, feel free to create a Issue stating the name, repository link and +the description for your repository. \ No newline at end of file diff --git a/addons/nexus.entour b/addons/nexus.entour new file mode 160000 index 00000000..9b2d802a --- /dev/null +++ b/addons/nexus.entour @@ -0,0 +1 @@ +Subproject commit 9b2d802aabe74a684b1b991535653ebaff8b9d8f From 570d9e6782431ecb9f98eab31031804d60875615 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 17:07:32 +0800 Subject: [PATCH 075/107] feat: add `onUpdate` support for `React` --- .../nexus/features/commons/Deferrable.kt | 5 +-- .../features/messages/ReactExtensions.kt | 31 ++++++++++------- .../pw/mihou/nexus/features/react/React.kt | 33 ++++++++++++++++++- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt index d99b41d4..14ff1928 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt @@ -86,7 +86,7 @@ object Deferrable { * @return the response from Discord. */ fun Interaction.autoDefer(ephemeral: Boolean, response: Function): CompletableFuture = - Deferrable.autoDefer(this, AtomicReference(null), ephemeral, response) + Deferrable.autoDefer(this, AtomicReference(null), ephemeral, response) /** * An experimental feature to use the new Nexus.R rendering mechanism to render Discord messages @@ -107,7 +107,8 @@ fun Interaction.R(ephemeral: Boolean, react: Reac return@autoDefer r.message!! }.thenApply { - r.resultingMessage = it.getOrRequestMessage().join() + val message = it.getOrRequestMessage().join() + r.acknowledgeUpdate(message) return@thenApply it } } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index a292ddcf..a64b1325 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -7,6 +7,19 @@ import org.javacord.api.event.message.CertainMessageEvent import pw.mihou.nexus.features.react.React import java.util.concurrent.CompletableFuture +/** + * An internal extension that acknowledges the message result and calls [React.acknowledgeUpdate]. + * This is used to enable `onUpdate` calls to render. + * + * @param react the [React] instance. + */ +private fun CompletableFuture.ack(react: React): CompletableFuture { + return this.thenApply { message -> + react.acknowledgeUpdate(message) + return@thenApply message + } +} + /** * An experimental feature to use the new Nexus.R rendering mechanism to render Discord messages * with a syntax similar to a template engine that sports states (writable) that can easily update message @@ -19,10 +32,10 @@ fun CertainMessageEvent.R(react: React.() -> Unit): CompletableFuture { val r = React(this.api, React.RenderMode.Message) react(r) - return r.messageBuilder!!.replyTo(message).send(channel).thenApply { - r.resultingMessage = it - return@thenApply it - } + return r.messageBuilder!! + .replyTo(message) + .send(channel) + .ack(r) } /** @@ -37,10 +50,7 @@ fun Messageable.R(api: DiscordApi, react: React.() -> Unit): CompletableFuture Unit): CompletableFuture { react(r) r.resultingMessage = this - return r.messageUpdater!!.replaceMessage().thenApply { - r.resultingMessage = it - return@thenApply it - } + return r.messageUpdater!!.replaceMessage().ack(r) } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 677c4306..8c91befd 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -20,6 +20,7 @@ typealias Subscription = (oldValue: T, newValue: T) -> Unit typealias Unsubscribe = () -> Unit typealias RenderSubscription = () -> Unit +typealias UpdateSubscription = (message: Message) -> Unit typealias ReactComponent = React.Component.() -> Unit typealias Derive = (T) -> K @@ -47,6 +48,8 @@ class React internal constructor(private val api: DiscordApi, private val render private var firstRenderSubscribers = mutableListOf() private var renderSubscribers = mutableListOf() + private var updateSubscribers = mutableListOf() + companion object { /** * Defines how long we should wait before proceeding to re-render the component, this is intended to ensure @@ -84,6 +87,34 @@ class React internal constructor(private val api: DiscordApi, private val render firstRenderSubscribers.add(subscription) } + /** + * Subscribes a task to be ran whenever the message itself updates, this can be when a re-render + * is successfully acknowledged by Discord, or when the message was sent for the first time. + * + * Not to be confused with [onRender] which executes before the message is updated and even before + * the message is rendered. + * + * @param subscription the subscription to execute on update. + */ + fun onUpdate(subscription: UpdateSubscription) { + updateSubscribers.add(subscription) + } + + /** + * An internal function to update the [resultingMessage] and run all the [updateSubscribers]. + * @param message the message resulting from a render. + */ + internal fun acknowledgeUpdate(message: Message) { + this.resultingMessage = message + updateSubscribers.forEach { + try { + it(message) + } catch (err: Exception) { + Nexus.logger.error("An uncaught exception was received by Nexus.R's update subscription dispatcher with the following stacktrace.", err) + } + } + } + /** * Renders the given component, this will also be used to re-render the component onwards. Note that using two * renders will result in the last executed render being used. @@ -195,7 +226,7 @@ class React internal constructor(private val api: DiscordApi, private val render updater.replaceMessage().exceptionally { Nexus.logger.error("Failed to re-render message using Nexus.R with the following stacktrace.", it) return@exceptionally null - } + }.thenAccept(::acknowledgeUpdate) } } mutex.unlock() From e06c6db0934b137acb8572e66ef07c5fd36a3bdf Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Wed, 1 Nov 2023 17:12:04 +0800 Subject: [PATCH 076/107] feat: allow use of `infix` in `Writable` --- src/main/java/pw/mihou/nexus/features/react/React.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 8c91befd..5c265021 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -286,7 +286,7 @@ class React internal constructor(private val api: DiscordApi, private val render * @param subscription the task to execute upon a change to the value is detected. * @return an [Unsubscribe] method to unsubscribe the [Subscription]. */ - fun subscribe(subscription: Subscription): Unsubscribe = writable.subscribe(subscription) + infix fun subscribe(subscription: Subscription): Unsubscribe = writable.subscribe(subscription) } /** @@ -330,7 +330,7 @@ class React internal constructor(private val api: DiscordApi, private val render * * @param value the new value of the [Writable]. */ - fun set(value: T) { + infix fun set(value: T) { val oldValue = _value.get() _value.set(value) @@ -345,7 +345,7 @@ class React internal constructor(private val api: DiscordApi, private val render * Similar to [set], this executes all the subscriptions asynchronously. * @param updater the updater to update the value of the [Writable]. */ - fun update(updater: (T) -> T) { + infix fun update(updater: (T) -> T) { val oldValue = _value.get() _value.getAndUpdate(updater) @@ -368,7 +368,7 @@ class React internal constructor(private val api: DiscordApi, private val render * @param subscription the task to execute upon a change to the value is detected. * @return an [Unsubscribe] method to unsubscribe the [Subscription]. */ - fun subscribe(subscription: Subscription): Unsubscribe { + infix fun subscribe(subscription: Subscription): Unsubscribe { subscribers.add(subscription) return { subscribers.remove(subscription) } } @@ -400,7 +400,7 @@ class React internal constructor(private val api: DiscordApi, private val render * @param modifier the action to do to mutate the value into the desired value. * @return a new [ReadOnly] state that is derived from the current [Writable]. */ - fun derive(modifier: Derive): ReadOnly { + infix fun derive(modifier: Derive): ReadOnly { val currentValue = get() val state = ReadOnly(modifier(currentValue)) From 01f919e98ba6f3acf5a4b39f468445125d25dcb0 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 23 Nov 2023 09:56:12 +0800 Subject: [PATCH 077/107] feat: fix `ephemeral` responses not being updated in Nexus.R --- .../command/responses/NexusAutoResponse.kt | 4 +-- .../nexus/features/commons/Deferrable.kt | 5 ++- .../pw/mihou/nexus/features/react/React.kt | 36 ++++++++++++++++--- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/responses/NexusAutoResponse.kt b/src/main/java/pw/mihou/nexus/features/command/responses/NexusAutoResponse.kt index 51815934..0d78e401 100644 --- a/src/main/java/pw/mihou/nexus/features/command/responses/NexusAutoResponse.kt +++ b/src/main/java/pw/mihou/nexus/features/command/responses/NexusAutoResponse.kt @@ -5,10 +5,10 @@ import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater import java.util.concurrent.CompletableFuture data class NexusAutoResponse internal constructor( - val updater: InteractionOriginalResponseUpdater?, + val updater: InteractionOriginalResponseUpdater, val message: Message? ) { fun getOrRequestMessage(): CompletableFuture = - if (message == null && updater != null) updater.update() + if (message == null) updater.update() else CompletableFuture.completedFuture(message) } diff --git a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt index 14ff1928..2846c31c 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt @@ -59,7 +59,7 @@ object Deferrable { } else { val completedUpdater = updater.join() message.into(completedUpdater).update() - .thenAccept { r -> future.complete(NexusAutoResponse(null, r)) } + .thenAccept { r -> future.complete(NexusAutoResponse(completedUpdater, r)) } .exceptionally { exception -> future.completeExceptionally(exception) return@exceptionally null @@ -108,7 +108,10 @@ fun Interaction.R(ephemeral: Boolean, react: Reac return@autoDefer r.message!! }.thenApply { val message = it.getOrRequestMessage().join() + + r.interactionUpdater = it.updater r.acknowledgeUpdate(message) + return@thenApply it } } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 5c265021..7ef168c1 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -7,6 +7,7 @@ import org.javacord.api.entity.message.MessageUpdater import org.javacord.api.entity.message.component.ActionRow import org.javacord.api.entity.message.component.LowLevelComponent import org.javacord.api.entity.message.embed.EmbedBuilder +import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater import org.javacord.api.listener.GloballyAttachableListener import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable @@ -36,6 +37,7 @@ class React internal constructor(private val api: DiscordApi, private val render internal var message: NexusMessage? = null internal var messageBuilder: MessageBuilder? = null internal var messageUpdater: MessageUpdater? = null + internal var interactionUpdater: InteractionOriginalResponseUpdater? = null private var unsubscribe: Unsubscribe = {} private var component: (Component.() -> Unit)? = null @@ -218,15 +220,25 @@ class React internal constructor(private val api: DiscordApi, private val render debounceTask = null - val message = resultingMessage - if (message != null) { - val updater = message.createUpdater() + val interactionUpdater = interactionUpdater + if (interactionUpdater != null) { val view = apply(component) - this.unsubscribe = view.render(updater, api) - updater.replaceMessage().exceptionally { + this.unsubscribe = view.render(interactionUpdater, api) + interactionUpdater.update().exceptionally { Nexus.logger.error("Failed to re-render message using Nexus.R with the following stacktrace.", it) return@exceptionally null }.thenAccept(::acknowledgeUpdate) + } else { + val message = resultingMessage + if (message != null) { + val updater = message.createUpdater() + val view = apply(component) + this.unsubscribe = view.render(updater, api) + updater.replaceMessage().exceptionally { + Nexus.logger.error("Failed to re-render message using Nexus.R with the following stacktrace.", it) + return@exceptionally null + }.thenAccept(::acknowledgeUpdate) + } } } mutex.unlock() @@ -516,5 +528,19 @@ class React internal constructor(private val api: DiscordApi, private val render } return attachListeners(api) } + + + fun render(updater: InteractionOriginalResponseUpdater, api: DiscordApi): Unsubscribe{ + updater.apply { + this.removeAllEmbeds() + this.addEmbeds(embeds) + + if (contents != null) { + this.setContent(contents) + } + chunkComponents().forEach { this.addComponents(it) } + } + return attachListeners(api) + } } } \ No newline at end of file From 2a12e85ee0b836117435287315a58c061d6f99a9 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 23 Nov 2023 09:57:18 +0800 Subject: [PATCH 078/107] feat: add Flyght to the bots using Nexus --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c1f0e0c8..7b087fe5 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Nexus is used in production by Discord bots, such as: - [Beemo](https://beemo.gg): An anti-raid Discord bot that prevents raids on many large servers. - [Amelia-chan](https://github.com/Amelia-chan/Amelia): A simple RSS Discord bot for the novel site, ScribbleHub. - [Threadscore](https://threadscore.mihou.pw): Gamifying Q&A for Discord. +- [Flyght](https://flyght.mihou.pw): Adds Know-Your-Member to Discord. Powered by [Nexus.R](https://github.com/ShindouMihou/Nexus/wiki/Nexus.R/). If you want to add your bot to the list, feel free to add it by creating a pull request! From db2b2eac6fe24a2c8f994ef6b40a369354dd87a3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 23 Nov 2023 10:07:35 +0800 Subject: [PATCH 079/107] feat: remove all components when re-rendering with `InteractionOriginalResponseUpdater` --- src/main/java/pw/mihou/nexus/features/react/React.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 7ef168c1..d7ebcf5e 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -533,6 +533,8 @@ class React internal constructor(private val api: DiscordApi, private val render fun render(updater: InteractionOriginalResponseUpdater, api: DiscordApi): Unsubscribe{ updater.apply { this.removeAllEmbeds() + this.removeAllComponents() + this.addEmbeds(embeds) if (contents != null) { From 9bbb0d2999015ff08816166985fa6438a9d40de2 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 24 Nov 2023 18:17:54 +0800 Subject: [PATCH 080/107] feat: add builder option to `SelectMenu.Option` in React --- .../pw/mihou/nexus/features/react/elements/SelectMenu.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt b/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt index 59ed48ab..d0f9c91b 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt @@ -4,6 +4,7 @@ import org.javacord.api.entity.channel.ChannelType import org.javacord.api.entity.message.component.ComponentType import org.javacord.api.entity.message.component.SelectMenuBuilder import org.javacord.api.entity.message.component.SelectMenuOption +import org.javacord.api.entity.message.component.SelectMenuOptionBuilder import org.javacord.api.event.interaction.SelectMenuChooseEvent import org.javacord.api.listener.interaction.SelectMenuChooseListener import pw.mihou.nexus.core.assignment.NexusUuidAssigner @@ -47,6 +48,12 @@ class SelectMenu(internal val selectMenu: SelectMenuBuilder) { selectMenu.addOption(option) } + fun Option(builder: SelectMenuOptionBuilder.() -> Unit) { + val original = SelectMenuOptionBuilder() + builder(original) + selectMenu.addOption(original.build()) + } + fun Placeholder(text: String) { selectMenu.setPlaceholder(text) } From 6caac300675b28cbdca561b0d27cf39209a574fd Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 25 Nov 2023 08:56:37 +0800 Subject: [PATCH 081/107] feat: add support for clearing unneeeded references to React. --- .../nexus/features/commons/Deferrable.kt | 8 +++-- .../features/commons/NexusInteractionEvent.kt | 7 ++-- .../pw/mihou/nexus/features/react/React.kt | 36 +++++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt index 2846c31c..465c2ebf 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/Deferrable.kt @@ -15,6 +15,9 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import java.util.function.Function +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours object Deferrable { internal fun autoDefer( @@ -97,11 +100,12 @@ fun Interaction.autoDefer(ephemeral: Boolean, res * and others that may happen due to situations such as data fetching, etc. * * @param ephemeral whether to send the response as ephemeral or not. + * @param lifetime indicates how long before the [React] instance self-destructs to free up references. * @param react the entire procedure over how rendering the response works. */ @JvmSynthetic -fun Interaction.R(ephemeral: Boolean, react: React.() -> Unit): CompletableFuture { - val r = React(this.api, React.RenderMode.Interaction) +fun Interaction.R(ephemeral: Boolean, lifetime: Duration = 1.hours, react: React.() -> Unit): CompletableFuture { + val r = React(this.api, React.RenderMode.Interaction, lifetime) return autoDefer(ephemeral) { react(r) diff --git a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt index 9dfb0445..75bf45ef 100644 --- a/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt +++ b/src/main/java/pw/mihou/nexus/features/commons/NexusInteractionEvent.kt @@ -20,6 +20,8 @@ import java.util.concurrent.CompletableFuture import java.util.function.Function import java.util.function.Predicate import kotlin.NoSuchElementException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours @JvmDefaultWithCompatibility interface NexusInteractionEvent { @@ -205,9 +207,10 @@ interface NexusInteractionEvent Unit): CompletableFuture = - interaction.R(ephemeral, react) + fun R(ephemeral: Boolean = false, lifetime: Duration = 1.hours, react: React.() -> Unit): CompletableFuture = + interaction.R(ephemeral, lifetime, react) } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index d7ebcf5e..31a9ba7a 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -16,6 +16,9 @@ import pw.mihou.nexus.features.messages.NexusMessage import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock import kotlin.reflect.KProperty +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours typealias Subscription = (oldValue: T, newValue: T) -> Unit typealias Unsubscribe = () -> Unit @@ -31,7 +34,7 @@ typealias Derive = (T) -> K * `event.R` method instead as it is mostly designed to enable this to work for your situation, or instead use the * available `interaction.R` method for interactions. */ -class React internal constructor(private val api: DiscordApi, private val renderMode: RenderMode) { +class React internal constructor(private val api: DiscordApi, private val renderMode: RenderMode, lifetime: Duration = 1.hours) { private var rendered: Boolean = false internal var message: NexusMessage? = null @@ -51,6 +54,11 @@ class React internal constructor(private val api: DiscordApi, private val render private var renderSubscribers = mutableListOf() private var updateSubscribers = mutableListOf() + private var expansions = mutableListOf() + + private var destroyJob: Cancellable? = Nexus.launch.scheduler.launch(lifetime.inWholeMilliseconds) { + destroy() + } companion object { /** @@ -117,6 +125,27 @@ class React internal constructor(private val api: DiscordApi, private val render } } + /** + * Destroys any references to this [React] instance. It is recommended to do this when + * you no longer need the interactivity as this will free up a ton of unused memory that should've + * been free. + */ + fun destroy() { + unsubscribe() + component = null + this.resultingMessage = null + this.interactionUpdater = null + this.updateSubscribers = mutableListOf() + this.messageUpdater = null + this.messageBuilder = null + this.renderSubscribers = mutableListOf() + this.message = null + this.debounceTask = null + this.destroyJob = null + this.expansions.forEach(Unsubscribe::invoke) + this.expansions = mutableListOf() + } + /** * Renders the given component, this will also be used to re-render the component onwards. Note that using two * renders will result in the last executed render being used. @@ -210,7 +239,7 @@ class React internal constructor(private val api: DiscordApi, private val render * @return the [Writable] with the re-render subscription attached. */ fun expand(writable: Writable): Writable { - writable.subscribe { _, _ -> + val stateUnsubscribe = writable.subscribe { _, _ -> try { if (!mutex.tryLock()) return@subscribe val component = this.component ?: return@subscribe @@ -219,7 +248,7 @@ class React internal constructor(private val api: DiscordApi, private val render this.unsubscribe() debounceTask = null - + if (this.component == null) return@launch val interactionUpdater = interactionUpdater if (interactionUpdater != null) { val view = apply(component) @@ -246,6 +275,7 @@ class React internal constructor(private val api: DiscordApi, private val render Nexus.logger.error("Failed to re-render message using Nexus.R with the following stacktrace.", err) } } + expansions.add(stateUnsubscribe) return writable } From f02cbb60fdb30a940cffd8013fc250c1097d8c50 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 25 Nov 2023 09:13:26 +0800 Subject: [PATCH 082/107] feat: add destroy subscriptions --- .../java/pw/mihou/nexus/features/react/React.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 31a9ba7a..9f1b3727 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -24,6 +24,7 @@ typealias Subscription = (oldValue: T, newValue: T) -> Unit typealias Unsubscribe = () -> Unit typealias RenderSubscription = () -> Unit +typealias DestroySubscription = () -> Unit typealias UpdateSubscription = (message: Message) -> Unit typealias ReactComponent = React.Component.() -> Unit typealias Derive = (T) -> K @@ -60,6 +61,8 @@ class React internal constructor(private val api: DiscordApi, private val render destroy() } + private var destroySubscribers = mutableListOf() + companion object { /** * Defines how long we should wait before proceeding to re-render the component, this is intended to ensure @@ -110,6 +113,17 @@ class React internal constructor(private val api: DiscordApi, private val render updateSubscribers.add(subscription) } + /** + * Subscribes a task to be ran when the [React] instance is destroyed. This can be when the lifetime + * ends or the message is deleted. This is ran all synchronous and will block the destruction thread + * until it completes. + * + * @param subscription the subscription to execute. + */ + fun onDestroy(subscription: DestroySubscription) { + destroySubscribers.add(subscription) + } + /** * An internal function to update the [resultingMessage] and run all the [updateSubscribers]. * @param message the message resulting from a render. @@ -131,6 +145,8 @@ class React internal constructor(private val api: DiscordApi, private val render * been free. */ fun destroy() { + destroySubscribers.forEach(DestroySubscription::invoke) + unsubscribe() component = null this.resultingMessage = null From f196f8de62dc866331c35db80c7376a058c4c0ec Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 25 Nov 2023 09:16:58 +0800 Subject: [PATCH 083/107] feat: destroy on message delete --- .../pw/mihou/nexus/features/messages/ReactExtensions.kt | 2 +- src/main/java/pw/mihou/nexus/features/react/React.kt | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index a64b1325..123e5fcc 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -65,6 +65,6 @@ fun Message.R(react: React.() -> Unit): CompletableFuture { val r = React(api, React.RenderMode.Message) react(r) - r.resultingMessage = this + r.acknowledgeUpdate(this) return r.messageUpdater!!.replaceMessage().ack(r) } \ No newline at end of file diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 9f1b3727..9dc744f6 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -9,6 +9,8 @@ import org.javacord.api.entity.message.component.LowLevelComponent import org.javacord.api.entity.message.embed.EmbedBuilder import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater import org.javacord.api.listener.GloballyAttachableListener +import org.javacord.api.listener.message.MessageDeleteListener +import org.javacord.api.util.event.ListenerManager import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.core.assignment.NexusUuidAssigner @@ -61,6 +63,7 @@ class React internal constructor(private val api: DiscordApi, private val render destroy() } + private var messageDeleteListenerManager: ListenerManager? = null private var destroySubscribers = mutableListOf() companion object { @@ -129,7 +132,10 @@ class React internal constructor(private val api: DiscordApi, private val render * @param message the message resulting from a render. */ internal fun acknowledgeUpdate(message: Message) { - this.resultingMessage = message + this.resultingMessage = message + messageDeleteListenerManager = this.resultingMessage?.addMessageDeleteListener { + this.destroy() + } updateSubscribers.forEach { try { it(message) @@ -160,6 +166,7 @@ class React internal constructor(private val api: DiscordApi, private val render this.destroyJob = null this.expansions.forEach(Unsubscribe::invoke) this.expansions = mutableListOf() + this.messageDeleteListenerManager?.remove() } /** From bcfeeaa9d0453d8a10d816b3eac2d8faba1c3f0e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 25 Nov 2023 09:20:14 +0800 Subject: [PATCH 084/107] feat: fix failed compilation --- .../nexus/features/command/core/NexusMiddlewareEventCore.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt index 2ec96b38..8c9d790a 100644 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt @@ -11,12 +11,14 @@ import pw.mihou.nexus.features.command.responses.NexusAutoResponse import pw.mihou.nexus.features.messages.NexusMessage import java.util.concurrent.CompletableFuture import java.util.function.Function +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours class NexusMiddlewareEventCore(private val _event: NexusCommandEvent, private val gate: NexusMiddlewareGateCore): NexusMiddlewareEvent { override val event: SlashCommandCreateEvent get() = _event.event override val command: NexusCommand get() = _event.command - override fun R(ephemeral: Boolean, react: React.() -> Unit): CompletableFuture { - return _event.R(ephemeral, react) + override fun R(ephemeral: Boolean, lifetime: Duration = 1.hours, react: React.() -> Unit): CompletableFuture { + return _event.R(ephemeral, lifetime, react) } override fun store(): MutableMap = _event.store() From 0f0146674c7f166f077d47e6eac335ee3553891d Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 25 Nov 2023 09:22:43 +0800 Subject: [PATCH 085/107] feat: fix failed compilation due to overriding function having default values --- .../nexus/features/command/core/NexusMiddlewareEventCore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt index 8c9d790a..75be4b4f 100644 --- a/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt +++ b/src/main/java/pw/mihou/nexus/features/command/core/NexusMiddlewareEventCore.kt @@ -17,7 +17,7 @@ import kotlin.time.Duration.Companion.hours class NexusMiddlewareEventCore(private val _event: NexusCommandEvent, private val gate: NexusMiddlewareGateCore): NexusMiddlewareEvent { override val event: SlashCommandCreateEvent get() = _event.event override val command: NexusCommand get() = _event.command - override fun R(ephemeral: Boolean, lifetime: Duration = 1.hours, react: React.() -> Unit): CompletableFuture { + override fun R(ephemeral: Boolean, lifetime: Duration, react: React.() -> Unit): CompletableFuture { return _event.R(ephemeral, lifetime, react) } From 0d3569fc2d39b5cab31ce14d37cd2c53dbf79fa3 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 25 Nov 2023 09:44:13 +0800 Subject: [PATCH 086/107] feat: add `lifetime` to message-based event react --- .../nexus/features/messages/ReactExtensions.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt index 123e5fcc..9839ad18 100644 --- a/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt +++ b/src/main/java/pw/mihou/nexus/features/messages/ReactExtensions.kt @@ -6,6 +6,8 @@ import org.javacord.api.entity.message.Messageable import org.javacord.api.event.message.CertainMessageEvent import pw.mihou.nexus.features.react.React import java.util.concurrent.CompletableFuture +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours /** * An internal extension that acknowledges the message result and calls [React.acknowledgeUpdate]. @@ -25,11 +27,12 @@ private fun CompletableFuture.ack(react: React): CompletableFuture Unit): CompletableFuture { - val r = React(this.api, React.RenderMode.Message) +fun CertainMessageEvent.R(lifetime: Duration = 1.hours, react: React.() -> Unit): CompletableFuture { + val r = React(this.api, React.RenderMode.Message, lifetime) react(r) return r.messageBuilder!! @@ -43,11 +46,12 @@ fun CertainMessageEvent.R(react: React.() -> Unit): CompletableFuture { * with a syntax similar to a template engine that sports states (writable) that can easily update message * upon state changes. * + * @param lifetime the lifetime it takes before the [React] destroys itself. * @param react the entire procedure over how rendering the response works. */ @JvmSynthetic -fun Messageable.R(api: DiscordApi, react: React.() -> Unit): CompletableFuture { - val r = React(api, React.RenderMode.Message) +fun Messageable.R(api: DiscordApi, lifetime: Duration = 1.hours, react: React.() -> Unit): CompletableFuture { + val r = React(api, React.RenderMode.Message, lifetime) react(r) return r.messageBuilder!!.send(this).ack(r) @@ -59,10 +63,11 @@ fun Messageable.R(api: DiscordApi, react: React.() -> Unit): CompletableFuture Unit): CompletableFuture { - val r = React(api, React.RenderMode.Message) +fun Message.R(lifetime: Duration = 1.hours, react: React.() -> Unit): CompletableFuture { + val r = React(api, React.RenderMode.Message, lifetime) react(r) r.acknowledgeUpdate(this) From 59775fd0af4aa8b94468af07b9540c65f0ec87f1 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 28 Nov 2023 10:54:13 +0800 Subject: [PATCH 087/107] feat: clear unsubscribe and lock `destroySubscribers` --- .../pw/mihou/nexus/features/react/React.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 9dc744f6..9e9e4e23 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -19,7 +19,6 @@ import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock import kotlin.reflect.KProperty import kotlin.time.Duration -import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours typealias Subscription = (oldValue: T, newValue: T) -> Unit @@ -151,22 +150,26 @@ class React internal constructor(private val api: DiscordApi, private val render * been free. */ fun destroy() { - destroySubscribers.forEach(DestroySubscription::invoke) - - unsubscribe() - component = null - this.resultingMessage = null - this.interactionUpdater = null - this.updateSubscribers = mutableListOf() - this.messageUpdater = null - this.messageBuilder = null - this.renderSubscribers = mutableListOf() - this.message = null - this.debounceTask = null - this.destroyJob = null - this.expansions.forEach(Unsubscribe::invoke) - this.expansions = mutableListOf() - this.messageDeleteListenerManager?.remove() + synchronized(destroySubscribers) { + destroySubscribers.forEach(DestroySubscription::invoke) + + unsubscribe() + component = null + this.unsubscribe = {} + this.destroySubscribers = mutableListOf() + this.resultingMessage = null + this.interactionUpdater = null + this.updateSubscribers = mutableListOf() + this.messageUpdater = null + this.messageBuilder = null + this.renderSubscribers = mutableListOf() + this.message = null + this.debounceTask = null + this.destroyJob = null + this.expansions.forEach(Unsubscribe::invoke) + this.expansions = mutableListOf() + this.messageDeleteListenerManager?.remove() + } } /** From 2ab7bc9794666d96aeb03cfa71d77e8a0d508c21 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Tue, 28 Nov 2023 11:00:59 +0800 Subject: [PATCH 088/107] feat: destroy `messageDeleteListenerManager` when destroy is called --- src/main/java/pw/mihou/nexus/features/react/React.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 9e9e4e23..c6353194 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -169,6 +169,7 @@ class React internal constructor(private val api: DiscordApi, private val render this.expansions.forEach(Unsubscribe::invoke) this.expansions = mutableListOf() this.messageDeleteListenerManager?.remove() + this.messageDeleteListenerManager = null } } From e9ec97ffa46e9981191be9a8b1400dc66d7a6de9 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Thu, 30 Nov 2023 10:49:43 +0800 Subject: [PATCH 089/107] feat: further more fixes to the lifetime of `React` --- .../java/pw/mihou/nexus/features/react/React.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index c6353194..7a6797c7 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -15,6 +15,8 @@ import pw.mihou.nexus.Nexus import pw.mihou.nexus.configuration.modules.Cancellable import pw.mihou.nexus.core.assignment.NexusUuidAssigner import pw.mihou.nexus.features.messages.NexusMessage +import java.time.Instant +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock import kotlin.reflect.KProperty @@ -44,6 +46,11 @@ class React internal constructor(private val api: DiscordApi, private val render internal var messageUpdater: MessageUpdater? = null internal var interactionUpdater: InteractionOriginalResponseUpdater? = null + // Used for debugging purposes + // TODO: Remove once we've identified memory consumption issues. + @Suppress("UNUSED", "PrivatePropertyName") + private val `react$creationDate` = Instant.now() + private var unsubscribe: Unsubscribe = {} private var component: (Component.() -> Unit)? = null @@ -132,8 +139,11 @@ class React internal constructor(private val api: DiscordApi, private val render */ internal fun acknowledgeUpdate(message: Message) { this.resultingMessage = message - messageDeleteListenerManager = this.resultingMessage?.addMessageDeleteListener { - this.destroy() + messageDeleteListenerManager?.remove() + messageDeleteListenerManager = this.resultingMessage?.run { + api.addMessageDeleteListener { + destroy() + } } updateSubscribers.forEach { try { From d2bbef4b424a962818330d5a7abb2ba01aa78869 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 20:39:01 +0800 Subject: [PATCH 090/107] feat: remove debugging property --- src/main/java/pw/mihou/nexus/features/react/React.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 7a6797c7..4d80c86a 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -46,11 +46,6 @@ class React internal constructor(private val api: DiscordApi, private val render internal var messageUpdater: MessageUpdater? = null internal var interactionUpdater: InteractionOriginalResponseUpdater? = null - // Used for debugging purposes - // TODO: Remove once we've identified memory consumption issues. - @Suppress("UNUSED", "PrivatePropertyName") - private val `react$creationDate` = Instant.now() - private var unsubscribe: Unsubscribe = {} private var component: (Component.() -> Unit)? = null From 11769c5507eeb5fb11a43c23d76d3d1ab1ab2e35 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 20:42:27 +0800 Subject: [PATCH 091/107] feat: clear state when unsubscribe happens --- src/main/java/pw/mihou/nexus/features/react/React.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 4d80c86a..0e25d2c2 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -514,7 +514,15 @@ class React internal constructor(private val api: DiscordApi, private val render private fun attachListeners(api: DiscordApi): Unsubscribe { val listenerManagers = listeners.map { api.addListener(it) } return { - listenerManagers.forEach { managers -> managers.forEach { it.remove() } } + listenerManagers.forEach { managers -> + managers.forEach { it.remove() } + } + listeners = mutableListOf() + + components = mutableListOf() + contents = null + embeds = mutableListOf() + uuids.forEach { NexusUuidAssigner.deny(it) } uuids = mutableListOf() } From 2a79b4c147801e1becc4782f545a1d96df277b30 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 20:45:46 +0800 Subject: [PATCH 092/107] feat: increment version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 33151d2f..2fe2b634 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { } group = 'pw.mihou' -version = '1.0.1' +version = '1.1.0' description = 'Nexus is the next-generation Javacord framework that aims to create Discord bots with less code, dynamic, more simplicity and beauty.' repositories { From 0b261cfe17da05508a719f08c9915575b792b1c7 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 20:47:33 +0800 Subject: [PATCH 093/107] feat: allow `Options` in `SelectMenu` --- .../java/pw/mihou/nexus/features/react/elements/SelectMenu.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt b/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt index d0f9c91b..2c02ccc4 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt @@ -54,6 +54,10 @@ class SelectMenu(internal val selectMenu: SelectMenuBuilder) { selectMenu.addOption(original.build()) } + fun Options(vararg options: SelectMenuOption) { + selectMenu.addOptions(options.toList()) + } + fun Placeholder(text: String) { selectMenu.setPlaceholder(text) } From 5b328d036dabb114cd652e624a45e8334984f822 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 20:53:07 +0800 Subject: [PATCH 094/107] feat: add easier builder for building bodies and fields conditionally --- .../pw/mihou/nexus/features/react/elements/Embed.kt | 10 ++++++++++ .../pw/mihou/nexus/features/react/elements/Text.kt | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt index 391ee54e..59b8d88f 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt @@ -32,9 +32,19 @@ class Embed: TextStyles { fun SpacedBody(vararg nodes: String) { embed.setDescription(nodes.joinToString("\n")) } + fun Body(spaced: Boolean = false, builder: MutableList.() -> Unit) { + val backing = mutableListOf() + builder(backing) + embed.setDescription(if (spaced) backing.joinToString("\n") else backing.joinToString()) + } fun Field(name: String, inline: Boolean = false, vararg nodes: String) { embed.addField(name, nodes.joinToString(""), inline) } + fun Field(name: String, inline: Boolean = false, spaced: Boolean = false, builder: MutableList.() -> Unit) { + val backing = mutableListOf() + builder(backing) + embed.addField(name, if (spaced) backing.joinToString("\n") else backing.joinToString(), inline) + } fun Image(url: String) { embed.setImage(url) } diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt index b2d9f67d..6065b1bb 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt @@ -19,4 +19,9 @@ class Text: TextStyles { fun SpacedBody(vararg nodes: String) { content = nodes.joinToString("\n") } + fun Body(spaced: Boolean = false, builder: MutableList.() -> Unit) { + val backing = mutableListOf() + builder(backing) + content = if (spaced) backing.joinToString("\n") else backing.joinToString() + } } \ No newline at end of file From e86e7a9e405e20a07d9fe49a8a62906ca44dfb22 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 20:54:06 +0800 Subject: [PATCH 095/107] feat: fix `codeblock` not properly creating new lines --- .../java/pw/mihou/nexus/features/react/styles/TextStyles.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt index 83a1787e..153f9a5f 100644 --- a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt @@ -166,7 +166,7 @@ interface TextStyles { * @param nodes the nodes to include in the codeblock. */ fun codeblock(language: String, vararg nodes: String): String { - return "```$language\n${nodes.joinToString("")}\n```" + return "```$language\n${nodes.joinToString("\n")}\n```" } /** From dee647df5840793b811bb74f40ba5e29ebd7ddc9 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Fri, 1 Dec 2023 21:16:19 +0800 Subject: [PATCH 096/107] feat: reference Nexus.R in README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7b087fe5..1d44a5e8 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ object ReportUserContextMenu: NexusUserContextMenu() { } ``` +For Kotlin users who are looking to build highly reactive, or interactive bots, you may be interested in our new rendering mechanism +called [Nexus.R], read more at [GitHub Wiki](https://github.com/ShindouMihou/Nexus/wiki/Nexus.R). + ## Getting Started To get started with Nexus, we recommend reading the wiki in its chronological order: @@ -62,6 +65,7 @@ Nexus was created from the ground up to power Discord bots with a simplistic yet on performance, allowing developers to build their Discord bots fast and clean. - [x] **Object-based commands** - [x] **Object-based context menus** +- [x] **Supports an innovative, React+Svelte-like Rendering Mechanism** - [x] **Middlewares, Afterwares** - [x] **Supports auto-deferring of responses** - [x] **Flexible command synchronization system** From 4bf58b14015d733b71912a722542000e79f6015b Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 2 Dec 2023 09:19:55 +0800 Subject: [PATCH 097/107] feat: respect `Duration.INFINITE` as lifetime --- src/main/java/pw/mihou/nexus/features/react/React.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index 0e25d2c2..a07f8d32 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock import kotlin.reflect.KProperty import kotlin.time.Duration +import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours typealias Subscription = (oldValue: T, newValue: T) -> Unit @@ -38,7 +39,7 @@ typealias Derive = (T) -> K * `event.R` method instead as it is mostly designed to enable this to work for your situation, or instead use the * available `interaction.R` method for interactions. */ -class React internal constructor(private val api: DiscordApi, private val renderMode: RenderMode, lifetime: Duration = 1.hours) { +class React internal constructor(private val api: DiscordApi, private val renderMode: RenderMode, lifetime: Duration = 1.days) { private var rendered: Boolean = false internal var message: NexusMessage? = null @@ -60,7 +61,7 @@ class React internal constructor(private val api: DiscordApi, private val render private var updateSubscribers = mutableListOf() private var expansions = mutableListOf() - private var destroyJob: Cancellable? = Nexus.launch.scheduler.launch(lifetime.inWholeMilliseconds) { + private var destroyJob: Cancellable? = if (lifetime.isInfinite()) null else Nexus.launch.scheduler.launch(lifetime.inWholeMilliseconds) { destroy() } From 0635dea9cb0928da305cf407c7011db678a08b10 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 2 Dec 2023 09:22:06 +0800 Subject: [PATCH 098/107] feat: only add `MessageDeleteListener` in React when the right intent is enabled --- src/main/java/pw/mihou/nexus/features/react/React.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/React.kt b/src/main/java/pw/mihou/nexus/features/react/React.kt index a07f8d32..412f59af 100644 --- a/src/main/java/pw/mihou/nexus/features/react/React.kt +++ b/src/main/java/pw/mihou/nexus/features/react/React.kt @@ -1,6 +1,7 @@ package pw.mihou.nexus.features.react import org.javacord.api.DiscordApi +import org.javacord.api.entity.intent.Intent import org.javacord.api.entity.message.Message import org.javacord.api.entity.message.MessageBuilder import org.javacord.api.entity.message.MessageUpdater @@ -135,10 +136,12 @@ class React internal constructor(private val api: DiscordApi, private val render */ internal fun acknowledgeUpdate(message: Message) { this.resultingMessage = message - messageDeleteListenerManager?.remove() - messageDeleteListenerManager = this.resultingMessage?.run { - api.addMessageDeleteListener { - destroy() + if (api.intents.contains(Intent.GUILD_MESSAGES) || api.intents.contains(Intent.DIRECT_MESSAGES)) { + messageDeleteListenerManager?.remove() + messageDeleteListenerManager = this.resultingMessage?.run { + api.addMessageDeleteListener { + destroy() + } } } updateSubscribers.forEach { From 934b808f5d5c9ec23e4f451ff63d93c60dda252a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 2 Dec 2023 10:39:53 +0800 Subject: [PATCH 099/107] feat: add `nexus.coroutines` addon --- .gitmodules | 4 ++++ addons/nexus.coroutines | 1 + 2 files changed, 5 insertions(+) create mode 160000 addons/nexus.coroutines diff --git a/.gitmodules b/.gitmodules index 65d0287d..248c914b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "addons/nexus.entour"] path = addons/nexus.entour url = https://github.com/ShindouMihou/nexus.entour + +[submodule "addons/nexus.coroutines"] + path = addons/nexus.coroutines + url = https://github.com/ShindouMihou/nexus.coroutines diff --git a/addons/nexus.coroutines b/addons/nexus.coroutines new file mode 160000 index 00000000..70dc511b --- /dev/null +++ b/addons/nexus.coroutines @@ -0,0 +1 @@ +Subproject commit 70dc511b0fcf37ccb56429017f92135d83de2192 From 68bededfc8ee798edc7dc0e94915a3a8dea0e306 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sat, 2 Dec 2023 10:40:24 +0800 Subject: [PATCH 100/107] feat: add `nexus.coroutines` addon on readme --- addons/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/README.md b/addons/README.md index 3789b44b..d7b6e11c 100644 --- a/addons/README.md +++ b/addons/README.md @@ -7,6 +7,7 @@ repositories and linked here through Git Submodule for others to see. Here are the list of each different add-on that extends upon Nexus' features: - [`entour`](nexus.entour) is a set of handy built-in components, hooks and tools for Nexus.R. This contains many different things for develoeprs who uses Nexus.R such as Confirmation Menus. +- [`coroutines`](nexus.coroutines) adds coroutine support to the framework. ### Contributing From cec9cf478bc54b3153cceed11f7633cd5bfa588e Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 3 Dec 2023 00:04:19 +0800 Subject: [PATCH 101/107] feat: add support for `Color(hex)` in Embed --- src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt index 59b8d88f..240dbea7 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt @@ -102,6 +102,9 @@ class Embed: TextStyles { fun Color(color: Color) { embed.setColor(color) } + fun Color(hex: Int) { + embed.setColor(java.awt.Color(hex)) + } fun Timestamp(timestamp: Instant) { embed.setTimestamp(timestamp) } From 6e0d2d9f38965efb4163fb76fc06d0366511f32a Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 3 Dec 2023 00:04:43 +0800 Subject: [PATCH 102/107] feat: add support for `CurrentTimestamp` in Embed --- src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt index 240dbea7..113de964 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Embed.kt @@ -108,6 +108,9 @@ class Embed: TextStyles { fun Timestamp(timestamp: Instant) { embed.setTimestamp(timestamp) } + fun CurrentTimestamp() { + embed.setTimestampToNow() + } fun Footer(text: String) { embed.setFooter(text) } From 29c31edbc0c75f42afed9d10a2a1b0e36ecd95f5 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 3 Dec 2023 00:27:57 +0800 Subject: [PATCH 103/107] feat: add more convenient select menus --- .../features/react/elements/SelectMenu.kt | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt b/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt index 2c02ccc4..9e1efb58 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/SelectMenu.kt @@ -1,3 +1,4 @@ +@file:Suppress("FunctionName") package pw.mihou.nexus.features.react.elements import org.javacord.api.entity.channel.ChannelType @@ -39,6 +40,100 @@ fun React.Component.SelectMenu( components += element.selectMenu.build() } +fun React.Component.ChannelSelectMenu( + types: Set, + placeholder: String? = null, + customId: String = NexusUuidAssigner.request(), + minimumValues: Int = 1, + maximumValues: Int = 1, + disabled: Boolean = false, + onSelect: ((event: SelectMenuChooseEvent) -> Unit)? = null +) = SelectMenu( + componentType = ComponentType.SELECT_MENU_CHANNEL, + customId = customId, + minimumValues = minimumValues, + maximumValues = maximumValues, + disabled = disabled, + onSelect = onSelect +) { + types.forEach(::ChannelType) + placeholder?.let { Placeholder(it) } +} + +fun React.Component.ChannelSelectMenu( + placeholder: String? = null, + customId: String = NexusUuidAssigner.request(), + minimumValues: Int = 1, + maximumValues: Int = 1, + disabled: Boolean = false, + onSelect: ((event: SelectMenuChooseEvent) -> Unit)? = null +) = SelectMenu( + componentType = ComponentType.SELECT_MENU_CHANNEL, + customId = customId, + minimumValues = minimumValues, + maximumValues = maximumValues, + disabled = disabled, + onSelect = onSelect +) { + placeholder?.let { Placeholder(it) } +} + +fun React.Component.UserSelectMenu( + placeholder: String? = null, + customId: String = NexusUuidAssigner.request(), + minimumValues: Int = 1, + maximumValues: Int = 1, + disabled: Boolean = false, + onSelect: ((event: SelectMenuChooseEvent) -> Unit)? = null +) = SelectMenu( + componentType = ComponentType.SELECT_MENU_USER, + customId = customId, + minimumValues = minimumValues, + maximumValues = maximumValues, + disabled = disabled, + onSelect = onSelect +) { + placeholder?.let { Placeholder(it) } +} + +fun React.Component.MentionableSelectMenu( + placeholder: String? = null, + customId: String = NexusUuidAssigner.request(), + minimumValues: Int = 1, + maximumValues: Int = 1, + disabled: Boolean = false, + onSelect: ((event: SelectMenuChooseEvent) -> Unit)? = null +) = SelectMenu( + componentType = ComponentType.SELECT_MENU_MENTIONABLE, + customId = customId, + minimumValues = minimumValues, + maximumValues = maximumValues, + disabled = disabled, + onSelect = onSelect +) { + placeholder?.let { Placeholder(it) } +} + +fun React.Component.SelectMenu( + options: List, + placeholder: String? = null, + customId: String = NexusUuidAssigner.request(), + minimumValues: Int = 1, + maximumValues: Int = 1, + disabled: Boolean = false, + onSelect: ((event: SelectMenuChooseEvent) -> Unit)? = null +) = SelectMenu( + componentType = ComponentType.SELECT_MENU_CHANNEL, + customId = customId, + minimumValues = minimumValues, + maximumValues = maximumValues, + disabled = disabled, + onSelect = onSelect +) { + options.forEach(::Option) + placeholder?.let { Placeholder(it) } +} + class SelectMenu(internal val selectMenu: SelectMenuBuilder) { fun ChannelType(type: ChannelType) { selectMenu.addChannelType(type) From 816e0981747c44f4750be6b5ce422ce00f2b7a24 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 3 Dec 2023 00:29:48 +0800 Subject: [PATCH 104/107] feat: switch to internal variable over `view` method --- src/main/java/pw/mihou/nexus/features/react/elements/Text.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt b/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt index 6065b1bb..2534543a 100644 --- a/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt +++ b/src/main/java/pw/mihou/nexus/features/react/elements/Text.kt @@ -7,12 +7,11 @@ fun React.Component.Text(text: Text.() -> Unit) { val element = Text() text(element) - contents = element.view() + contents = element.content } class Text: TextStyles { - private var content: String = "" - fun view() = content + internal var content: String = "" fun Body(vararg nodes: String) { content = nodes.joinToString("") } From 821d8e848863f0145af636c7566648f6b2b180fc Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 3 Dec 2023 00:36:48 +0800 Subject: [PATCH 105/107] feat: fix mismatch between `mark` and `spoiler` --- .../nexus/features/react/styles/TextStyles.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt index 153f9a5f..6824370b 100644 --- a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt @@ -5,7 +5,8 @@ import java.time.Instant interface TextStyles { private fun renderTextStyles(bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false, spoiler: Boolean): Pair { + strikethrough: Boolean = false, spoiler: Boolean = false, + highlighted: Boolean = false): Pair { var prefix = "" var suffix = "" @@ -33,11 +34,16 @@ interface TextStyles { suffix = "~~$suffix" } - if (spoiler) { + if (highlighted) { prefix += "`" suffix = "`$suffix" } + if (spoiler) { + prefix += "||" + suffix = "$suffix||" + } + return prefix to suffix } @@ -75,13 +81,21 @@ interface TextStyles { fun italic(text: String) = "*$text*" /** - * Renders a spoiled (hidden) text. We recommend using [p] when you want to stack different + * Renders a highlighted text. We recommend using [p] when you want to stack different * styling over one another. * * @param text the text to render. */ fun mark(text: String) = "`$text`" + /** + * Renders a spoiler text. We recommend using [p] when you want to stack different + * styling over one another. + * + * @param text the text to render. + */ + fun spoiler(text: String) = "||$text||" + /** * Renders a text with a strikethrough. We recommend using [p] when you want to stack different * styling over one another. @@ -104,10 +118,11 @@ interface TextStyles { * @param italic whether to make the text italic. * @param strikethrough whether to add a strikethrough to the text. * @param spoiler whether to hide the text behind a spoiler. + * @param highlighted whether to make the text highlighted. */ fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false, spoiler: Boolean = false): String { - val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) + strikethrough: Boolean = false, spoiler: Boolean = false, highlighted: Boolean): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler, highlighted) return "$prefix[$text]($href)$suffix" } From 9296a21338a27310f0a985047f2c23309ae4e4e2 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 3 Dec 2023 00:40:59 +0800 Subject: [PATCH 106/107] fix: breaking change caused by previous commit --- .../java/pw/mihou/nexus/features/react/styles/TextStyles.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt index 6824370b..8c1c3cfb 100644 --- a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt @@ -59,8 +59,8 @@ interface TextStyles { * @param spoiler whether to hide the text behind a spoiler. */ fun p(text: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false, spoiler: Boolean = false): String { - val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler) + strikethrough: Boolean = false, spoiler: Boolean = false, highlighted: Boolean = false): String { + val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler, highlighted) return prefix + text + suffix } @@ -121,7 +121,7 @@ interface TextStyles { * @param highlighted whether to make the text highlighted. */ fun link(text: String, href: String, bold: Boolean = false, underline: Boolean = false, italic: Boolean = false, - strikethrough: Boolean = false, spoiler: Boolean = false, highlighted: Boolean): String { + strikethrough: Boolean = false, spoiler: Boolean = false, highlighted: Boolean = false): String { val (prefix, suffix) = renderTextStyles(bold, underline, italic, strikethrough, spoiler, highlighted) return "$prefix[$text]($href)$suffix" } From b7002ac203d558b9b523f61ca1f35900df6f1ee5 Mon Sep 17 00:00:00 2001 From: Shindou Mihou Date: Sun, 3 Dec 2023 00:42:45 +0800 Subject: [PATCH 107/107] fix: fix styling with highlights --- .../nexus/features/react/styles/TextStyles.kt | 2 +- src/test/java/react/TextStylesTest.kt | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/test/java/react/TextStylesTest.kt diff --git a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt index 8c1c3cfb..eed9f9aa 100644 --- a/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt +++ b/src/main/java/pw/mihou/nexus/features/react/styles/TextStyles.kt @@ -40,7 +40,7 @@ interface TextStyles { } if (spoiler) { - prefix += "||" + prefix = "||$prefix" suffix = "$suffix||" } diff --git a/src/test/java/react/TextStylesTest.kt b/src/test/java/react/TextStylesTest.kt new file mode 100644 index 00000000..a93a6c50 --- /dev/null +++ b/src/test/java/react/TextStylesTest.kt @@ -0,0 +1,17 @@ +package react + +import org.junit.jupiter.api.Test +import pw.mihou.nexus.features.react.styles.TextStyles +import kotlin.test.assertEquals + +class TextStylesTest: TextStyles { + companion object { + const val PLACEHOLDER_TEXT = "Hello World" + } + + @Test + fun `test stacked styling`() { + val result = p(PLACEHOLDER_TEXT, bold = true, underline = true, italic = true, strikethrough = true, spoiler = true) + assertEquals("||***__~~Hello World~~__***||", result) + } +} \ No newline at end of file