summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/copy-link.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-05 12:23:18 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-05 12:23:18 +0200
commit1d94a92936abb3d37a8f9bfb36c8850246fee15d (patch)
treef07aa4f65277d703ac64a84882b0c559bf379f30 /packages/meshbay-hub/src/meshbay_hub/static/copy-link.js
parent8f25294b0f6bc3f292442edd69a2e149f0717b52 (diff)
downloadmeshbay-1d94a92936abb3d37a8f9bfb36c8850246fee15d.tar.gz
feat: copy a file's or folder's #/name@owner link from Files, Music, Photos and Search
"Copy link" puts the address group-link.js resolves on the clipboard, on the hub's origin rather than the page's, so a link copied in the desktop application is not app://meshbay. Files offers it for one row, from the right-click menu or the toolbar with one row ticked (a phone's way in); Music on one track's menu, whose dots a phone has; Photos on a right-clicked tile and in the lightbox's bar. The video player and the file preview carry a link button next to Download. Applications get a `linkFor(entry | folderPath)` prop (MESHBAY_DESIGN.md ยง9.2) and offer the action only when it names a link. The group page builds it from the hub's row; Search from each result's own group and its path before the merged views prefixed it, and names no link for a folder of the merged tree, which a group name alone does not identify. harness/copy_link_probe.py mounts the three applications in Chrome and reads what reached the clipboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/copy-link.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/copy-link.js67
1 files changed, 67 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/copy-link.js b/packages/meshbay-hub/src/meshbay_hub/static/copy-link.js
new file mode 100644
index 0000000..ad6c3be
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/copy-link.js
@@ -0,0 +1,67 @@
+import { t } from './i18n.js';
+import * as platform from './platform.js';
+import { groupLinkRoute } from './group-link.js';
+
+/**
+ * "Copy link": the `#/name@owner/path` address of a file or folder
+ * (group-link.js), on this hub, put on the clipboard.
+ *
+ * The hub's origin, never the page's: in the desktop application the page is
+ * `app://meshbay`, and a link that only opens inside one person's application
+ * is not a link.
+ */
+
+/** The full link to `path` inside `group` (`{ name, owner_username }`). */
+export function groupItemLink(group, path) {
+ return platform.hubOrigin() + '/#' + groupLinkRoute(group, path);
+}
+
+/** An entry's path inside its group: its folder, root first, then its name. */
+export function entryPath(entry, dir = entry.path) {
+ return [dir, entry.name].filter(Boolean).join('/');
+}
+
+// The async clipboard first; the old command where it is refused โ€” a page
+// without focus, a WebView without the permission. Neither needs to read
+// anything back.
+async function _write(text) {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch { /* fall through */ }
+ const area = document.createElement('textarea');
+ area.value = text;
+ area.setAttribute('readonly', '');
+ area.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
+ document.body.appendChild(area);
+ area.select();
+ let ok = false;
+ try { ok = document.execCommand('copy'); } catch { ok = false; }
+ area.remove();
+ return ok;
+}
+
+let _note = null;
+let _noteTimer = null;
+
+// Said once and gone, in the playlists' note (style.css `.playlist-note`).
+// Appended to the body rather than rendered by a view, because the views that
+// copy include modals and a context menu that has already closed.
+function _say(text) {
+ if (!_note) {
+ _note = document.createElement('div');
+ _note.className = 'playlist-note copy-link-note';
+ _note.setAttribute('role', 'status');
+ }
+ _note.textContent = text;
+ document.body.appendChild(_note);
+ clearTimeout(_noteTimer);
+ _noteTimer = setTimeout(() => { if (_note) _note.remove(); }, 2500);
+}
+
+/** Copy `url`, and say whether it worked. The link itself is shown if not. */
+export async function copyLink(url) {
+ if (!url) return;
+ const ok = await _write(url);
+ _say(ok ? t('link.copied') : t('link.copy_failed', { url }));
+}