From 54dc93759f5fb362fa8d614ceeb8071073e9db1f Mon Sep 17 00:00:00 2001 From: Marcus Fihlon Date: Tue, 9 Apr 2024 23:15:04 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20admin=20authent?= =?UTF-8?q?ication=20closes=20#80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcus Fihlon --- .../fihlon/apus/ui/view/MessageView.java | 145 +----------------- .../swiss/fihlon/apus/ui/view/SocialView.java | 89 ++++++++++- .../vaadin-i18n/translations.properties | 29 ++-- .../vaadin-i18n/translations_de.properties | 29 ++-- 4 files changed, 108 insertions(+), 184 deletions(-) diff --git a/src/main/java/swiss/fihlon/apus/ui/view/MessageView.java b/src/main/java/swiss/fihlon/apus/ui/view/MessageView.java index 5ddc1d3..427aba3 100644 --- a/src/main/java/swiss/fihlon/apus/ui/view/MessageView.java +++ b/src/main/java/swiss/fihlon/apus/ui/view/MessageView.java @@ -22,24 +22,14 @@ import com.vaadin.flow.component.Text; import com.vaadin.flow.component.UI; import com.vaadin.flow.component.avatar.Avatar; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; -import com.vaadin.flow.component.contextmenu.ContextMenu; import com.vaadin.flow.component.dependency.CssImport; -import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Footer; import com.vaadin.flow.component.html.Header; import com.vaadin.flow.component.html.Image; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.textfield.PasswordField; -import com.vaadin.flow.data.value.ValueChangeMode; import org.jetbrains.annotations.NotNull; import org.jsoup.Jsoup; import org.ocpsoft.prettytime.PrettyTime; -import swiss.fihlon.apus.configuration.Configuration; -import swiss.fihlon.apus.service.SocialService; import swiss.fihlon.apus.social.Message; @CssImport(value = "./themes/apus/views/message-view.css") @@ -47,14 +37,8 @@ public final class MessageView extends Div { private static final int MAX_LENGTH = 500; private static final String TRUNC_INDICATOR = " […]"; - private final transient SocialService socialService; - private final transient Configuration configuration; - public MessageView(@NotNull final Message message, - @NotNull final SocialService socialService, - @NotNull final Configuration configuration) { - this.socialService = socialService; - this.configuration = configuration; + public MessageView(@NotNull final Message message) { setId("message-" + message.id()); addClassName("message-view"); add(createHeaderComponent(message)); @@ -75,132 +59,7 @@ public MessageView(@NotNull final Message message, } private Component createAvatarComponent(@NotNull final Message message) { - final var avatar = new Avatar(message.author(), message.avatar()); - if (!configuration.getAdmin().password().isBlank()) { - final var menu = new ContextMenu(); - menu.addItem(getTranslation("social.message.contextmenu.hide.message"), event -> confirmHideMessage(message)); - menu.addItem(getTranslation("social.message.contextmenu.hide.profile"), event -> confirmHideProfile(message)); - menu.setTarget(avatar); - } - return avatar; - } - - private void confirmHideMessage(@NotNull final Message message) { - final var dialog = new ConfirmDialog(); - dialog.setHeader(getTranslation("social.message.dialog.hide.message.confirm.title")); - dialog.setText(getTranslation("social.message.dialog.hide.message.confirm.text", message.author(), message.date())); - dialog.setCloseOnEsc(true); - - dialog.setCancelable(true); - dialog.setCancelButton(getTranslation("social.message.dialog.hide.message.confirm.cancel"), event -> dialog.close()); - - dialog.setConfirmText(getTranslation("social.message.dialog.hide.message.confirm.button")); - dialog.addConfirmListener(event -> { - dialog.close(); - authorizeHideMessage(message); - }); - - dialog.open(); - } - - private void authorizeHideMessage(@NotNull final Message message) { - final Dialog dialog = new Dialog(); - dialog.setHeaderTitle(getTranslation("social.message.dialog.hide.message.authorize.title")); - dialog.setCloseOnEsc(true); - dialog.setCloseOnOutsideClick(true); - - final PasswordField passwordField = new PasswordField(); - passwordField.setPlaceholder(getTranslation("social.message.dialog.hide.message.authorize.password")); - passwordField.setRequired(true); - passwordField.setValueChangeMode(ValueChangeMode.EAGER); - - final Button hideButton = new Button(getTranslation("social.message.dialog.hide.message.authorize.button"), event -> { - dialog.close(); - hideMessage(message, passwordField.getValue()); - }); - hideButton.setEnabled(false); - hideButton.setDisableOnClick(true); - hideButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - final Button cancelButton = new Button(getTranslation("social.message.dialog.hide.message.authorize.cancel"), event -> dialog.close()); - cancelButton.setDisableOnClick(true); - cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - dialog.getFooter().add(hideButton, cancelButton); - - passwordField.addKeyDownListener(event -> hideButton.setEnabled(!passwordField.isEmpty())); - dialog.add(passwordField); - - dialog.open(); - passwordField.focus(); - } - - private void hideMessage(@NotNull final Message message, @NotNull final String password) { - if (password.equals(configuration.getAdmin().password())) { - socialService.hideMessage(message); - removeFromParent(); - Notification.show(getTranslation("social.message.notification.hide.message.success", message.author())); - } else { - Notification.show(getTranslation("social.message.notification.hide.message.rejected")); - } - } - - private void confirmHideProfile(@NotNull final Message message) { - final var dialog = new ConfirmDialog(); - dialog.setHeader(getTranslation("social.message.dialog.hide.profile.confirm.title")); - dialog.setText(getTranslation("social.message.dialog.hide.profile.confirm.text", message.profile(), message.author())); - dialog.setCloseOnEsc(true); - - dialog.setCancelable(true); - dialog.setCancelButton(getTranslation("social.message.dialog.hide.profile.confirm.cancel"), event -> dialog.close()); - - dialog.setConfirmText(getTranslation("social.message.dialog.hide.profile.confirm.button")); - dialog.addConfirmListener(event -> { - dialog.close(); - authorizeHideProfile(message); - }); - - dialog.open(); - } - - private void authorizeHideProfile(@NotNull final Message message) { - final Dialog dialog = new Dialog(); - dialog.setHeaderTitle(getTranslation("social.message.dialog.hide.profile.authorize.title")); - dialog.setCloseOnEsc(true); - dialog.setCloseOnOutsideClick(true); - - final PasswordField passwordField = new PasswordField(); - passwordField.setPlaceholder(getTranslation("social.message.dialog.hide.profile.authorize.password")); - passwordField.setRequired(true); - passwordField.setValueChangeMode(ValueChangeMode.EAGER); - - final Button hideButton = new Button(getTranslation("social.message.dialog.hide.profile.authorize.button"), event -> { - dialog.close(); - hideProfile(message, passwordField.getValue()); - }); - hideButton.setEnabled(false); - hideButton.setDisableOnClick(true); - hideButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - final Button cancelButton = new Button(getTranslation("social.message.dialog.hide.profile.authorize.cancel"), event -> dialog.close()); - cancelButton.setDisableOnClick(true); - cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - dialog.getFooter().add(hideButton, cancelButton); - - passwordField.addKeyDownListener(event -> hideButton.setEnabled(!passwordField.isEmpty())); - dialog.add(passwordField); - - dialog.open(); - passwordField.focus(); - } - - private void hideProfile(@NotNull final Message message, @NotNull final String password) { - if (password.equals(configuration.getAdmin().password())) { - socialService.hideProfile(message); - removeFromParent(); - Notification.show(getTranslation("social.message.notification.hide.profile.success", message.profile(), message.author())); - } else { - Notification.show(getTranslation("social.message.notification.hide.profile.rejected")); - } + return new Avatar(message.author(), message.avatar()); } @NotNull diff --git a/src/main/java/swiss/fihlon/apus/ui/view/SocialView.java b/src/main/java/swiss/fihlon/apus/ui/view/SocialView.java index 3c6e188..a2637d3 100644 --- a/src/main/java/swiss/fihlon/apus/ui/view/SocialView.java +++ b/src/main/java/swiss/fihlon/apus/ui/view/SocialView.java @@ -17,9 +17,17 @@ */ package swiss.fihlon.apus.ui.view; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; import com.vaadin.flow.component.dependency.CssImport; +import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.data.value.ValueChangeMode; import org.jetbrains.annotations.NotNull; import org.springframework.scheduling.TaskScheduler; import swiss.fihlon.apus.configuration.Configuration; @@ -38,6 +46,8 @@ public final class SocialView extends Div { private final transient SocialService socialService; private final transient Configuration configuration; private final Div messageContainer = new Div(); + private final ContextMenu contextMenu; + private boolean adminModeEnabled = false; public SocialView(@NotNull final SocialService socialService, @NotNull final TaskScheduler taskScheduler, @@ -49,11 +59,69 @@ public SocialView(@NotNull final SocialService socialService, add(new H2(getTranslation("social.heading", configuration.getMastodon().hashtag()))); add(messageContainer); messageContainer.addClassName("masonry"); + + if (adminModeEnabled || configuration.getAdmin().password().isBlank()) { + contextMenu = null; + } else { + contextMenu = new ContextMenu(); + contextMenu.addItem(getTranslation("social.admin.login.menu"), event -> showLoginDialog()); + contextMenu.setTarget(messageContainer); + } + final ScheduledFuture updateScheduler = taskScheduler.scheduleAtFixedRate( this::updateScheduler, Instant.now().plusSeconds(1), UPDATE_FREQUENCY); addDetachListener(event -> updateScheduler.cancel(true)); } + private void showLoginDialog() { + final Dialog dialog = new Dialog(); + dialog.setHeaderTitle(getTranslation("social.admin.login.title")); + dialog.setCloseOnEsc(true); + dialog.setCloseOnOutsideClick(true); + + final PasswordField passwordField = new PasswordField(); + passwordField.setPlaceholder(getTranslation("social.admin.login.password")); + passwordField.setRequired(true); + passwordField.setValueChangeMode(ValueChangeMode.EAGER); + + final Button loginButton = new Button(getTranslation("social.admin.login.button"), event -> { + handleLogin(passwordField.getValue()); + dialog.close(); + }); + loginButton.setEnabled(false); + loginButton.setDisableOnClick(true); + loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + final Button cancelButton = new Button(getTranslation("social.admin.login.cancel"), event -> dialog.close()); + cancelButton.setDisableOnClick(true); + cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + dialog.getFooter().add(loginButton, cancelButton); + + passwordField.addKeyDownListener(event -> { + loginButton.setEnabled(!passwordField.isEmpty()); + if (event.getKey().equals(Key.ENTER)) { + handleLogin(passwordField.getValue()); + dialog.close(); + } + }); + dialog.add(passwordField); + + dialog.open(); + passwordField.focus(); + } + + private void handleLogin(@NotNull final String password) { + if (configuration.getAdmin().password().equals(password)) { + adminModeEnabled = true; + contextMenu.setTarget(null); + updateMessages(); + Notification.show(getTranslation("social.admin.login.successful")); + } else { + Notification.show(getTranslation("social.admin.login.rejected")); + } + } + private void updateScheduler() { getUI().ifPresent(ui -> ui.access(this::updateMessages)); } @@ -61,7 +129,26 @@ private void updateScheduler() { private void updateMessages() { messageContainer.removeAll(); for (final Message message : socialService.getMessages(30)) { - messageContainer.add(new MessageView(message, socialService, configuration)); + final var messageView = new MessageView(message); + if (adminModeEnabled) { + final var messageMenu = new ContextMenu(); + messageMenu.addItem(getTranslation("social.message.contextmenu.hide.message"), event -> hideMessage(message)); + messageMenu.addItem(getTranslation("social.message.contextmenu.hide.profile"), event -> blockProfile(message)); + messageMenu.setTarget(messageView); + } + messageContainer.add(messageView); } } + + private void hideMessage(@NotNull final Message message) { + socialService.hideMessage(message); + Notification.show(getTranslation("social.message.contextmenu.hide.message.done")); + updateMessages(); + } + + private void blockProfile(@NotNull final Message message) { + socialService.hideProfile(message); + Notification.show(getTranslation("social.message.contextmenu.hide.profile.done")); + updateMessages(); + } } diff --git a/src/main/resources/vaadin-i18n/translations.properties b/src/main/resources/vaadin-i18n/translations.properties index 1e23d58..3bae801 100644 --- a/src/main/resources/vaadin-i18n/translations.properties +++ b/src/main/resources/vaadin-i18n/translations.properties @@ -7,26 +7,15 @@ conference.legend.running-session=running session conference.room.empty=closed conference.session.countdown.plural=ends in {0} minutes conference.session.countdown.singular=ends in {0} minute +social.admin.login.button=Login +social.admin.login.cancel=Cancel +social.admin.login.menu=Login as Administrator +social.admin.login.password=Password +social.admin.login.rejected=Your authentication was not successful! +social.admin.login.successful=You have administrative rights. +social.admin.login.title=Authentification social.heading=Posts with #{0} on Mastodon +social.message.contextmenu.hide.message.done=The message was hidden as requested. social.message.contextmenu.hide.message=Hide this message -social.message.dialog.hide.message.authorize.button=Hide -social.message.dialog.hide.message.authorize.cancel=Cancel -social.message.dialog.hide.message.authorize.password=Password -social.message.dialog.hide.message.authorize.title=Authorize -social.message.dialog.hide.message.confirm.button=Hide -social.message.dialog.hide.message.confirm.cancel=Cancel -social.message.dialog.hide.message.confirm.text=Do you really want to hide this message from {0} posted at {1}? -social.message.dialog.hide.message.confirm.title=Confirm -social.message.notification.hide.message.rejected=You are not authorized to hide messages! -social.message.notification.hide.message.success=This message from {0} was hidden as requested. +social.message.contextmenu.hide.profile.done=All messages from this profile have been hidden. social.message.contextmenu.hide.profile=Hide this profile -social.message.dialog.hide.profile.authorize.button=Hide -social.message.dialog.hide.profile.authorize.cancel=Cancel -social.message.dialog.hide.profile.authorize.password=Password -social.message.dialog.hide.profile.authorize.title=Authorize -social.message.dialog.hide.profile.confirm.button=Hide -social.message.dialog.hide.profile.confirm.cancel=Cancel -social.message.dialog.hide.profile.confirm.text=Do you really want to hide the profile {0} ({1})? -social.message.dialog.hide.profile.confirm.title=Confirm -social.message.notification.hide.profile.rejected=You are not authorized to hide profiles! -social.message.notification.hide.profile.success=The profile {0} ({1}) was hidden as requested. diff --git a/src/main/resources/vaadin-i18n/translations_de.properties b/src/main/resources/vaadin-i18n/translations_de.properties index 1ed982b..37bc7b3 100644 --- a/src/main/resources/vaadin-i18n/translations_de.properties +++ b/src/main/resources/vaadin-i18n/translations_de.properties @@ -7,26 +7,15 @@ conference.legend.running-session=laufender Vortrag conference.room.empty=geschlossen conference.session.countdown.plural=endet in {0} Minuten conference.session.countdown.singular=endet in {0} Minute +social.admin.login.button=Anmelden +social.admin.login.cancel=Abbrechen +social.admin.login.menu=Als Administrator anmelden +social.admin.login.password=Passwort +social.admin.login.rejected=Du hast dich nicht erfolgreich authentifiziert! +social.admin.login.successful=Du hast jetzt Administrationsrechte. +social.admin.login.title=Authentifizierung social.heading=Schreibe einen Beitrag mit #{0} auf Mastodon +social.message.contextmenu.hide.message.done=Der Beitrag wurde wie gewünscht ausgeblendet. social.message.contextmenu.hide.message=Diesen Beitrag ausblenden -social.message.dialog.hide.message.authorize.button=Ausblenden -social.message.dialog.hide.message.authorize.cancel=Abbrechen -social.message.dialog.hide.message.authorize.password=Passwort -social.message.dialog.hide.message.authorize.title=Authorisieren -social.message.dialog.hide.message.confirm.button=Ausblenden -social.message.dialog.hide.message.confirm.cancel=Abbrechen -social.message.dialog.hide.message.confirm.text=Möchtest du diesen am {1} geschriebenen Beitrag von {0} ausblenden? -social.message.dialog.hide.message.confirm.title=Bestätigen -social.message.notification.hide.message.rejected=Du bist nicht authorisiert, Beiträge auszublenden! -social.message.notification.hide.message.success=Dieser Beitrag von {0} wurde wie gewünscht ausgeblendet. +social.message.contextmenu.hide.profile.done=Alle Beiträge dieses Profils wurden wie gewünscht ausgeblendet. social.message.contextmenu.hide.profile=Dieses Profil ausblenden -social.message.dialog.hide.profile.authorize.button=Ausblenden -social.message.dialog.hide.profile.authorize.cancel=Abbrechen -social.message.dialog.hide.profile.authorize.password=Passwort -social.message.dialog.hide.profile.authorize.title=Authorisieren -social.message.dialog.hide.profile.confirm.button=Ausblenden -social.message.dialog.hide.profile.confirm.cancel=Abbrechen -social.message.dialog.hide.profile.confirm.text=Möchtest du das Profil {1} von {0} ausblenden? -social.message.dialog.hide.profile.confirm.title=Bestätigen -social.message.notification.hide.profile.rejected=Du bist nicht authorisiert, Profile auszublenden! -social.message.notification.hide.profile.success=Das Profile {0} von {1} wurde wie gewünscht ausgeblendet.