diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-05 13:13:54 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-05 13:13:54 +0200 |
| commit | 92b7afcb186d7aaf475939d58a0385f408425eeb (patch) | |
| tree | a0283cdda0e91cf1af2a1ff7b1a0a896d3a4d1f4 /packages | |
| parent | 97fa4cf78a826050b9d1df0b484a7794861cda80 (diff) | |
| download | meshbay-92b7afcb186d7aaf475939d58a0385f408425eeb.tar.gz | |
The line sat above the results for as long as the page was open. It is now
said once a cross-group pass is over, for five seconds, in the same passing
note as "Link copied" — moved out of copy-link.js into note.js (`say(text,
ms)`), one note at a time for the whole page. The `.search-unreachable` rule
goes with the line it styled.
The copy-link probe now also checks in Chrome that the note goes by itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
7 files changed, 86 insertions, 34 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 index ad6c3be..4cc3f30 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/copy-link.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/copy-link.js @@ -1,6 +1,7 @@ import { t } from './i18n.js'; import * as platform from './platform.js'; import { groupLinkRoute } from './group-link.js'; +import { say } from './note.js'; /** * "Copy link": the `#/name@owner/path` address of a file or folder @@ -41,27 +42,9 @@ async function _write(text) { 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 })); + say(ok ? t('link.copied') : t('link.copy_failed', { url })); } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/note.js b/packages/meshbay-hub/src/meshbay_hub/static/note.js new file mode 100644 index 0000000..f55c307 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/note.js @@ -0,0 +1,24 @@ +/** + * A line said once and gone, in the playlists' note (style.css + * `.playlist-note`): "Link copied", "2 groups unreachable". Not a dialog — + * nothing here is anyone's to dismiss. + * + * Appended to the body rather than rendered by a view, because the views that + * speak include modals and a context menu that has already closed. One note + * at a time: a second replaces the first and restarts the clock. + */ + +let _note = null; +let _timer = null; + +export function say(text, ms = 2500) { + if (!_note) { + _note = document.createElement('div'); + _note.className = 'playlist-note page-note'; + _note.setAttribute('role', 'status'); + } + _note.textContent = text; + document.body.appendChild(_note); + clearTimeout(_timer); + _timer = setTimeout(() => { if (_note) _note.remove(); }, ms); +} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js index 1ede1f6..ae8eac7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js @@ -14,6 +14,7 @@ import { PhotosApp, groupPhotoAlbums } from './photos-app.js'; import { VideoPlayer } from './video-player.js'; import { transfers } from './transfers.js'; import { copyLink, entryPath, groupItemLink } from './copy-link.js'; +import { say } from './note.js'; import { mergeUnitEntries } from './source-merge.js'; import { useStickyBand } from './sticky.js'; import { ConnectionPool, MAX_IN_FLIGHT, MAX_POOL_SIZE } from './connection-pool.js'; @@ -22,6 +23,8 @@ const DEBOUNCE_MS = 200; const SEARCH_VIDEO_ROOT = '__search__'; const SEARCH_AUDIO_ROOT = '__search__'; const SEARCH_PHOTO_ROOTS = ['__search_photos__']; +// How long "N groups unreachable" stays up once a search pass is over. +const UNREACHABLE_NOTE_MS = 5000; // -- Index fetching ----------------------------------------------------------- @@ -313,13 +316,20 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) setProgress({ done: 0, total: groups.length, unreachable: [] }); if (!poolRef.current) return; - await fetchAllIndexes( + const { unreachable } = await fetchAllIndexes( poolRef.current, groups, token, username, userId, (p) => { if (!cancelled) setProgress(p); }, (results) => { if (!cancelled) setIndexedGroups(new Map(results)); }, ); - if (!cancelled) setFetching(false); + if (cancelled) return; + setFetching(false); + // Said once the pass is over, for a few seconds, rather than left above + // the results: by then nothing more can be done about it here, and a + // line that stays is one more thing between the reader and the results. + if (unreachable.length) { + say(t('search.unreachable', { n: unreachable.length }), UNREACHABLE_NOTE_MS); + } })(); return () => { cancelled = true; }; @@ -715,11 +725,6 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) </div> `} - ${progress.unreachable.length > 0 && html` - <p class="search-unreachable"> - ${t('search.unreachable', { n: progress.unreachable.length })} - </p> - `} ${viewMode === 'files' && hasResults && html` <${FilesPanel} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index bde1c61..21d92df 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -156,10 +156,6 @@ a:hover { text-decoration: underline; } height: 100%; background: var(--accent); transition: width 0.3s; } -.search-unreachable { - font-size: 0.8em; color: var(--text-dim); margin-bottom: 8px; -} - .view-toggle { display: flex; gap: 0; flex-shrink: 0; margin-left: auto; } @@ -5613,9 +5609,9 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } overflow: hidden; text-overflow: ellipsis; } -/* The same note for "Copy link" (copy-link.js). When the clipboard refused, it - carries the link itself, which must be readable whole rather than cut. */ -.copy-link-note.playlist-note { +/* The same note, said from anywhere (note.js). It can carry a link the + clipboard refused, which must be readable whole rather than cut. */ +.page-note.playlist-note { white-space: normal; overflow-wrap: anywhere; border-radius: 12px; diff --git a/packages/meshbay-hub/tests/harness/copy_link_probe.py b/packages/meshbay-hub/tests/harness/copy_link_probe.py index 50d269d..564d277 100644 --- a/packages/meshbay-hub/tests/harness/copy_link_probe.py +++ b/packages/meshbay-hub/tests/harness/copy_link_probe.py @@ -69,7 +69,7 @@ const pick = async (label) => { await wait(50); return true; }; -const note = () => (document.querySelector('.copy-link-note') || {}).textContent || ''; +const note = () => (document.querySelector('.page-note') || {}).textContent || ''; const closeMenus = async () => { document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); await frame(); @@ -177,6 +177,12 @@ const rowNamed = (name) => [...document.querySelectorAll('tr.file-row')] cases.push({ case: 'photos', tile: Boolean(tile), labels: tileLabels, copied: fromTile, lightbox_button: Boolean(lb), lightbox_copied: COPIES.at(-1) || null }); + // Said, then gone (note.js): still up just after, not 2.5 s later. + const upAfterCopy = Boolean(document.querySelector('.page-note')); + await wait(2700); + cases.push({ case: 'note goes', up_after_copy: upAfterCopy, + up_later: Boolean(document.querySelector('.page-note')) }); + parent.postMessage({ cases, logs: LOGS, origin: location.origin }, '*'); } catch (err) { parent.postMessage({ error: String(err && (err.stack || err)), logs: LOGS }, '*'); diff --git a/packages/meshbay-hub/tests/test_copy_link.py b/packages/meshbay-hub/tests/test_copy_link.py index 7115d04..22e56e7 100644 --- a/packages/meshbay-hub/tests/test_copy_link.py +++ b/packages/meshbay-hub/tests/test_copy_link.py @@ -96,3 +96,8 @@ def test_the_link_is_the_hubs_not_the_pages(): source = (STATIC / "copy-link.js").read_text(encoding="utf-8") assert "platform.hubOrigin() + '/#' + groupLinkRoute(" in source assert "location.origin" not in source + + +def test_the_note_goes_by_itself(cases): + c = cases["note goes"] + assert c["up_after_copy"] is True and c["up_later"] is False diff --git a/packages/meshbay-hub/tests/test_search_unreachable_note.py b/packages/meshbay-hub/tests/test_search_unreachable_note.py new file mode 100644 index 0000000..41ed25c --- /dev/null +++ b/packages/meshbay-hub/tests/test_search_unreachable_note.py @@ -0,0 +1,33 @@ +""" +"N groups unreachable" on the cross-group Search page is said once a pass is +over, for a few seconds, in the same passing note as "Link copied" (note.js) — +not left above the results for as long as the page is open. + +Source-level, as the other Search wiring checks are: what is held is where the +line is said and that nothing renders it in the page any more. +""" + +import re +from pathlib import Path + +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" + + +def test_said_once_the_pass_is_over_and_not_rendered(): + source = (STATIC / "search-page.js").read_text(encoding="utf-8") + assert "import { say } from './note.js';" in source + after = source[source.index("const { unreachable } = await fetchAllIndexes("):] + after = after[:after.index("})();")] + assert re.search(r"if \(cancelled\) return;\s*setFetching\(false\);", after) + assert "say(t('search.unreachable', { n: unreachable.length }), UNREACHABLE_NOTE_MS)" in after + assert "search-unreachable" not in source + ms = int(re.search(r"const UNREACHABLE_NOTE_MS = (\d+);", source).group(1)) + assert 2000 <= ms <= 8000 + + +def test_one_note_for_the_whole_page(): + note = (STATIC / "note.js").read_text(encoding="utf-8") + assert "export function say(text, ms = 2500)" in note + assert "clearTimeout(_timer)" in note + copy = (STATIC / "copy-link.js").read_text(encoding="utf-8") + assert "import { say } from './note.js';" in copy and "_say" not in copy |