Skip to content

feat: delete-all button and language dropdown for multi-message code blocks#550

Open
Neil-Tomar wants to merge 4 commits into
Java-Discord:mainfrom
Neil-Tomar:feat/format-code-message-linking
Open

feat: delete-all button and language dropdown for multi-message code blocks#550
Neil-Tomar wants to merge 4 commits into
Java-Discord:mainfrom
Neil-Tomar:feat/format-code-message-linking

Conversation

@Neil-Tomar

Copy link
Copy Markdown
Contributor

What

Multi-message formatted code blocks now have two interactions, backed by a small message-linking helper:

  • Delete-all button — removes every message of the block in one click
  • Language dropdown (ephemeral) — re-colors every chunk to a chosen language

How

  • LinkedMessages resolves the block from the component's message (getHistoryBefore/After), touching only the bot's own messages.
  • FormatCodeInteractionHandler handles the delete button and the language dropdown.
  • Permission: requester / staff / owner (same rule as the existing delete button).

Open questions

  • The language dropdown is ephemeral per your suggestion, so it's a one-time picker that's gone on reload — happy to make it a permanent public control instead.
  • Rare edge case: two simultaneous formats could interleave bot messages; the resolver is best-effort there.

…ching

Delete-all button removes every message of a block; ephemeral language dropdown re-colors every chunk. Only the bot's own messages are touched.
@Neil-Tomar Neil-Tomar requested a review from a team as a code owner July 4, 2026 15:41
@Neil-Tomar Neil-Tomar requested a review from danthe1st July 5, 2026 13:24
@Neil-Tomar Neil-Tomar requested a review from danthe1st July 5, 2026 16:34
* @return an action row containing the language dropdown
*/
public static ActionRow buildLanguageMenu(long requesterId, int total) {
return ActionRow.of(languageMenu(ComponentIdBuilder.build(COMPONENT_ID, requesterId, total)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed it's possible to do the following:

  • User A runs the format code command
  • User B runs the format code command
  • User A deletes the code they formatted
  • User A changes the language from their ephemeral message

In this case, the language of the code block of user B is changed. Could you prevent this in one of the following ways?

  • In LinkedMessages.resolveAfter, check that the last message contains the delete button with the delete button with the correct requestor (= current user) and that no other messages have a delete button
  • Change the ephemeral message to be sent after the formatted codeblocks, use resolveBefore and add an additional argument containing the message ID of the first code block message to the ID here. If this doesn't match, it is a failure.

I think the first approach is easier to do so I recommend that one.

@Neil-Tomar Neil-Tomar requested a review from danthe1st July 6, 2026 12:57
}, () -> Responses.error(event.getHook(), "Could not update the code block — it may have changed or no longer be yours.").queue());
}

private static boolean ownsBlock(List<Message> messages, long userId) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please change this name to something more descriptive like isCodeBlockMessagesOwnedBy or isFormattedCodeMessagesOwnedBy?

boolean lastIsUsers = buttonsOf(last).stream().anyMatch(button -> isDeleteButtonOf(button, userId));
boolean noOtherHasButton = messages.size() <= 1 ;
for (Message message : messages.subList(0,messages.size()-1)){
for(Button button: buttonsOf(message)){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatted code block messages shouldn't have any componens except for the last message. I think the following would be easier:

for (int i = 1; i < messages.size(); i++) {
    if (!message.getComponents().isEmpty()) {
        return false;
    }
}
return messages.size() >= 1 && hasDeleteButtonOf(messages.getFirst(), userId)

for (Message message : messages.subList(0,messages.size()-1)){
for(Button button: buttonsOf(message)){
if(isDeleteButton(button)){
noOtherHasButton = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you are basically requiring that there is a delete button but you are excluding the chronologically first message from the list using subList which makes no sense.

@@ -105,9 +110,46 @@ public void handleStringSelectMenu(@NonNull StringSelectInteractionEvent event,
event.deferEdit().queue();

LinkedMessages.resolveAfter(event.getMessage(), Integer.parseInt(id[2]),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please document that resolveAfter and resolveBefore return the messages ordered from newest to oldest?

return lastIsUsers && noOtherHasButton;
}

private static List<Button> buttonsOf(Message message) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you'll need a buttonsOf method, you can just create a method like hasDeleteButtonOf that checks whether there is a delete button of a given user. Such a method could then just iterate over the components/buttons and checks for the delete button.

}, () -> Responses.error(event.getHook(), "Could not update the code block — it may have changed or no longer be yours.").queue());
}

private static boolean ownsBlock(List<Message> messages, long userId) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this method requires a certain order of the messages, please document this in a Javadoc.

}

private static boolean isDeleteButtonOf(Button button, long userId) {
return isDeleteButton(button)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be easier to use ComponentIdBuilder.split and:

  • verify that there are sufficiently many elements
  • verify that the element at index 1 equals COMPONENT_ID
  • verify that the element at index 1 equals String.valueOf(userId)

messages.forEach(message ->
message.editMessage(withLanguage(message.getContentRaw(), language)).queue());
} else {
Responses.error(event.getHook(),"Could not update the code block — it may have changed or no longer be yours.").queue();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"no longer be yours" is a bit confusing in my opinion. What about "The code block could not be updated. The messages may have been deleted."

Also, I assume you are not able to delete it because the original message is ephemeral? Is this correct (this is perfectly fine)?

@Neil-Tomar Neil-Tomar force-pushed the feat/format-code-message-linking branch from 43bd07e to 8dece97 Compare July 9, 2026 07:48

if (messages.size() > MAX_MESSAGES) {
Responses.errorWithTitle(event.getHook(), "Output Too Large", "The formatted result is too large to send. Please provide a smaller code snippet or use a paste service instead."
Responses.errorWithTitle(event.getHook(), "Code out of Bound", "The formatted result is too large to send. Please provide a smaller code snippet or use a paste service instead."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant to say Code Out of Bounds (plural).

private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event) {
private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event, long firstMessageID) {
if (index >= messages.size()) {
event.getHook().sendMessage("Your message has been set. If needed, you can change the language used for syntax highlighting below.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sent, not set.

.queue();
return;
}
var action = channel.sendMessage(messages.get(index))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to specify the type explicitly here instead of using var?

@Contract("_,_,_ -> new")
static @NotNull ActionRow buildActionRow(@NotNull Message target, long requesterId, int total) {
return ActionRow.of(
FormatCodeInteractionHandler.createDeleteAllButton(requesterId, total),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to add the first message ID to the delete button and verify that it is actually the first message before deleting. I think that should allow you using (almost) the same logic for finding and checking the messages.

public static Button createDeleteAllButton(long requesterID, int total) {
return Button.secondary(ComponentIdBuilder.build(COMPONENT_ID, requesterID, total), "\uD83D\uDDD1\uFE0F");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please move the delete logic together and the language change logic together? e.g. first the methods for creating and handling the delete button and then the methods for creating and handling the language menu.

}, () -> Responses.errorWithTitle(event.getHook(), "Undefined Behavior", "Could not update the code block").queue());
}

private boolean canManage(Member member, long requesterId) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please rename this method isMemberAuthorizedToDeleteMessagesOf? Also, I think the isOwner check is redundant (but I don't have a problem with you keeping this).


LinkedMessages.resolveBefore(event.getMessage(), Integer.parseInt(id[2]),
messages -> event.getChannel().purgeMessages(messages),
() -> Responses.errorWithTitle(event.getHook(), "Undefined Behavior", "Could not delete the code block").queue());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the "Undefined Behavior" adds much of value. I think it's better to not use a title here.

if (messages.getLast().getIdLong() == Long.parseLong(id[3])) {
messages.forEach(message -> message.editMessage(withLanguage(message.getContentRaw(), language)).queue());
} else {
Responses.errorWithTitle(event.getHook(), "Merge Conflict", "The code block could not be updated. The messages may have been deleted.").queue();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really a merge conflict. If the titles don't add much of value, please don't specify them.

Language language = Language.fromString(values.getFirst());
event.deferEdit().queue();

LinkedMessages.resolveAfter(event.getMessage(), Integer.parseInt(id[2]), messages -> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure resolveAfter is still correct when you are sending the ephemeral message after the codeblocks?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This is assuming you are changing the interaction to use deleteOriginal() to move the message below.

private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event) {
private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event, long firstMessageID) {
if (index >= messages.size()) {
event.getHook().sendMessage("Your message has been set. If needed, you can change the language used for syntax highlighting below.")

@danthe1st danthe1st Jul 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to send the ephemeral response after the code block messages, I think you can do the following:

// use deleteOriginal() to delete the intermediate message sent by deferReply(false)
event.getHook().deleteOriginal().queue(_ -> 
    event.getHook().sendMessage("Your message has been sent. If needed, you can change the language used for syntax highlighting below.")
    ...
    .queue();
);

Otherwise you would need to check the last message and not the first message I think (the goal is to check the message that is furthest from the anchor).

Language language = Language.fromString(values.getFirst());
event.deferEdit().queue();

LinkedMessages.resolveAfter(event.getMessage(), Integer.parseInt(id[2]), messages -> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This is assuming you are changing the interaction to use deleteOriginal() to move the message below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants