diff options
author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2020-04-13 17:01:35 +0200 |
---|---|---|
committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2020-04-13 17:01:35 +0200 |
commit | 06e6c81c77f8098693473e49c11557820541dd15 (patch) | |
tree | 90093ef738927496a5c3f48540ed62871b565bbb | |
parent | 7f4daccab9dc21cfd95be219e5c8c86545d47125 (diff) |
Lazy Chat: Implement editing of messages.
Change-Id: I291201da1fbc7c2b6563f0837f7ce3e2f7f8555c
12 files changed, 236 insertions, 50 deletions
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.User; 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 class Role extends PanacheEntityBase { 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 @@ package eu.mulk.mulkcms2.benki.bookmarks; 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.inject.Inject; 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 @@ public class BookmarkResource extends PostResource { @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 @@ public class BookmarkResource extends PostResource { 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.benki.posts.Post; 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 class LazychatMessage extends Post { 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 @@ public class LazychatMessage extends Post { @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 @@ public class LazychatResource extends PostResource { @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 @@ public class LazychatResource extends PostResource { 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.List; 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 @@ public abstract class Post extends PanacheEntityBase { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner", referencedColumnName = "id") + @JsonbTransient public User owner; @ManyToMany(fetch = FetchType.LAZY) @@ -65,6 +67,7 @@ public abstract class Post extends PanacheEntityBase { schema = "benki", joinColumns = @JoinColumn(name = "message"), inverseJoinColumns = @JoinColumn(name = "user")) + @JsonbTransient public Set<User> visibleTo; @ManyToMany(fetch = FetchType.LAZY) @@ -73,6 +76,7 @@ public abstract class Post extends PanacheEntityBase { schema = "benki", joinColumns = @JoinColumn(name = "message"), inverseJoinColumns = @JoinColumn(name = "target")) + @JsonbTransient public Set<Role> targets; public abstract boolean isBookmark(); @@ -88,6 +92,18 @@ public abstract class Post extends PanacheEntityBase { @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 @@ public abstract class Post extends PanacheEntityBase { 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.atom.Link; 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.time.temporal.TemporalAccessor; 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.inject.Inject; 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 @@ public abstract class PostResource { @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 @@ public abstract class PostResource { 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.accesscontrol.Role; 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 class User extends PanacheEntityBase { 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 @@ template.innerHTML = ` <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 @@ template.innerHTML = ` <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 @@ article.lazychat-message { 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 @@ template.innerHTML = ` <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 @@ template.innerHTML = ` export class MlkLazychatSubmissionForm extends HTMLElement { /*:: textInput: HTMLTextAreaElement; + visibilityInput: HTMLInputElement; + loaded: boolean; */ constructor() { @@ -41,20 +43,57 @@ export class MlkLazychatSubmissionForm extends HTMLElement { 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"); + } + + get isEditor() { + return this.editedId !== null; + } - disconnectedCallback () {} + 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 @@ document.addEventListener('DOMContentLoaded', () => { 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} |