diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-05 12:23:18 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-05 12:23:18 +0200 |
| commit | 1d94a92936abb3d37a8f9bfb36c8850246fee15d (patch) | |
| tree | f07aa4f65277d703ac64a84882b0c559bf379f30 /packages/meshbay-hub/src/meshbay_hub/static/copy-link.js | |
| parent | 8f25294b0f6bc3f292442edd69a2e149f0717b52 (diff) | |
| download | meshbay-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.js | 67 |
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 })); +} |