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"> </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"> </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"> </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"> </div>
<div style="clear: both;"></div>
</body>