You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ponymail.apache.org by rb...@apache.org on 2026/08/08 16:35:20 UTC

(ponymail-foal) 01/02: UI element to copy permalink

This is an automated email from the ASF dual-hosted git repository.

rbowen pushed a commit to branch copy-permalink
in repository https://gitbox.apache.org/repos/asf/ponymail-foal.git

commit ee25ce7d764c38d8f8c18d639ed2494f8b6c20c7
Author: Rich Bowen <rb...@rcbowen.com>
AuthorDate: Sat Aug 8 12:34:12 2026 -0400

    UI element to copy permalink
---
 webui/admin.html                |  8 ++--
 webui/css/scaffolding.css       | 39 +++++++++++++++++
 webui/index.html                |  6 +--
 webui/js/ponymail.js            | 92 +++++++++++++++++++++++++++++++++--------
 webui/js/source/render-email.js | 82 +++++++++++++++++++++++++++++-------
 webui/list.html                 |  8 ++--
 webui/oauth.html                |  8 ++--
 webui/thread.html               |  8 ++--
 8 files changed, 198 insertions(+), 53 deletions(-)

diff --git a/webui/admin.html b/webui/admin.html
index 953a843..64faaaa 100644
--- a/webui/admin.html
+++ b/webui/admin.html
@@ -25,7 +25,7 @@ the License. -->
     <!-- Bootstrap -->
 
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=5e59be9" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=fa25c37" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
     <link rel="alternate" href="/api/static.lua"/>
@@ -79,9 +79,9 @@ the License. -->
     <script src="js/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=5e59be9"></script>
-    <script src="js/wordcloud.js?revision=5e59be9"></script>
-    <script src="js/ponymail.js?revision=5e59be9"></script>
+    <script src="js/config.js?revision=fa25c37"></script>
+    <script src="js/wordcloud.js?revision=fa25c37"></script>
+    <script src="js/ponymail.js?revision=fa25c37"></script>
     <div id="splash" class="splash fade-in"> &nbsp; </div>
     <div style="clear: both;"></div>
   </body>
diff --git a/webui/css/scaffolding.css b/webui/css/scaffolding.css
index 25e12c7..bec814f 100644
--- a/webui/css/scaffolding.css
+++ b/webui/css/scaffolding.css
@@ -1306,6 +1306,45 @@ a.disabled {
   color: #FFF !important;
 }
 
+.permalink_dropdown {
+  display: inline-block;
+  position: relative;
+}
+
+.permalink_menu {
+  display: none;
+  position: absolute;
+  bottom: 100%;
+  left: 0;
+  margin-bottom: 4px;
+  background: #fff;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+  z-index: 1000;
+  min-width: 140px;
+  white-space: nowrap;
+}
+
+.permalink_menu.show {
+  display: block;
+}
+
+.permalink_menu_item {
+  display: block;
+  padding: 6px 12px;
+  color: #333;
+  text-decoration: none;
+  font-size: 13px;
+  cursor: pointer;
+}
+
+.permalink_menu_item:hover {
+  background: #f0f0f0;
+  color: #333;
+  text-decoration: none;
+}
+
 .toolbar_button_source {
   background: #c93d24;
   color: #FFF !important;
diff --git a/webui/index.html b/webui/index.html
index 6265a4f..25c0db7 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -24,7 +24,7 @@ the License. -->
     <!-- Bootstrap -->
     
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=5e59be9" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=fa25c37" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
     <link rel="alternate" href="/api/static.lua"/>
@@ -63,8 +63,8 @@ the License. -->
     <script src="js/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=5e59be9"></script>
-    <script src="js/ponymail.js?revision=5e59be9"></script>
+    <script src="js/config.js?revision=fa25c37"></script>
+    <script src="js/ponymail.js?revision=fa25c37"></script>
     <div id="splash" class="splash fade-in"> &nbsp; </div>
     <div style="clear: both;"></div>
     
diff --git a/webui/js/ponymail.js b/webui/js/ponymail.js
index 5d3ad5e..afc9128 100644
--- a/webui/js/ponymail.js
+++ b/webui/js/ponymail.js
@@ -16,7 +16,7 @@
 */
 // THIS IS AN AUTOMATICALLY COMBINED FILE. PLEASE EDIT THE source/ FILES!
 
-const PONYMAIL_REVISION = '5e59be9';
+const PONYMAIL_REVISION = 'fa25c37';
 
 
 /******************************************
@@ -214,7 +214,8 @@ String.prototype.format = function() {
     let args = arguments;
     let n = 0;
     let t = this;
-    let rtn = this.replace(/(?!%)?%([-+]*)([0-9.]*)([a-zA-Z])/g, function(m, pm, len, fmt) {
+    let rtn = this.replace(/(?!%)?%([-+]?)([0-9.]*)([disu])/g, function(m, pm, len, fmt) {
+        // pm (plus/minus) is currently ignored
         len = parseInt(len || '1');
         // We need the correct number of args, balk otherwise, using ourselves to format the error!
         if (args.length <= n) {
@@ -238,6 +239,11 @@ String.prototype.format = function() {
                 return varg;
         }
     });
+    if (args.length != n) {
+        let err = "Error interpolating string '%s': Expected %u argments, got %u!".format(t, n, args.length);
+        console.log(err);
+        throw err;
+    }
     return rtn;
 };
 
@@ -3719,6 +3725,70 @@ function get_rcpts(addresses) {
     return list_of_emails;
 }
 
+// Build a permalink button with a dropdown for "open" and "copy link"
+function make_permalink_dropdown(mid) {
+    let permalink = new URL('thread/%s'.format(mid), window.location.href).href;
+
+    let wrapper = new HTML('div', {
+        class: 'permalink_dropdown'
+    });
+
+    let button = new HTML('button', {
+        title: "Permanent link to this email",
+        class: 'btn toolbar_btn toolbar_button_link',
+        onclick: 'permalink_toggle(this);'
+    }, new HTML('span', {
+        class: 'glyphicon glyphicon-link'
+    }, ' '));
+    wrapper.inject(button);
+
+    let menu = new HTML('div', {
+        class: 'permalink_menu'
+    });
+    let open_link = new HTML('a', {
+        href: permalink,
+        target: '_blank',
+        class: 'permalink_menu_item'
+    }, 'Open in new tab');
+    let copy_link = new HTML('a', {
+        href: 'javascript:void(0);',
+        class: 'permalink_menu_item',
+        onclick: "permalink_copy(this, '%s');".format(permalink.replace(/'/g, "\\'"))
+    }, 'Copy link');
+    menu.inject([open_link, copy_link]);
+    wrapper.inject(menu);
+
+    return wrapper;
+}
+
+function permalink_toggle(btn) {
+    let menu = btn.parentNode.querySelector('.permalink_menu');
+    // Close any other open permalink menus first
+    document.querySelectorAll('.permalink_menu.show').forEach(m => {
+        if (m !== menu) m.classList.remove('show');
+    });
+    menu.classList.toggle('show');
+    // If we just opened it, add a one-time click-outside listener to close
+    if (menu.classList.contains('show')) {
+        setTimeout(() => {
+            document.addEventListener('click', function _dismiss(e) {
+                if (!menu.contains(e.target) && e.target !== btn) {
+                    menu.classList.remove('show');
+                    document.removeEventListener('click', _dismiss);
+                }
+            });
+        }, 0);
+    }
+}
+
+function permalink_copy(el, url) {
+    navigator.clipboard.writeText(url).then(() => {
+        el.textContent = 'Copied!';
+        setTimeout(() => { el.textContent = 'Copy link'; }, 1500);
+    });
+    el.closest('.permalink_menu').classList.remove('show');
+}
+
 async function render_email(state, json) {
     let div = state.div;
     G_full_emails[json.mid] = json; // Save for composer if replying later...
@@ -3880,14 +3950,7 @@ async function render_email(state, json) {
     toolbar.inject(replybutton);
 
     // permalink button
-    let linkbutton = new HTML('a', {
-        href: 'thread/%s'.format(json.mid),
-        target: '_blank',
-        title: "Permanent link to this email",
-        class: 'btn toolbar_btn toolbar_button_link'
-    }, new HTML('span', {
-        class: 'glyphicon glyphicon-link'
-    }, ' '));
+    let linkbutton = make_permalink_dropdown(json.mid);
     toolbar.inject(linkbutton);
 
     // Source-view button
@@ -4011,14 +4074,7 @@ async function render_email_chatty(state, json) {
     toolbar.inject(replybutton);
 
     // permalink button
-    let linkbutton = new HTML('a', {
-        href: 'thread/%s'.format(json.mid),
-        title: "Permanent link to this email",
-        target: '_blank',
-        class: 'btn toolbar_btn toolbar_button_link'
-    }, new HTML('span', {
-        class: 'glyphicon glyphicon-link'
-    }, ' '));
+    let linkbutton = make_permalink_dropdown(json.mid);
     toolbar.inject(linkbutton);
 
     // Source-view button
diff --git a/webui/js/source/render-email.js b/webui/js/source/render-email.js
index 51101cd..bde9286 100644
--- a/webui/js/source/render-email.js
+++ b/webui/js/source/render-email.js
@@ -14,6 +14,70 @@ function get_rcpts(addresses) {
     return list_of_emails;
 }
 
+// Build a permalink button with a dropdown for "open" and "copy link"
+function make_permalink_dropdown(mid) {
+    let permalink = new URL('thread/%s'.format(mid), window.location.href).href;
+
+    let wrapper = new HTML('div', {
+        class: 'permalink_dropdown'
+    });
+
+    let button = new HTML('button', {
+        title: "Permanent link to this email",
+        class: 'btn toolbar_btn toolbar_button_link',
+        onclick: 'permalink_toggle(this);'
+    }, new HTML('span', {
+        class: 'glyphicon glyphicon-link'
+    }, ' '));
+    wrapper.inject(button);
+
+    let menu = new HTML('div', {
+        class: 'permalink_menu'
+    });
+    let open_link = new HTML('a', {
+        href: permalink,
+        target: '_blank',
+        class: 'permalink_menu_item'
+    }, 'Open in new tab');
+    let copy_link = new HTML('a', {
+        href: 'javascript:void(0);',
+        class: 'permalink_menu_item',
+        onclick: "permalink_copy(this, '%s');".format(permalink.replace(/'/g, "\\'"))
+    }, 'Copy link');
+    menu.inject([open_link, copy_link]);
+    wrapper.inject(menu);
+
+    return wrapper;
+}
+
+function permalink_toggle(btn) {
+    let menu = btn.parentNode.querySelector('.permalink_menu');
+    // Close any other open permalink menus first
+    document.querySelectorAll('.permalink_menu.show').forEach(m => {
+        if (m !== menu) m.classList.remove('show');
+    });
+    menu.classList.toggle('show');
+    // If we just opened it, add a one-time click-outside listener to close
+    if (menu.classList.contains('show')) {
+        setTimeout(() => {
+            document.addEventListener('click', function _dismiss(e) {
+                if (!menu.contains(e.target) && e.target !== btn) {
+                    menu.classList.remove('show');
+                    document.removeEventListener('click', _dismiss);
+                }
+            });
+        }, 0);
+    }
+}
+
+function permalink_copy(el, url) {
+    navigator.clipboard.writeText(url).then(() => {
+        el.textContent = 'Copied!';
+        setTimeout(() => { el.textContent = 'Copy link'; }, 1500);
+    });
+    el.closest('.permalink_menu').classList.remove('show');
+}
+
 async function render_email(state, json) {
     let div = state.div;
     G_full_emails[json.mid] = json; // Save for composer if replying later...
@@ -175,14 +239,7 @@ async function render_email(state, json) {
     toolbar.inject(replybutton);
 
     // permalink button
-    let linkbutton = new HTML('a', {
-        href: 'thread/%s'.format(json.mid),
-        target: '_blank',
-        title: "Permanent link to this email",
-        class: 'btn toolbar_btn toolbar_button_link'
-    }, new HTML('span', {
-        class: 'glyphicon glyphicon-link'
-    }, ' '));
+    let linkbutton = make_permalink_dropdown(json.mid);
     toolbar.inject(linkbutton);
 
     // Source-view button
@@ -306,14 +363,7 @@ async function render_email_chatty(state, json) {
     toolbar.inject(replybutton);
 
     // permalink button
-    let linkbutton = new HTML('a', {
-        href: 'thread/%s'.format(json.mid),
-        title: "Permanent link to this email",
-        target: '_blank',
-        class: 'btn toolbar_btn toolbar_button_link'
-    }, new HTML('span', {
-        class: 'glyphicon glyphicon-link'
-    }, ' '));
+    let linkbutton = make_permalink_dropdown(json.mid);
     toolbar.inject(linkbutton);
 
     // Source-view button
diff --git a/webui/list.html b/webui/list.html
index 1258495..f3103ab 100644
--- a/webui/list.html
+++ b/webui/list.html
@@ -24,7 +24,7 @@ the License. -->
     <!-- Bootstrap -->
     
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=5e59be9" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=fa25c37" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
     <link rel="alternate" href="/api/static.lua"/>
@@ -180,9 +180,9 @@ the License. -->
     </script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=5e59be9"></script>
-    <script src="js/wordcloud.js?revision=5e59be9"></script>
-    <script src="js/ponymail.js?revision=5e59be9"></script>
+    <script src="js/config.js?revision=fa25c37"></script>
+    <script src="js/wordcloud.js?revision=fa25c37"></script>
+    <script src="js/ponymail.js?revision=fa25c37"></script>
     <div id="splash" class="splash fade-in"> &nbsp; </div>
     <div style="clear: both;"></div>
     <script type="text/javascript">
diff --git a/webui/oauth.html b/webui/oauth.html
index aac6871..da6c318 100644
--- a/webui/oauth.html
+++ b/webui/oauth.html
@@ -21,7 +21,7 @@ the License. -->
 
     <!-- CSS -->
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=5e59be9" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=fa25c37" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
 
@@ -54,8 +54,8 @@ the License. -->
     <script src="js/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=5e59be9"></script>
-    <script src="js/ponymail.js?revision=5e59be9"></script>
-    <script src="js/oauth.js?revision=5e59be9"></script>
+    <script src="js/config.js?revision=fa25c37"></script>
+    <script src="js/ponymail.js?revision=fa25c37"></script>
+    <script src="js/oauth.js?revision=fa25c37"></script>
   </body>
 </html>
diff --git a/webui/thread.html b/webui/thread.html
index 091867f..94e302e 100644
--- a/webui/thread.html
+++ b/webui/thread.html
@@ -25,7 +25,7 @@ the License. -->
     <!-- Bootstrap -->
     
     <link href="css/bootstrap.min.css" rel="stylesheet" media="all">
-    <link href="css/scaffolding.css?revision=5e59be9" rel="stylesheet" media="all">
+    <link href="css/scaffolding.css?revision=fa25c37" rel="stylesheet" media="all">
     <link href="css/modal.css" rel="stylesheet" media="all">
     <link href="css/spinner.css" rel="stylesheet" media="all">
     <link rel="alternate" href="/api/static.lua"/>
@@ -97,9 +97,9 @@ the License. -->
     <script src="js/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script>
     <!-- Include all compiled plugins (below), or include individual files as needed -->
     <script src="js/bootstrap.min.js"></script>
-    <script src="js/config.js?revision=5e59be9"></script>
-    <script src="js/wordcloud.js?revision=5e59be9"></script>
-    <script src="js/ponymail.js?revision=5e59be9"></script>
+    <script src="js/config.js?revision=fa25c37"></script>
+    <script src="js/wordcloud.js?revision=fa25c37"></script>
+    <script src="js/ponymail.js?revision=fa25c37"></script>
     <div id="splash" class="splash fade-in"> &nbsp; </div>
     <div style="clear: both;"></div>
   </body>