Enable lazy chat message submission.

Change-Id: I9e9060e29bb63a78591f618cc54acdfb5b49575f
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 fcea278..df1fd55 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
@@ -109,6 +109,7 @@
         .data("feedUri", "/bookmarks/feed")
         .data("pageTitle", "Bookmarks")
         .data("showBookmarkForm", false)
+        .data("showLazychatForm", false)
         .data("authenticated", !identity.isAnonymous())
         .data("hasPreviousPage", q.prevCursor != null)
         .data("hasNextPage", q.nextCursor != null)
@@ -136,6 +137,7 @@
         .data("feedUri", String.format("/bookmarks/~%s/feed", ownerName))
         .data("pageTitle", "Bookmarks")
         .data("showBookmarkForm", false)
+        .data("showLazychatForm", false)
         .data("authenticated", !identity.isAnonymous())
         .data("hasPreviousPage", q.prevCursor != null)
         .data("hasNextPage", q.nextCursor != null)
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 74b0b2a..173fb10 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatResource.java
@@ -2,25 +2,38 @@
 
 import static javax.ws.rs.core.MediaType.TEXT_HTML;
 
+import eu.mulk.mulkcms2.benki.accesscontrol.Role;
 import eu.mulk.mulkcms2.benki.users.User;
 import io.quarkus.qute.Template;
 import io.quarkus.qute.TemplateExtension;
 import io.quarkus.qute.TemplateInstance;
 import io.quarkus.qute.api.ResourcePath;
+import io.quarkus.security.Authenticated;
 import io.quarkus.security.identity.SecurityIdentity;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
 import java.time.temporal.TemporalAccessor;
+import java.util.Set;
 import javax.annotation.CheckForNull;
 import javax.inject.Inject;
 import javax.json.spi.JsonProvider;
 import javax.persistence.EntityManager;
 import javax.persistence.PersistenceContext;
+import javax.transaction.Transactional;
+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;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.hibernate.Session;
 import org.jboss.logging.Logger;
@@ -63,6 +76,7 @@
         .data("posts", q.posts)
         .data("pageTitle", "Lazy Chat")
         .data("showBookmarkForm", false)
+        .data("showLazychatForm", true)
         .data("hasPreviousPage", q.prevCursor != null)
         .data("hasNextPage", q.nextCursor != null)
         .data("previousCursor", q.prevCursor)
@@ -88,6 +102,7 @@
         .data("posts", q.posts)
         .data("pageTitle", "Lazy Chat")
         .data("showBookmarkForm", false)
+        .data("showLazychatForm", true)
         .data("hasPreviousPage", q.prevCursor != null)
         .data("hasNextPage", q.nextCursor != null)
         .data("previousCursor", q.prevCursor)
@@ -95,6 +110,38 @@
         .data("pageSize", maxResults);
   }
 
+  @POST
+  @Transactional
+  @Authenticated
+  public Response postMessage(
+      @FormParam("text") String text,
+      @FormParam("visibility") @NotNull @Pattern(regexp = "public|semiprivate|private")
+          String visibility)
+      throws URISyntaxException {
+
+    var userName = identity.getPrincipal().getName();
+    var user = User.findByNickname(userName);
+
+    var message = new LazychatMessage();
+    message.content = text;
+    message.format = "markdown";
+    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));
+    }
+
+    message.persistAndFlush();
+
+    return Response.seeOther(new URI("/lazychat")).build();
+  }
+
   @TemplateExtension
   static String humanDateTime(TemporalAccessor x) {
     return humanDateFormatter.format(x);
diff --git a/src/main/resources/META-INF/resources/bookmarks/bookmarkList.js b/src/main/resources/META-INF/resources/bookmarks/bookmarkList.js
deleted file mode 100644
index a6f78e6..0000000
--- a/src/main/resources/META-INF/resources/bookmarks/bookmarkList.js
+++ /dev/null
@@ -1,9 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
-  let bookmarkSubmissionPane = document.getElementById(
-      'bookmark-submission-pane');
-  let bookmarkSubmissionForm = document.getElementById(
-      'bookmark-submission-form');
-
-  bookmarkSubmissionPane.addEventListener('opened', () => bookmarkSubmissionForm.focus());
-});
-
diff --git a/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.css b/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.css
new file mode 100644
index 0000000..007a172
--- /dev/null
+++ b/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.css
@@ -0,0 +1,30 @@
+fieldset {
+  display: grid;
+  grid-template-columns: 1fr;
+  grid-gap: 5px;
+}
+
+label,
+input,
+button,
+textarea {
+  grid-column: 1 / 2;
+}
+
+@media (min-width: 30em) {
+  fieldset {
+    display: grid;
+    grid-template-columns: 1fr 5fr;
+    grid-gap: 0;
+  }
+
+  label {
+    grid-column: 1 / 2;
+  }
+
+  input,
+  button,
+  textarea {
+    grid-column: 2 / 3;
+  }
+}
diff --git a/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js b/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js
new file mode 100644
index 0000000..8c85d72
--- /dev/null
+++ b/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js
@@ -0,0 +1,61 @@
+// @flow
+
+import /*:: ProgressSpinner from */ "../web_modules/elix/define/ProgressSpinner.js";
+import { cast } from "../cms2/types.js";
+
+const template = document.createElement('template');
+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">
+    <fieldset>
+      <legend>New 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" selected>Public</option>
+        <option value="semiprivate">Semiprivate</option>
+        <option value="private">Private</option>
+      </select>
+
+      <div class="controls">
+        <button type="submit" class="pure-button pure-button-primary">Submit Message</button>
+      </div>
+    </fieldset>
+  </form>`;
+
+export class MlkLazychatSubmissionForm extends HTMLElement {
+  /*::
+  textInput: HTMLTextAreaElement;
+  */
+
+  constructor() {
+    super();
+
+    let shadow = this.attachShadow({mode: "open"});
+    shadow.appendChild(template.content.cloneNode(true));
+
+    this.textInput =
+        cast(shadow.getElementById('text-input'));
+  }
+
+  static get observedAttributes() {
+    return [];
+  }
+
+  connectedCallback () {}
+
+  disconnectedCallback () {}
+
+  attributeChangedCallback(name /*:string*/, oldValue /*:string*/, newValue /*:string*/) {}
+
+  focus() {
+    this.textInput.focus();
+  }
+}
+
+customElements.define("mlk-lazychat-submission-form", MlkLazychatSubmissionForm);
diff --git a/src/main/resources/META-INF/resources/lazychat/newLazychatMessage.js b/src/main/resources/META-INF/resources/lazychat/newLazychatMessage.js
new file mode 100644
index 0000000..ed5072f
--- /dev/null
+++ b/src/main/resources/META-INF/resources/lazychat/newLazychatMessage.js
@@ -0,0 +1,4 @@
+document.addEventListener('DOMContentLoaded', () => {
+  let bookmarkSubmissionForm = document.getElementById('lazychat-submission-form');
+  bookmarkSubmissionForm.focus();
+});
diff --git a/src/main/resources/META-INF/resources/posts/postList.js b/src/main/resources/META-INF/resources/posts/postList.js
new file mode 100644
index 0000000..0578d7b
--- /dev/null
+++ b/src/main/resources/META-INF/resources/posts/postList.js
@@ -0,0 +1,13 @@
+document.addEventListener('DOMContentLoaded', () => {
+  let bookmarkSubmissionPane = document.getElementById('bookmark-submission-pane');
+  if (bookmarkSubmissionPane) {
+    let bookmarkSubmissionForm = document.getElementById('bookmark-submission-form');
+    bookmarkSubmissionPane.addEventListener('opened',() => bookmarkSubmissionForm.focus());
+  }
+
+  let lazychatSubmissionPane = document.getElementById('lazychat-submission-pane');
+  if (lazychatSubmissionPane) {
+    let lazychatSubmissionForm = document.getElementById('lazychat-submission-form');
+    lazychatSubmissionPane.addEventListener('opened',() => lazychatSubmissionForm.focus());
+  }
+});
diff --git a/src/main/resources/templates/benki/posts/postList.html b/src/main/resources/templates/benki/posts/postList.html
index b68f796..a29d886 100644
--- a/src/main/resources/templates/benki/posts/postList.html
+++ b/src/main/resources/templates/benki/posts/postList.html
@@ -19,7 +19,8 @@
 
   <script type="module" src="/web_modules/elix/define/ExpandableSection.js"></script>
   <script type="module" src="/bookmarks/MlkBookmarkSubmissionForm.js"></script>
-  <script type="module" src="/bookmarks/bookmarkList.js" defer></script>
+  <script type="module" src="/lazychat/MlkLazychatSubmissionForm.js"></script>
+  <script type="module" src="/posts/postList.js" defer></script>
 {/head}
 
 {#body}
@@ -33,6 +34,15 @@
   </elix-expandable-section>
 {/if}
 
+{#if showLazychatForm}
+  <elix-expandable-section id="lazychat-submission-pane">
+    <h2 slot="header" class="small-title expandable-section-title"><button class="pure-button">Post Message</button></h2>
+    <section id="lazychat-submission">
+      <mlk-lazychat-submission-form id="lazychat-submission-form"></mlk-lazychat-submission-form>
+    </section>
+  </elix-expandable-section>
+{/if}
+
 <div class="paging">
   {#if hasPreviousPage}<a href="?i={previousCursor}&n={pageSize}" class="pure-button">⇠ previous page</a>{/if}
   <span class="filler"></span>