diff mbox series

[bug#68680,mumi,v3,4/4] html: Add a button to copy a Message-ID to the clipboard.

Message ID 20240126022717.31305-5-maxim.cournoyer@gmail.com
State New
Headers show
Series Add a button to copy a message Message-ID to the clipboard. | expand

Commit Message

Maxim Cournoyer Jan. 26, 2024, 2:26 a.m. UTC
* mumi/web/view/html.scm (issue-page)
<copy-message-id-button>: New HTML element.
(layout): Bump timestamp on .css and .js files to force reload.
* mumi/web/view/utils.scm (download-icon): Update source.  Use 'rem'
as size unit.
(copy-icon): New variable.
(display-message-body) <download-part>: Add IDs to buttons.  Add to
"message-button" class.
* assets/mumi.scss: Refactor spacing of message header buttons via a
flex display.  Add a shrinking animation to message buttons on hover.
* assets/js/mumi.js (mumi): Register an event for it that copies the
Message-ID to the clipboard.  Add js-indent-level prop line as well as
copyright notices.
* mumi/web/view/html.scm

---

Changes in v3:
 - Allow using the new copy button via the keyboard
 - Register event handlers on all copy message-id buttons
 - Move download icon sizes to CSS to resolve warning in Firefox
 - Add guard inside download button event to ensure only one event
   runs at a time, avoiding tooltip getting stuck on 'Copied!'
 - Use a class name instead of a unique ID for the message-id buttons
 - Register handlers to every message-id buttons

Changes in v2:
 - Add timestamp to CSS and JavaScript file names to force reload

 assets/js/mumi.js       | 59 ++++++++++++++++++++++++++++++++++++++++-
 assets/mumi.scss        | 28 ++++++++++++++++---
 mumi/web/view/html.scm  | 24 ++++++++++++-----
 mumi/web/view/utils.scm | 40 +++++++++++++++++-----------
 4 files changed, 126 insertions(+), 25 deletions(-)
diff mbox series

Patch

diff --git a/assets/js/mumi.js b/assets/js/mumi.js
index ab29f08..77b9276 100644
--- a/assets/js/mumi.js
+++ b/assets/js/mumi.js
@@ -1,4 +1,8 @@ 
-// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3.0-or-later
+// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3.0-or-later  -*- js-indent-level: 2; -*-
+//
+// Copyright © 2019, 2022, 2023 Ricardo Wurmus <rekado@elephly.net>
+// Copyright © 2024 Maxim Cournoyer <maxim.cournoyer@gmail.com>
+//
 var mumi = (function () {
   const possibleTokens = [
     { text: 'is:open' },
@@ -129,12 +133,65 @@  var mumi = (function () {
   var init = function () {
     initTokenInput ();
   };
+
+  // Copy a message Message-ID to the clipboard.
+  var setupMessageIdButtonHandler = () => {
+
+    // Avoid having the async timeout code starting while a previous
+    // one is ongoing, which would corrupt the tooltip text.
+    var isMessageIdHandlerRunning = false;
+
+    let messageIdHandler = (evt) => {
+      if (isMessageIdHandlerRunning) return;
+      isMessageIdHandlerRunning = true;
+
+      // If the button was triggered by a keyup event, check if it was
+      // the Enter key.
+      if (evt.type === "keyup" && event.key !== "Enter") return;
+
+      messageIdButton = evt.currentTarget;
+      originalTooltip = messageIdButton.dataset.tooltip;
+      var messageId = messageIdButton.dataset.messageId;
+      if (!window.isSecureContext) {
+	console.log("not in a secure context -- " +
+		    "cannot copy message-id to clipboard\n" +
+		    "tip: for testing, open via http://localhost:1234");
+	return;
+      }
+
+      // Avoid the button being stuck in the focused state when a
+      // mouse is used, to avoid UI clutter.
+      if (evt.type === "click") {
+	messageIdButton.removeAttribute('tabindex');
+      }
+
+      // Update button's tooltip for the next 3 s.
+      messageIdButton.dataset.tooltip = "Copied!";
+      setTimeout(() => {
+	messageIdButton.dataset.tooltip = originalTooltip;
+	if (evt.type === "click") {
+	  // Re-add the tabindex attribute.
+	  messageIdButton.setAttribute("tabindex", "0")
+	  isMessageIdHandlerRunning = false;
+	}}, 3000);
+
+      navigator.clipboard.writeText(messageId);
+      console.log("copied Message-ID " + messageId + " to clipboard");
+    };
+
+    document.querySelectorAll(".copy-message-id-button").forEach((btn) => {
+      btn.addEventListener("click", messageIdHandler);
+      btn.addEventListener("keyup", messageIdHandler)});
+  };
+
   return({
     'init': init,
     'lines': setupLineHandler,
+    'messageIdButtonHandler': setupMessageIdButtonHandler,
   });
 })();
 
 window.addEventListener ("load", mumi.init);
 window.addEventListener ("DOMContentLoaded", mumi.lines);
+window.addEventListener ("DOMContentLoaded", mumi.messageIdButtonHandler);
 // @license-end
diff --git a/assets/mumi.scss b/assets/mumi.scss
index 822f110..b12a733 100644
--- a/assets/mumi.scss
+++ b/assets/mumi.scss
@@ -506,11 +506,33 @@  details {
     margin-right: 0.2em;
 }
 
-.download-message,
 .download-part {
     float: right;
-    font-size: 0.8em;
-    font-style: italic;
+}
+
+.header-buttons {
+    display: flex;
+    flex-direction: row;
+    float: right;
+    justify-content: flex-end;
+ }
+
+.header-buttons > * {
+    margin: 0 0 0 0.5rem;
+    // Custom buttons: undo the Pico CSS default style.
+    background: revert;
+    border: revert;
+    color: revert;
+    padding: revert;
+}
+
+.message-button {
+    height: 1rem;
+    width: 1rem;
+}
+
+.message-button:hover {
+    transform: scale(0.95);
 }
 
 @media (min-width: 768px) {
diff --git a/mumi/web/view/html.scm b/mumi/web/view/html.scm
index 8f06a36..8ae1a6f 100644
--- a/mumi/web/view/html.scm
+++ b/mumi/web/view/html.scm
@@ -1,6 +1,7 @@ 
 ;;; mumi -- Mediocre, uh, mail interface
 ;;; Copyright © 2016, 2017, 2018, 2019, 2020, 2021, 2022 Ricardo Wurmus <rekado@elephly.net>
 ;;; Copyright © 2018, 2019, 2023 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2024 Maxim Cournoyer <maxim.cournoyer@gmail.com>
 ;;;
 ;;; This program is free software: you can redistribute it and/or
 ;;; modify it under the terms of the GNU Affero General Public License
@@ -62,11 +63,11 @@ 
        (@ (rel "stylesheet")
           (media "screen")
           (type "text/css")
-          (href "/css/mumi.css?20221231000000")))
+          (href "/css/mumi.css?20240125000002")))
       (script
        (@ (src "/js/tokeninput.js")))
       (script
-       (@ (src "/js/mumi.js")))
+       (@ (src "/js/mumi.js?20240125000002")))
       ,@head)
      (body (script
             (@ (src "/js/theme-switcher.js")))
@@ -612,6 +613,7 @@  currently disabled."))
                 (not (bug-archived bug)))
            mailer-form
            disabled-mailer)))
+
   (define (show-message message-number message previous-subject)
     `((div
        (a (@ (class "message-anchor")
@@ -642,10 +644,20 @@  currently disabled."))
                                                  message-number)))
                       (title ,(date->string (date message))))
                    ,(time->string (date message)))))
-         (div (@ (class "download-message"))
-              (a (@ (href ,(format #f "/issue/~a/raw/~a"
-                                   id message-number)))
-                 ,download-icon))
+         (div (@ (class "header-buttons"))
+              (div (@ (class "copy-message-id-button message-button")
+                      (tabindex "0")    ;make it keyboard-usable
+                      (role "button")   ;specific to Pico CSS
+                      (data-tooltip "Copy Message-ID")
+                      (data-message-id ,(message-id message)))
+                   ,copy-icon)
+              (div (@ (id "download-raw-message-button")
+                      (class "message-button")
+                      (role "button")
+                      (data-tooltip "Download raw message"))
+                   (a (@ (href ,(format #f "/issue/~a/raw/~a"
+                                        id message-number)))
+                      ,download-icon)))
          ,@(if (string-suffix? previous-subject (subject message))
                '()
                `((div (@ (class "subject")) ,(subject message))))
diff --git a/mumi/web/view/utils.scm b/mumi/web/view/utils.scm
index 1ce7b64..712d198 100644
--- a/mumi/web/view/utils.scm
+++ b/mumi/web/view/utils.scm
@@ -2,6 +2,7 @@ 
 ;;; Copyright © 2017, 2018, 2019, 2020 Ricardo Wurmus <rekado@elephly.net>
 ;;; Copyright © 2018, 2019 Arun Isaac <arunisaac@systemreboot.net>
 ;;; Copyright © 2018 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2024 Maxim Cournoyer <maxim.cournoyer@gmail.com>
 ;;;
 ;;; This program is free software: you can redistribute it and/or
 ;;; modify it under the terms of the GNU Affero General Public License
@@ -35,6 +36,7 @@ 
   #:use-module (web uri)
   #:export (prettify
             avatar-color
+            copy-icon
             download-icon
             display-message-body
             time->string))
@@ -215,23 +217,27 @@  numbers with the given MESSAGE-NUMBER."
 ;; https://icons.getbootstrap.com/icons/download/
 (define download-icon
   '(svg (@ (class "bi bi-download")
-           (width "1em")
-           (height "1em")
            (viewBox "0 0 16 16")
            (fill "currentColor")
            (xmlns "http://www.w3.org/2000/svg"))
-        (title "Download")
-        (path (@ (fill-rule "evenodd")
-                 (clip-rule "evenodd")
-                 (d "M.5 8a.5.5 0 01.5.5V12a1 1 0 001 1h12a1 1 0 001-1\
-V8.5a.5.5 0 011 0V12a2 2 0 01-2 2H2a2 2 0 01-2-2V8.5A.5.5 0 01.5 8z")) "")
-        (path (@ (fill-rule "evenodd")
-                 (clip-rule "evenodd")
-                 (d "M5 7.5a.5.5 0 01.707 0L8 9.793 10.293 7.5a.5.5 0 \
-11.707.707l-2.646 2.647a.5.5 0 01-.708 0L5 8.207A.5.5 0 015 7.5z")) "")
+        (path (@ (d "M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 \
+1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 \
+1-2-2v-2.5a.5.5 0 0 1 .5-.5")))
+        (path (@ (d "M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 \
+0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 \
+1 0-.708.708z")))))
+
+;; https://icons.getbootstrap.com/icons/copy/
+(define copy-icon
+  '(svg (@ (class "bi bi-copy")
+           (viewBox "0 0 16 16")
+           (fill "currentColor")
+           (xmlns "http://www.w3.org/2000/svg"))
         (path (@ (fill-rule "evenodd")
-                 (clip-rule "evenodd")
-                 (d "M8 1a.5.5 0 01.5.5v8a.5.5 0 01-1 0v-8A.5.5 0 018 1z")) "")))
+                 (d "M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 \
+2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 \
+0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 \
+2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z")))))
 
 (define* (display-message-body bug-num message-number message #:optional plain?)
   "Convenience procedure to render MESSAGE (part of bug with number
@@ -285,7 +291,9 @@  lines when PLAIN? is #T."
                    "")))
        ((string-suffix? ".scm" attachment-name)
         `(div (@ (class "multipart scheme"))
-              (div (@ (class "download-part"))
+              (div (@ (id "download-scheme-part-button")
+                      (class "download-part message-button")
+                      (data-tooltip "Download Scheme file"))
                    (a (@ (href ,(attachment-url)))
                       ,download-icon))
               ,(highlights->sxml (highlight lex-scheme body))))
@@ -294,7 +302,9 @@  lines when PLAIN? is #T."
                           (list "multipart" (or (and content-type
                                                      (content-type->css-class content-type))
                                                 "")))))
-              (div (@ (class "download-part"))
+              (div (@ (id "download-part-button")
+                      (class "download-part message-button")
+                      (data-tooltip "Download MIME part"))
                    (a (@ (href ,(attachment-url)))
                       ,download-icon))
               ,(cond