Lazy Chat: Implement editing of messages.
Change-Id: I291201da1fbc7c2b6563f0837f7ce3e2f7f8555c
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/Role.java b/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/Role.java
index 6298245..349322b 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/Role.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/Role.java
@@ -5,6 +5,7 @@
import eu.mulk.mulkcms2.benki.users.UserRole;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import java.util.Collection;
+import java.util.Objects;
import java.util.Set;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
@@ -78,4 +79,21 @@
public static Role getWorld() {
return find("from Role r join r.tags tag where tag = 'world'").singleResult();
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Role)) {
+ return false;
+ }
+ Role role = (Role) o;
+ return Objects.equals(id, role.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
index f2e3067..cb4c20f 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
@@ -3,10 +3,9 @@
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.TEXT_HTML;
-import eu.mulk.mulkcms2.benki.accesscontrol.Role;
+import eu.mulk.mulkcms2.benki.posts.Post;
import eu.mulk.mulkcms2.benki.posts.PostFilter;
import eu.mulk.mulkcms2.benki.posts.PostResource;
-import eu.mulk.mulkcms2.benki.users.User;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.api.ResourcePath;
@@ -21,9 +20,6 @@
import javax.json.JsonObject;
import javax.transaction.Transactional;
import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-import javax.validation.constraints.Pattern;
-import javax.ws.rs.BadRequestException;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@@ -51,12 +47,10 @@
@FormParam("uri") URI uri,
@FormParam("title") @NotEmpty String title,
@FormParam("description") String description,
- @FormParam("visibility") @NotNull @Pattern(regexp = "public|semiprivate|private")
- String visibility)
+ @FormParam("visibility") Post.Visibility visibility)
throws URISyntaxException {
- var userName = identity.getPrincipal().getName();
- var user = User.findByNickname(userName);
+ var user = getCurrentUser();
var bookmark = new Bookmark();
bookmark.uri = uri.toString();
@@ -66,14 +60,7 @@
bookmark.owner = user;
bookmark.date = OffsetDateTime.now();
- if (visibility.equals("public")) {
- Role world = Role.find("from Role r join r.tags tag where tag = 'world'").singleResult();
- bookmark.targets = Set.of(world);
- } else if (visibility.equals("semiprivate")) {
- bookmark.targets = Set.copyOf(user.defaultTargets);
- } else if (!visibility.equals("private")) {
- throw new BadRequestException(String.format("invalid visibility “%s”", visibility));
- }
+ assignPostTargets(visibility, user, bookmark);
bookmark.persistAndFlush();
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessage.java b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessage.java
index 918cad7..1e92c38 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessage.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessage.java
@@ -4,6 +4,7 @@
import eu.mulk.mulkcms2.common.markdown.MarkdownConverter;
import java.util.Collection;
import javax.annotation.CheckForNull;
+import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -22,9 +23,11 @@
public String format;
@OneToMany(mappedBy = "referrer", fetch = FetchType.LAZY)
+ @JsonbTransient
public Collection<LazychatReference> references;
@Transient
+ @JsonbTransient
public String getContentHtml() {
return new MarkdownConverter().htmlify(content);
}
@@ -43,6 +46,7 @@
@CheckForNull
@Override
+ @JsonbTransient
public String getDescriptionHtml() {
return getContentHtml();
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java
index 8a4d2a3..fd672f8 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java
@@ -1,21 +1,24 @@
package eu.mulk.mulkcms2.benki.lazychat;
-import eu.mulk.mulkcms2.benki.accesscontrol.Role;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+import eu.mulk.mulkcms2.benki.posts.Post;
import eu.mulk.mulkcms2.benki.posts.PostFilter;
import eu.mulk.mulkcms2.benki.posts.PostResource;
-import eu.mulk.mulkcms2.benki.users.User;
import io.quarkus.security.Authenticated;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.OffsetDateTime;
-import java.util.Set;
+import java.util.Objects;
import javax.transaction.Transactional;
-import javax.validation.constraints.NotNull;
-import javax.validation.constraints.Pattern;
-import javax.ws.rs.BadRequestException;
+import javax.ws.rs.ForbiddenException;
import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
@Path("/lazychat")
@@ -29,13 +32,10 @@
@Transactional
@Authenticated
public Response postMessage(
- @FormParam("text") String text,
- @FormParam("visibility") @NotNull @Pattern(regexp = "public|semiprivate|private")
- String visibility)
+ @FormParam("text") String text, @FormParam("visibility") Post.Visibility visibility)
throws URISyntaxException {
- var userName = identity.getPrincipal().getName();
- var user = User.findByNickname(userName);
+ var user = getCurrentUser();
var message = new LazychatMessage();
message.content = text;
@@ -43,17 +43,57 @@
message.owner = user;
message.date = OffsetDateTime.now();
- if (visibility.equals("public")) {
- Role world = Role.find("from Role r join r.tags tag where tag = 'world'").singleResult();
- message.targets = Set.of(world);
- } else if (visibility.equals("semiprivate")) {
- message.targets = Set.copyOf(user.defaultTargets);
- } else if (!visibility.equals("private")) {
- throw new BadRequestException(String.format("invalid visibility “%s”", visibility));
- }
+ assignPostTargets(visibility, user, message);
message.persistAndFlush();
return Response.seeOther(new URI("/lazychat")).build();
}
+
+ @POST
+ @Transactional
+ @Authenticated
+ @Path("/p/{id}/edit")
+ public Response patchMessage(
+ @PathParam("id") int id,
+ @FormParam("text") String text,
+ @FormParam("visibility") Post.Visibility visibility)
+ throws URISyntaxException {
+
+ var user = getCurrentUser();
+
+ var message = getSession().byId(LazychatMessage.class).load(id);
+
+ if (message == null) {
+ throw new NotFoundException();
+ }
+
+ if (!Objects.equals(message.owner.id, user.id)) {
+ throw new ForbiddenException();
+ }
+
+ message.content = text;
+ message.format = "markdown";
+
+ assignPostTargets(visibility, user, message);
+
+ return Response.seeOther(new URI("/lazychat")).build();
+ }
+
+ @GET
+ @Transactional
+ @Produces(APPLICATION_JSON)
+ @Path("/p/{id}")
+ public LazychatMessage getMessage(@PathParam("id") int id) {
+
+ var user = getCurrentUser();
+
+ var message = getSession().byId(LazychatMessage.class).load(id);
+
+ if (!user.canSee(message)) {
+ throw new ForbiddenException();
+ }
+
+ return message;
+ }
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
index 20aec05..356461c 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
@@ -13,6 +13,7 @@
import java.util.Objects;
import java.util.Set;
import javax.annotation.CheckForNull;
+import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -57,6 +58,7 @@
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner", referencedColumnName = "id")
+ @JsonbTransient
public User owner;
@ManyToMany(fetch = FetchType.LAZY)
@@ -65,6 +67,7 @@
schema = "benki",
joinColumns = @JoinColumn(name = "message"),
inverseJoinColumns = @JoinColumn(name = "user"))
+ @JsonbTransient
public Set<User> visibleTo;
@ManyToMany(fetch = FetchType.LAZY)
@@ -73,6 +76,7 @@
schema = "benki",
joinColumns = @JoinColumn(name = "message"),
inverseJoinColumns = @JoinColumn(name = "target"))
+ @JsonbTransient
public Set<Role> targets;
public abstract boolean isBookmark();
@@ -88,6 +92,18 @@
@CheckForNull
public abstract String getUri();
+ public Visibility getVisibility() {
+ if (targets.isEmpty()) {
+ return Visibility.PRIVATE;
+ } else if (targets.contains(Role.getWorld())) {
+ return Visibility.PUBLIC;
+ } else {
+ // FIXME: There should really be a check whether targets.equals(owner.defaultTargets) here.
+ // Otherwise the actual visibility is DISCRETIONARY.
+ return Visibility.SEMIPRIVATE;
+ }
+ }
+
protected static <T extends Post> CriteriaQuery<T> queryViewable(
Class<T> entityClass,
SecurityIdentity readerIdentity,
@@ -236,4 +252,28 @@
return new PostPage<T>(prevCursor, cursor, nextCursor, forwardResults);
}
+
+ public enum Visibility {
+ PUBLIC,
+ SEMIPRIVATE,
+ DISCRETIONARY,
+ PRIVATE,
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Post)) {
+ return false;
+ }
+ Post post = (Post) o;
+ return Objects.equals(id, post.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
index fbe6bf7..a691490 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
@@ -10,6 +10,7 @@
import com.rometools.rome.feed.synd.SyndPersonImpl;
import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.WireFeedOutput;
+import eu.mulk.mulkcms2.benki.accesscontrol.Role;
import eu.mulk.mulkcms2.benki.users.User;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateExtension;
@@ -26,6 +27,7 @@
import java.util.Comparator;
import java.util.Date;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
@@ -33,6 +35,7 @@
import javax.json.spi.JsonProvider;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
+import javax.ws.rs.BadRequestException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -70,7 +73,7 @@
@ConfigProperty(name = "mulkcms.tag-base")
String tagBase;
- @PersistenceContext EntityManager entityManager;
+ @PersistenceContext protected EntityManager entityManager;
private final PostFilter postFilter;
private final String pageTitle;
@@ -256,4 +259,29 @@
throw new IllegalStateException();
}
}
+
+ protected Session getSession() {
+ return entityManager.unwrap(Session.class);
+ }
+
+ protected static void assignPostTargets(Post.Visibility visibility, User user, Post post) {
+ switch (visibility) {
+ case PUBLIC:
+ post.targets = Set.of(Role.getWorld());
+ break;
+ case SEMIPRIVATE:
+ post.targets = Set.copyOf(user.defaultTargets);
+ break;
+ case PRIVATE:
+ post.targets = Set.of();
+ break;
+ default:
+ throw new BadRequestException(String.format("invalid visibility “%s”", visibility));
+ }
+ }
+
+ protected User getCurrentUser() {
+ var userName = identity.getPrincipal().getName();
+ return User.findByNickname(userName);
+ }
}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/users/User.java b/src/main/java/eu/mulk/mulkcms2/benki/users/User.java
index 5879046..ab89baa 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/users/User.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/users/User.java
@@ -5,6 +5,7 @@
import eu.mulk.mulkcms2.benki.bookmarks.Bookmark;
import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage;
import eu.mulk.mulkcms2.benki.posts.Post;
+import eu.mulk.mulkcms2.benki.posts.Post.Visibility;
import eu.mulk.mulkcms2.benki.wiki.WikiPageRevision;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import java.util.Collection;
@@ -141,4 +142,9 @@
public static User findByNickname(String nickname) {
return User.find("from BenkiUser u join u.nicknames n where ?1 = n", nickname).singleResult();
}
+
+ public boolean canSee(Post message) {
+ // FIXME: Make this more efficient.
+ return message.getVisibility() == Visibility.PUBLIC || visiblePosts.contains(message);
+ }
}
diff --git a/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js b/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
index 0a8fad7..3dd3754 100644
--- a/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
+++ b/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
@@ -10,7 +10,7 @@
<form class="pure-form" method="post" action="/bookmarks">
<fieldset>
- <legend>New Bookmark</legend>
+ <legend>Edit Bookmark</legend>
<label for="uri-input">URI:</label>
<input name="uri" id="uri-input" type="text" placeholder="URI" required />
@@ -24,9 +24,9 @@
<label for="visibility-input">Visibility:</label>
<select id="visibility-input" name="visibility" required>
- <option value="public" selected>Public</option>
- <option value="semiprivate">Semiprivate</option>
- <option value="private">Private</option>
+ <option value="PUBLIC" selected>Public</option>
+ <option value="SEMIPRIVATE">Semiprivate</option>
+ <option value="PRIVATE">Private</option>
</select>
<div class="controls">
diff --git a/src/main/resources/META-INF/resources/cms2/base.css b/src/main/resources/META-INF/resources/cms2/base.css
index 61f447c..c455ce8 100644
--- a/src/main/resources/META-INF/resources/cms2/base.css
+++ b/src/main/resources/META-INF/resources/cms2/base.css
@@ -187,6 +187,15 @@
min-width: calc(100% - 12em);
}
+elix-expandable-section.editor-pane::part(toggle) {
+ margin: 0;
+ display: inline;
+}
+
+elix-expandable-section.editor-pane::part(header) {
+ display: inline-block;
+}
+
.paging {
display: flex;
flex-direction: row;
diff --git a/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js b/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js
index a2bef8c..6cb3059 100644
--- a/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js
+++ b/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js
@@ -8,18 +8,18 @@
<link rel="stylesheet" type="text/css" href="/cms2/base.css" />
<link rel="stylesheet" type="text/css" href="/lazychat/MlkLazychatSubmissionForm.css" />
- <form class="pure-form" method="post" action="/lazychat">
+ <form id="main-form" class="pure-form" method="post" action="/lazychat">
<fieldset>
- <legend>New Message</legend>
+ <legend>Edit Message</legend>
<label for="text-input">Text:</label>
<textarea name="text" id="text-input" placeholder="Text"></textarea>
<label for="visibility-input">Visibility:</label>
<select id="visibility-input" name="visibility" required>
- <option value="public">Public</option>
- <option value="semiprivate" selected>Semiprivate</option>
- <option value="private">Private</option>
+ <option value="PUBLIC">Public</option>
+ <option value="SEMIPRIVATE" selected>Semiprivate</option>
+ <option value="PRIVATE">Private</option>
</select>
<div class="controls">
@@ -31,6 +31,8 @@
export class MlkLazychatSubmissionForm extends HTMLElement {
/*::
textInput: HTMLTextAreaElement;
+ visibilityInput: HTMLInputElement;
+ loaded: boolean;
*/
constructor() {
@@ -41,20 +43,57 @@
this.textInput =
cast(shadow.getElementById('text-input'));
+ this.visibilityInput =
+ cast(shadow.getElementById('visibility-input'));
+ this.loaded = false;
}
static get observedAttributes() {
return [];
}
- connectedCallback () {}
+ get editedId() {
+ return this.getAttribute("edited-id");
+ }
- disconnectedCallback () {}
+ get isEditor() {
+ return this.editedId !== null;
+ }
+
+ connectedCallback() {
+ if (this.isEditor) {
+ let form = this.shadowRoot.getElementById("main-form");
+ form.method = "post";
+ form.action = `/lazychat/p/${this.editedId}/edit`;
+ }
+ }
+
+ disconnectedCallback() {}
attributeChangedCallback(name /*:string*/, oldValue /*:string*/, newValue /*:string*/) {}
focus() {
this.textInput.focus();
+ this.load();
+ }
+
+ async load() {
+ if (!this.editedId || this.loaded) {
+ return;
+ }
+
+ let fetchUrl = new URL(`/lazychat/p/${this.editedId}`, document.URL);
+ let r = await fetch(fetchUrl);
+
+ if (!r.ok) {
+ return;
+ }
+
+ let post = await r.json();
+ this.textInput.value = post.content;
+ this.visibilityInput.value = post.visibility;
+
+ this.loaded = true;
}
}
diff --git a/src/main/resources/META-INF/resources/posts/postList.js b/src/main/resources/META-INF/resources/posts/postList.js
index 0578d7b..3eb23ce 100644
--- a/src/main/resources/META-INF/resources/posts/postList.js
+++ b/src/main/resources/META-INF/resources/posts/postList.js
@@ -10,4 +10,10 @@
let lazychatSubmissionForm = document.getElementById('lazychat-submission-form');
lazychatSubmissionPane.addEventListener('opened',() => lazychatSubmissionForm.focus());
}
+
+ let lazychatEditorPanes = document.getElementsByClassName('lazychat-editor-pane');
+ for (let pane of lazychatEditorPanes) {
+ let form = pane.getElementsByTagName('mlk-lazychat-submission-form')[0];
+ pane.addEventListener('opened', () => form.focus());
+ }
});
diff --git a/src/main/resources/templates/benki/posts/postList.html b/src/main/resources/templates/benki/posts/postList.html
index a29d886..8dd5210 100644
--- a/src/main/resources/templates/benki/posts/postList.html
+++ b/src/main/resources/templates/benki/posts/postList.html
@@ -1,4 +1,4 @@
-{@java.util.List<eu.mulk.mulkcms2.benki.bookmarks.Bookmark> posts}
+{@java.util.List<eu.mulk.mulkcms2.benki.posts.Post> posts}
{@java.lang.String pageTitle}
{@java.lang.Boolean showBookmarkForm}
{@java.lang.Boolean hasPreviousPage}
@@ -69,15 +69,24 @@
{#else}
<article class="lazychat-message">
<header>
- <div class="lazychat-message-info">
+ <div class="lazychat-message-info" style="display: inline-block">
<time datetime="{date.htmlDateTime}">{date.humanDateTime}</time>
<span class="lazychat-message-owner">{owner.firstName} {owner.lastName}</span>
</div>
+
+ {#if showLazychatForm}
+ <elix-expandable-section class="lazychat-editor-pane editor-pane">
+ <mlk-lazychat-submission-form edited-id="{post.id}"></mlk-lazychat-submission-form>
+ </elix-expandable-section>
+ {/if}
</header>
<section class="lazychat-message-content">
{contentHtml.raw}
</section>
+
+ <section class="lazychat-editor">
+ </section>
</article>
{/if}
{/with}