From ba9ef2d30b484fa0827a710039c671a1ca49ddae Mon Sep 17 00:00:00 2001 From: bastian <96827097+bastianbeckeru@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:27:44 -0300 Subject: [PATCH] feat(dialog): add option to stack dialog buttons vertically --- .../authme/platform/DialogWindowSpec.java | 13 +++++++-- .../authme/service/DialogWindowService.java | 15 ++++++---- .../properties/RegistrationSettings.java | 6 ++++ .../service/DialogWindowServiceTest.java | 28 +++++++++++++++++++ .../authme/platform/PaperDialogHelper.java | 16 +++++++---- .../authme/platform/SpigotDialogHelper.java | 3 +- .../platform/SpigotDialogHelperTest.java | 23 +++++++++++++++ docs/config.md | 7 +++-- 8 files changed, 94 insertions(+), 17 deletions(-) diff --git a/authme-core/src/main/java/fr/xephi/authme/platform/DialogWindowSpec.java b/authme-core/src/main/java/fr/xephi/authme/platform/DialogWindowSpec.java index 6341cd52c9..56fdd2a9a1 100644 --- a/authme-core/src/main/java/fr/xephi/authme/platform/DialogWindowSpec.java +++ b/authme-core/src/main/java/fr/xephi/authme/platform/DialogWindowSpec.java @@ -15,6 +15,7 @@ * @param secondaryButtonCommand command template the secondary button should execute in post-join dialogs * (null for pre-join dialogs where the secondary button is a cancel action) * @param body optional translated body text shown below the title (null = no body) + * @param stackButtonsVertically whether buttons should be stacked vertically instead of shown side by side */ public record DialogWindowSpec(String title, List inputs, @@ -23,7 +24,8 @@ public record DialogWindowSpec(String title, boolean showSecondaryButton, boolean canCloseWithEscape, String secondaryButtonCommand, - String body) { + String body, + boolean stackButtonsVertically) { public DialogWindowSpec { Objects.requireNonNull(title, "title"); @@ -42,12 +44,17 @@ public DialogWindowSpec(String title, boolean canCloseWithEscape, String secondaryButtonCommand) { this(title, inputs, primaryButtonLabel, secondaryButtonLabel, - showSecondaryButton, canCloseWithEscape, secondaryButtonCommand, null); + showSecondaryButton, canCloseWithEscape, secondaryButtonCommand, null, false); } /** Returns a copy of this spec with the given body text (may be null). */ public DialogWindowSpec withBody(String newBody) { return new DialogWindowSpec(title, inputs, primaryButtonLabel, secondaryButtonLabel, - showSecondaryButton, canCloseWithEscape, secondaryButtonCommand, newBody); + showSecondaryButton, canCloseWithEscape, secondaryButtonCommand, newBody, stackButtonsVertically); + } + + /** Returns the number of columns the dialog buttons should be arranged in. */ + public int buttonColumns() { + return stackButtonsVertically ? 1 : 2; } } diff --git a/authme-core/src/main/java/fr/xephi/authme/service/DialogWindowService.java b/authme-core/src/main/java/fr/xephi/authme/service/DialogWindowService.java index 60c63a25af..4f3d54ffd8 100644 --- a/authme-core/src/main/java/fr/xephi/authme/service/DialogWindowService.java +++ b/authme-core/src/main/java/fr/xephi/authme/service/DialogWindowService.java @@ -67,7 +67,8 @@ public DialogWindowSpec createPreJoinLoginDialog(String playerName) { showRecovery || showCancelButton, canCloseWithEscape, showRecovery ? "forgot_password" : null, - showBody ? text.apply(MessageKey.DIALOG_LOGIN_BODY) : null); + showBody ? text.apply(MessageKey.DIALOG_LOGIN_BODY) : null, + commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)); } public DialogWindowSpec createPreJoinRecoveryDialog(String playerName) { @@ -82,7 +83,8 @@ public DialogWindowSpec createPreJoinRecoveryDialog(String playerName) { true, false, null, - showBody ? text.apply(MessageKey.DIALOG_RECOVERY_BODY) : null); + showBody ? text.apply(MessageKey.DIALOG_RECOVERY_BODY) : null, + commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)); } public DialogWindowSpec createTotpDialog(Player player) { @@ -99,7 +101,8 @@ public DialogWindowSpec createTotpDialog(Player player) { false, false, null, - showBody ? text.apply(MessageKey.DIALOG_TWO_FACTOR_BODY) : null); + showBody ? text.apply(MessageKey.DIALOG_TWO_FACTOR_BODY) : null, + commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)); } public DialogWindowSpec createRegisterDialog(Player player, @@ -144,7 +147,8 @@ private DialogWindowSpec createLoginDialogSpec(Function text showSecondaryButton, canCloseWithEscape, showRecovery ? "email recover $(email)" : null, - showBody ? textResolver.apply(MessageKey.DIALOG_LOGIN_BODY) : null); + showBody ? textResolver.apply(MessageKey.DIALOG_LOGIN_BODY) : null, + commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)); } private DialogWindowSpec createRegisterDialogSpec(RegistrationType type, @@ -194,7 +198,8 @@ private DialogWindowSpec createRegisterDialogSpec(RegistrationType type, showSecondaryButton, canCloseWithEscape, null, - showBody ? textResolver.apply(MessageKey.DIALOG_REGISTER_BODY) : null); + showBody ? textResolver.apply(MessageKey.DIALOG_REGISTER_BODY) : null, + commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)); } private String getPostJoinMessage(Player player, MessageKey key) { diff --git a/authme-core/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java b/authme-core/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java index 479d18d068..02e54d9ea3 100644 --- a/authme-core/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java +++ b/authme-core/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java @@ -120,6 +120,12 @@ public final class RegistrationSettings implements SettingsHolder { public static final Property DIALOG_SHOW_BODY = newProperty("settings.registration.dialog.showBody", true); + @Comment({ + "Stack dialog buttons vertically, with the primary button on top.", + "If disabled, dialog buttons are shown side by side."}) + public static final Property DIALOG_STACK_BUTTONS_VERTICALLY = + newProperty("settings.registration.dialog.stackButtonsVertically", false); + @Comment({ "Show the login/register dialog during the Paper/Folia pre-join phase.", "Independent from postJoin.enable; you may enable either or both options.", diff --git a/authme-core/src/test/java/fr/xephi/authme/service/DialogWindowServiceTest.java b/authme-core/src/test/java/fr/xephi/authme/service/DialogWindowServiceTest.java index a03aef01ee..d978713480 100644 --- a/authme-core/src/test/java/fr/xephi/authme/service/DialogWindowServiceTest.java +++ b/authme-core/src/test/java/fr/xephi/authme/service/DialogWindowServiceTest.java @@ -49,6 +49,7 @@ void shouldBuildPostJoinLoginDialogWithForgotPasswordButtonWhenEmailServiceConfi .willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey()); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_FORGOT_PASSWORD_BUTTON)).willReturn(true); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(true); + given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false); given(emailService.hasAllInformation()).willReturn(true); // when @@ -73,6 +74,7 @@ void shouldNotShowForgotPasswordButtonWhenEmailServiceNotConfigured() { .willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey()); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_FORGOT_PASSWORD_BUTTON)).willReturn(true); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false); + given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false); given(emailService.hasAllInformation()).willReturn(false); // when — email service not configured: recovery must not appear regardless of the flag @@ -92,6 +94,7 @@ void shouldBuildPostJoinLoginDialogWithoutForgotPasswordButtonWhenDisabledByConf .willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey()); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_FORGOT_PASSWORD_BUTTON)).willReturn(false); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false); + given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false); // when — disabled by config even when email service is available DialogWindowSpec dialog = dialogWindowService.createLoginDialog(player); @@ -110,6 +113,7 @@ void shouldBuildPostJoinRegisterDialogWithEmailBeforePasswordForMandatoryEmailFl given(commonService.retrieveSingleMessage(eq(player), any(MessageKey.class))) .willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey()); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false); + given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false); // when DialogWindowSpec dialog = dialogWindowService.createRegisterDialog( @@ -126,6 +130,7 @@ void shouldBuildPostJoinRegisterDialogForOptionalEmailFlow() { given(commonService.retrieveSingleMessage(eq(player), any(MessageKey.class))) .willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey()); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false); + given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false); // when DialogWindowSpec dialog = dialogWindowService.createRegisterDialog( @@ -148,6 +153,7 @@ void shouldBuildPreJoinLoginDialogWithCancelButtonWhenEmailServiceNotConfigured( given(commonService.getProperty(RegistrationSettings.PRE_JOIN_DIALOG_SHOW_CANCEL_BUTTON)).willReturn(true); given(commonService.getProperty(RegistrationSettings.PRE_JOIN_DIALOG_ALLOW_CLOSE_WITH_ESCAPE)).willReturn(true); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(true); + given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false); given(emailService.hasAllInformation()).willReturn(false); // when — email not configured: no recovery, cancel button shown instead @@ -160,6 +166,7 @@ void shouldBuildPreJoinLoginDialogWithCancelButtonWhenEmailServiceNotConfigured( assertThat(dialog.showSecondaryButton(), is(true)); assertThat(dialog.canCloseWithEscape(), is(true)); assertThat(dialog.body(), is("dialog.login.body")); + assertThat(dialog.buttonColumns(), is(2)); assertThat(dialog.inputs().stream().map(input -> input.id()).toList(), contains("password")); assertThat(dialog.secondaryButtonCommand(), is(nullValue())); } @@ -171,6 +178,7 @@ void shouldBuildPreJoinLoginDialogWithRecoveryButtonWhenEmailServiceConfigured() .willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey()); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_FORGOT_PASSWORD_BUTTON)).willReturn(true); given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false); + given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false); given(emailService.hasAllInformation()).willReturn(true); // when — email configured: recovery button shown, cancel suppressed, email field present @@ -183,4 +191,24 @@ void shouldBuildPreJoinLoginDialogWithRecoveryButtonWhenEmailServiceConfigured() assertThat(dialog.secondaryButtonCommand(), is(notNullValue())); assertThat(dialog.inputs().stream().map(input -> input.id()).toList(), contains("password")); } + + @Test + void shouldKeepStackedButtonsInPreJoinRegisterDialogWhenBodyIsReplaced() { + // given + given(messages.retrieveSingle(eq("Bobby"), any(MessageKey.class))) + .willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey()); + given(commonService.getProperty(RegistrationSettings.PRE_JOIN_DIALOG_SHOW_CANCEL_BUTTON)).willReturn(true); + given(commonService.getProperty(RegistrationSettings.PRE_JOIN_DIALOG_ALLOW_CLOSE_WITH_ESCAPE)).willReturn(true); + given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false); + given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(true); + + // when + DialogWindowSpec dialog = dialogWindowService.createPreJoinRegisterDialog( + "Bobby", RegistrationType.PASSWORD, RegisterSecondaryArgument.CONFIRMATION).withBody("error"); + + // then + assertThat(dialog.body(), is("error")); + assertThat(dialog.showSecondaryButton(), is(true)); + assertThat(dialog.buttonColumns(), is(1)); + } } diff --git a/authme-paper-common/src/main/java/fr/xephi/authme/platform/PaperDialogHelper.java b/authme-paper-common/src/main/java/fr/xephi/authme/platform/PaperDialogHelper.java index 808b48eca1..e39149a5dd 100644 --- a/authme-paper-common/src/main/java/fr/xephi/authme/platform/PaperDialogHelper.java +++ b/authme-paper-common/src/main/java/fr/xephi/authme/platform/PaperDialogHelper.java @@ -63,7 +63,7 @@ public static Dialog createPreJoinLoginDialog(DialogWindowSpec dialog) { return Dialog.create(factory -> factory.empty() .base(base) - .type(DialogType.multiAction(createPreJoinButtons(dialog, loginButton, secondaryKey)).build())); + .type(createButtonDialogType(dialog, createPreJoinButtons(dialog, loginButton, secondaryKey)))); } public static Dialog createPreJoinRecoveryDialog(DialogWindowSpec dialog) { @@ -80,8 +80,8 @@ public static Dialog createPreJoinRecoveryDialog(DialogWindowSpec dialog) { return Dialog.create(factory -> factory.empty() .base(base) - .type(DialogType.multiAction(createPreJoinButtons(dialog, submitButton, - PaperDialogActionKeys.PRE_JOIN_RECOVERY_CANCEL)).build())); + .type(createButtonDialogType(dialog, createPreJoinButtons(dialog, submitButton, + PaperDialogActionKeys.PRE_JOIN_RECOVERY_CANCEL)))); } public static Dialog createPreJoinRegisterDialog(DialogWindowSpec dialog) { @@ -98,9 +98,9 @@ public static Dialog createPreJoinRegisterDialog(DialogWindowSpec dialog) { return Dialog.create(factory -> factory.empty() .base(base) - .type(DialogType.multiAction(createPreJoinButtons(dialog, + .type(createButtonDialogType(dialog, createPreJoinButtons(dialog, registerButton, - PaperDialogActionKeys.PRE_JOIN_REGISTER_CANCEL)).build())); + PaperDialogActionKeys.PRE_JOIN_REGISTER_CANCEL)))); } private static Dialog createInGameCommandDialog(DialogWindowSpec dialog, String commandTemplate) { @@ -124,7 +124,11 @@ private static Dialog createInGameCommandDialog(DialogWindowSpec dialog, String return Dialog.create(factory -> factory.empty() .base(base) - .type(DialogType.multiAction(buttons).build())); + .type(createButtonDialogType(dialog, buttons))); + } + + private static DialogType createButtonDialogType(DialogWindowSpec dialog, List buttons) { + return DialogType.multiAction(buttons).columns(dialog.buttonColumns()).build(); } private static List createPreJoinButtons(DialogWindowSpec dialog, diff --git a/authme-spigot-1.21/src/main/java/fr/xephi/authme/platform/SpigotDialogHelper.java b/authme-spigot-1.21/src/main/java/fr/xephi/authme/platform/SpigotDialogHelper.java index d587c547b9..428b0052c5 100644 --- a/authme-spigot-1.21/src/main/java/fr/xephi/authme/platform/SpigotDialogHelper.java +++ b/authme-spigot-1.21/src/main/java/fr/xephi/authme/platform/SpigotDialogHelper.java @@ -40,7 +40,8 @@ static void showLoginDialog(Player player, DialogWindowSpec dialog) { buttons.add(new ActionButton(toTextComponent(dialog.secondaryButtonLabel()), new RunCommandAction(dialog.secondaryButtonCommand()))); } - player.showDialog(new MultiActionDialog(base, buttons.toArray(new ActionButton[0]))); + player.showDialog(new MultiActionDialog(base, buttons.toArray(new ActionButton[0])) + .columns(dialog.buttonColumns())); } static void showTotpDialog(Player player, DialogWindowSpec dialog) { diff --git a/authme-spigot-1.21/src/test/java/fr/xephi/authme/platform/SpigotDialogHelperTest.java b/authme-spigot-1.21/src/test/java/fr/xephi/authme/platform/SpigotDialogHelperTest.java index b18a24b7ae..212490c3dc 100644 --- a/authme-spigot-1.21/src/test/java/fr/xephi/authme/platform/SpigotDialogHelperTest.java +++ b/authme-spigot-1.21/src/test/java/fr/xephi/authme/platform/SpigotDialogHelperTest.java @@ -29,6 +29,29 @@ public void showLoginDialogSendsCorrectCommandTemplate() { assertThat(action.template(), is("login $(password)")); } + @Test + public void showLoginDialogStacksButtonsWhenEnabled() { + // given + Player player = mock(Player.class); + DialogWindowSpec dialog = new DialogWindowSpec("Login", + java.util.List.of(new DialogInputSpec("password", "Password", 100)), + "Login", + "Forgot Password?", + true, + false, + "email recover $(email)", + null, + true); + + // when + SpigotDialogHelper.showLoginDialog(player, dialog); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(MultiActionDialog.class); + verify(player).showDialog(captor.capture()); + assertThat(captor.getValue().columns(), is(1)); + } + @Test public void showLoginDialogIncludesForgotPasswordButtonWhenSpecified() { // given diff --git a/docs/config.md b/docs/config.md index f4c442c30c..901c9b68e8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,5 @@ - + ## AuthMe Configuration The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder, @@ -335,6 +335,9 @@ settings: # Show a short description body in login, register, and two-factor dialogs. # The body text helps players understand what the dialog is asking them to do. showBody: true + # Stack dialog buttons vertically, with the primary button on top. + # If disabled, dialog buttons are shown side by side. + stackButtonsVertically: false preJoin: # Show the login/register dialog during the Paper/Folia pre-join phase. # Independent from postJoin.enable; you may enable either or both options. @@ -650,4 +653,4 @@ To change settings on a running server, save your changes to config.yml and use --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Sep 18 18:25:33 CST 2026 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon Nov 02 14:57:12 CLST 2026