diff --git a/README.md b/README.md
index f47311d..dc37eb0 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,7 @@ You can now also attach breakpoints in code for debugging purposes, by clicking
| Variable | Default | Description |
|-------------------|------------------------------------|-----------------------------------------------------------------|
+| ADMIN_PASSWORD | | The password to get admin access (empty = disabled). |
| DOAG_EVENT_ID | 0 | The ID of the DOAG event to read the conference agenda. |
| FILTER_REPLIES | true | Hide social media messages which are replies. |
| FILTER_SENSITIVE | true | Hide social media messages which contain sensitive information. |
diff --git a/src/main/java/swiss/fihlon/apus/configuration/Admin.java b/src/main/java/swiss/fihlon/apus/configuration/Admin.java
new file mode 100644
index 0000000..77fbd68
--- /dev/null
+++ b/src/main/java/swiss/fihlon/apus/configuration/Admin.java
@@ -0,0 +1,22 @@
+/*
+ * Apus - A social wall for conferences with additional features.
+ * Copyright (C) Marcus Fihlon and the individual contributors to Apus.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package swiss.fihlon.apus.configuration;
+
+import org.jetbrains.annotations.NotNull;
+
+public record Admin(@NotNull String password) { }
diff --git a/src/main/java/swiss/fihlon/apus/configuration/Configuration.java b/src/main/java/swiss/fihlon/apus/configuration/Configuration.java
index 1413499..7c8a54c 100644
--- a/src/main/java/swiss/fihlon/apus/configuration/Configuration.java
+++ b/src/main/java/swiss/fihlon/apus/configuration/Configuration.java
@@ -28,6 +28,7 @@
public class Configuration {
private String version;
+ private Admin admin;
private DOAG doag;
private Mastodon mastodon;
private Filter filter;
@@ -39,6 +40,14 @@ public void setVersion(@NotNull final String version) {
this.version = version;
}
+ public Admin getAdmin() {
+ return admin;
+ }
+
+ public void setAdmin(@NotNull final Admin admin) {
+ this.admin = admin;
+ }
+
public DOAG getDoag() {
return doag;
}
diff --git a/src/main/java/swiss/fihlon/apus/service/SocialService.java b/src/main/java/swiss/fihlon/apus/service/SocialService.java
index 1b0f444..3a5e511 100644
--- a/src/main/java/swiss/fihlon/apus/service/SocialService.java
+++ b/src/main/java/swiss/fihlon/apus/service/SocialService.java
@@ -20,6 +20,8 @@
import jakarta.annotation.PreDestroy;
import org.jetbrains.annotations.NotNull;
import org.jsoup.Jsoup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;
import swiss.fihlon.apus.configuration.Configuration;
@@ -27,9 +29,12 @@
import swiss.fihlon.apus.social.mastodon.MastodonAPI;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import java.util.concurrent.ScheduledFuture;
@Service
@@ -37,6 +42,7 @@ public final class SocialService {
private static final Duration UPDATE_FREQUENCY = Duration.ofMinutes(1);
private static final Locale DEFAULT_LOCALE = Locale.getDefault();
+ private static final Logger LOGGER = LoggerFactory.getLogger(SocialService.class);
private final ScheduledFuture> updateScheduler;
private final MastodonAPI mastodonAPI;
@@ -44,6 +50,7 @@ public final class SocialService {
private final boolean filterReplies;
private final boolean filterSensitive;
private final List filterWords;
+ private final Set manuallyHiddenId = new HashSet<>();
private List messages = List.of();
public SocialService(@NotNull final TaskScheduler taskScheduler,
@@ -66,12 +73,13 @@ public void stopUpdateScheduler() {
private void updateMessages() {
final var newMessages = mastodonAPI.getMessages(hashtag).stream()
+ .filter(message -> !manuallyHiddenId.contains(message.id()))
.filter(message -> !filterSensitive || !message.isSensitive())
.filter(message -> !filterReplies || !message.isReply())
.filter(this::checkWordFilter)
.toList();
synchronized (this) {
- messages = newMessages;
+ messages = new ArrayList<>(newMessages);
}
}
@@ -95,4 +103,10 @@ public List getMessages(final int limit) {
}
}
+ public void hideMessage(@NotNull final Message message) {
+ LOGGER.warn("Hiding message (id={}, profile={}, author={})",
+ message.id(), message.profile(), message.author());
+ messages.remove(message);
+ manuallyHiddenId.add(message.id());
+ }
}
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 9d50ea7..05f5645 100644
--- a/src/main/java/swiss/fihlon/apus/ui/view/MessageView.java
+++ b/src/main/java/swiss/fihlon/apus/ui/view/MessageView.java
@@ -22,14 +22,23 @@
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.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")
@@ -37,8 +46,15 @@ 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) {
+ public MessageView(@NotNull final Message message,
+ @NotNull final SocialService socialService,
+ @NotNull final Configuration configuration) {
+ this.socialService = socialService;
+ this.configuration = configuration;
+ setId("message-" + message.id());
addClassName("message-view");
add(createHeaderComponent(message));
add(createTextComponent(message));
@@ -47,7 +63,7 @@ public MessageView(@NotNull final Message message) {
}
@NotNull Component createHeaderComponent(@NotNull final Message message) {
- final var avatar = new Avatar(message.author(), message.avatar());
+ final var avatar = createAvatarComponent(message);
final var author = new Div(new Text(message.author()));
author.addClassName("author");
final var profile = new Div(new Text(message.profile()));
@@ -57,6 +73,73 @@ public MessageView(@NotNull final Message message) {
return new Header(avatar, authorContainer);
}
+ 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("Hide", event -> confirmHideMessage(message));
+ menu.setTarget(avatar);
+ }
+ return avatar;
+ }
+
+ private void confirmHideMessage(@NotNull final Message message) {
+ final var dialog = new ConfirmDialog();
+ dialog.setHeader("Confirm hide message");
+ dialog.setText(String.format("Do you really want to hide the message from %s posted at %s?", message.author(), message.date()));
+ dialog.setCloseOnEsc(true);
+
+ dialog.setCancelable(true);
+ dialog.addCancelListener(event -> dialog.close());
+
+ dialog.setConfirmText("Hide");
+ dialog.addConfirmListener(event -> {
+ dialog.close();
+ authorizeHideMessage(message);
+ });
+
+ dialog.open();
+ }
+
+ private void authorizeHideMessage(@NotNull final Message message) {
+ final Dialog dialog = new Dialog();
+ dialog.setHeaderTitle("Authorize hide message");
+ dialog.setCloseOnEsc(true);
+ dialog.setCloseOnOutsideClick(true);
+
+ final PasswordField passwordField = new PasswordField();
+ passwordField.setPlaceholder("password");
+ passwordField.setRequired(true);
+ passwordField.setValueChangeMode(ValueChangeMode.EAGER);
+
+ final Button hideButton = new Button("Hide", event -> {
+ dialog.close();
+ hideMessage(message, passwordField.getValue());
+ });
+ hideButton.setEnabled(false);
+ hideButton.setDisableOnClick(true);
+
+ final Button cancelButton = new Button("Cancel", event -> dialog.close());
+ cancelButton.setDisableOnClick(true);
+ 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("The message was hidden as requested.");
+ } else {
+ Notification.show("You are not authorized to hide messages!");
+ }
+ }
+
@NotNull
private Component createTextComponent(@NotNull final Message message) {
final String messageText = Jsoup.parse(message.html()).text();
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 003e4ea..03ede1a 100644
--- a/src/main/java/swiss/fihlon/apus/ui/view/SocialView.java
+++ b/src/main/java/swiss/fihlon/apus/ui/view/SocialView.java
@@ -35,12 +35,14 @@ public final class SocialView extends Div {
private static final Duration UPDATE_FREQUENCY = Duration.ofMinutes(1);
private final transient SocialService socialService;
+ private final transient Configuration configuration;
private final Div messageContainer = new Div();
public SocialView(@NotNull final SocialService socialService,
@NotNull final TaskScheduler taskScheduler,
@NotNull final Configuration configuration) {
this.socialService = socialService;
+ this.configuration = configuration;
setId("social-view");
add(new H2(getTranslation("social.heading", configuration.getMastodon().hashtag())));
@@ -58,7 +60,7 @@ private void updateScheduler() {
private void updateMessages() {
messageContainer.removeAll();
for (final Message message : socialService.getMessages(30)) {
- messageContainer.add(new MessageView(message));
+ messageContainer.add(new MessageView(message, socialService, configuration));
}
}
}
diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 0815139..8e83595 100644
--- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -29,5 +29,10 @@
"name": "apus.filter.words",
"type": "java.lang.String",
"description": "Hide social media messages which contain these words."
+ },
+ {
+ "name": "apus.admin.password",
+ "type": "java.lang.String",
+ "description": "The password to get admin access (empty = disabled)."
}
] }
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 3235483..2661ffe 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -9,6 +9,7 @@ vaadin.launch-browser=false
vaadin.frontend.hotdeploy=true
apus.version=@project.version@
+apus.admin.password=${ADMIN_PASSWORD:}
apus.doag.eventId=${DOAG_EVENT_ID:0}
apus.mastodon.instance=${MASTODON_INSTANCE:mastodon.social}
apus.mastodon.hashtag=${MASTODON_HASHTAG:java}