diff options
Diffstat (limited to 'packages')
14 files changed, 148 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index dfd0aeb..ba24822 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -474,10 +474,20 @@ function Sidebar({ groups, presence, indexProgressPct, route, menuOpen, role, ha }) } </div> + <a class="sidebar-legal" href="${legalUrl()}" target="_blank" rel="noopener"> + ${t('sidebar.legal')}</a> </aside> `; } +// The legal pages of the hub in use: the page's own origin in a browser, the +// configured hub in the application. A new tab either way: the application +// refuses to navigate away from its interface and hands a new window to the +// system browser instead, and in a browser it keeps the session on screen. +function legalUrl() { + return `${platform.hubBase().replace(/\/$/, '')}/legal/`; +} + // ── Home Page ──────────────────────────────────────────────────────────────── function NotificationFeed({ notifications, onMarkRead, onPurge }) { diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 116bbe9..7eed742 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -23,6 +23,7 @@ export default { // Sidebar 'sidebar.my_groups': 'Meine Gruppen', 'sidebar.no_groups': 'Noch keine Gruppen', + 'sidebar.legal': 'Rechtliche Hinweise', 'sidebar.discover': 'Entdecken', 'sidebar.public_groups': 'Öffentliche Gruppen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 80dd819..143f462 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -26,6 +26,7 @@ export default { // Sidebar 'sidebar.my_groups': 'My Groups', 'sidebar.no_groups': 'No groups yet', + 'sidebar.legal': 'Legal information', 'sidebar.discover': 'Discover', 'sidebar.public_groups': 'Public groups', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 9f8b60e..f35d05d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -22,6 +22,7 @@ export default { // Sidebar 'sidebar.my_groups': 'Mis grupos', 'sidebar.no_groups': 'Aún no hay grupos', + 'sidebar.legal': 'Información legal', 'sidebar.discover': 'Descubrir', 'sidebar.public_groups': 'Grupos públicos', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index d928f52..e8ac577 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -22,6 +22,7 @@ export default { // Sidebar 'sidebar.my_groups': 'Mes groupes', 'sidebar.no_groups': 'Aucun groupe pour le moment', + 'sidebar.legal': 'Informations légales', 'sidebar.discover': 'Découvrir', 'sidebar.public_groups': 'Groupes publics', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 58e2389..76caa51 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -23,6 +23,7 @@ export default { // Sidebar 'sidebar.my_groups': 'I miei gruppi', 'sidebar.no_groups': 'Ancora nessun gruppo', + 'sidebar.legal': 'Note legali', 'sidebar.discover': 'Esplora', 'sidebar.public_groups': 'Gruppi pubblici', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 234b238..a6c39ee 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -23,6 +23,7 @@ export default { // Sidebar 'sidebar.my_groups': 'マイグループ', 'sidebar.no_groups': 'グループはまだありません', + 'sidebar.legal': '法的情報', 'sidebar.discover': '見つける', 'sidebar.public_groups': '公開グループ', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 72511f5..e4a7914 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -23,6 +23,7 @@ export default { // Sidebar 'sidebar.my_groups': 'Mijn groepen', 'sidebar.no_groups': 'Nog geen groepen', + 'sidebar.legal': 'Juridische informatie', 'sidebar.discover': 'Ontdekken', 'sidebar.public_groups': 'Openbare groepen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 12a31aa..d13e3ef 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -26,6 +26,7 @@ export default { // Sidebar 'sidebar.my_groups': 'Moje grupy', 'sidebar.no_groups': 'Nie ma jeszcze żadnych grup', + 'sidebar.legal': 'Informacje prawne', 'sidebar.discover': 'Odkrywaj', 'sidebar.public_groups': 'Grupy publiczne', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index c5f826b..1b517bb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -24,6 +24,7 @@ export default { // Sidebar 'sidebar.my_groups': 'Meus grupos', 'sidebar.no_groups': 'Nenhum grupo ainda', + 'sidebar.legal': 'Informações legais', 'sidebar.discover': 'Descobrir', 'sidebar.public_groups': 'Grupos públicos', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index e06c5c6..9922a2d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -23,6 +23,7 @@ export default { // Sidebar 'sidebar.my_groups': '我的群组', 'sidebar.no_groups': '暂无群组', + 'sidebar.legal': '法律信息', 'sidebar.discover': '发现', 'sidebar.public_groups': '公开群组', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js index 2d3c002..a4a8c5c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js @@ -3,6 +3,7 @@ import { } from './vendor/htm-preact.js'; import { t } from './i18n.js'; import { Icon } from './icon.js'; +import { useStickyBand } from './sticky.js'; import { CHUNK_SIZE, pipelinedDownload } from './file-utils.js'; /** @@ -179,6 +180,9 @@ function QueuePanel({ tracks, order, pos, onSelect, onClose }) { } function MusicPlayerBar({ getConnection, queue, onClose, userPrefs }) { + // Pinned to the bottom of the window, over the bottom of the sidebar; the + // sidebar subtracts this so its last entry is not underneath. + const barBand = useStickyBand('--music-bar-h'); const audioRef = useRef(null); const blobCacheRef = useRef(new Map()); // file id -> { url, order: insertion index } const blobInsertRef = useRef(0); @@ -480,7 +484,7 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs }) { : repeat === 'all' ? t('music.player_repeat_all') : t('music.player_repeat_one'); return html` - <div class="music-player-bar"> + <div class="music-player-bar" ref=${barBand}> <audio ref=${audioRef} onTimeUpdate=${(e) => setCurrentTime(e.target.currentTime)} onDurationChange=${(e) => setDuration(e.target.duration)} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index ff6fe57..2b35088 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -71,6 +71,8 @@ --nav-h: 52px; --chrome-h: 0px; --toolbar-h: 0px; + /* Published by the music bar while it is on screen (sticky.js). */ + --music-bar-h: 0px; } /* ── Reset ────────────────────────────────────────────────────────────────── */ @@ -387,15 +389,33 @@ a:hover { text-decoration: underline; } /* ── Sidebar ──────────────────────────────────────────────────────────────── */ +/* Pinned under the navigation bar at the window's height, so the legal link at + its foot is at the bottom of the window whatever the page's length — on a + long file list it would otherwise be at the bottom of the page. It stops + above the music bar, which is pinned to the bottom of the window too. */ .sidebar { + position: sticky; + top: var(--nav-h); + align-self: flex-start; + display: flex; + flex-direction: column; width: 240px; + height: calc(100vh - var(--nav-h) - var(--music-bar-h)); background: var(--sidebar-bg); border-right: 1px solid var(--border); - padding: 16px 0; + padding: 16px 0 0; flex-shrink: 0; overflow-y: auto; } +.sidebar-legal { + margin-top: auto; + padding: 10px 20px 12px; + color: var(--text-dim); + font-size: 0.75em; +} +.sidebar-legal:hover { color: var(--accent); } + .sidebar-section { padding: 0 12px; margin-bottom: 24px; } .sidebar-heading { @@ -2465,7 +2485,7 @@ a.transfer-name { position: fixed; left: -240px; top: 52px; - height: calc(100vh - 52px); + height: calc(100vh - 52px - var(--music-bar-h)); z-index: 50; transition: left 0.2s; box-shadow: none; diff --git a/packages/meshbay-hub/tests/test_sidebar_legal_measured.py b/packages/meshbay-hub/tests/test_sidebar_legal_measured.py new file mode 100644 index 0000000..faaaec7 --- /dev/null +++ b/packages/meshbay-hub/tests/test_sidebar_legal_measured.py @@ -0,0 +1,101 @@ +""" +The legal link at the foot of the sidebar, measured in a browser. + +It is pinned to the bottom of the window, not of the page: the sidebar sticks +under the navigation bar at the window's height, so on a long file list the +link is still on screen. And the music bar, pinned to the bottom of the window +as well, must not cover it — the bar publishes its height as `--music-bar-h` +(sticky.js, as the toolbars do), and the sidebar stops above it. That value +is set by hand here; what is under test is the stylesheet's arithmetic. + +On a phone the sidebar is the slide-out panel, measured open. +""" + +import json +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + +HARNESS = Path(__file__).parent / "harness" / "layout_probe.py" +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" + +pytestmark = pytest.mark.skipif( + shutil.which("google-chrome") is None or not (STATIC / "style.css").exists(), + reason="Chrome or the SPA stylesheet is not available") + +# The probe's frames are this tall. +WINDOW_H = 740 +MUSIC_BAR_H = 64 +WIDTHS = [390, 1024, 1440] + + +def _page(music: bool) -> str: + groups = "".join( + f'<a class="sidebar-item sidebar-group" href="#/"><span class="si-head">' + f'<span class="sidebar-item-name">Some group {i}</span></span></a>' for i in range(4)) + bar = (f'<div class="music-player-bar" style="height:{MUSIC_BAR_H}px;' + f'box-sizing:border-box">a track</div>') if music else "" + style = f' style="--music-bar-h:{MUSIC_BAR_H}px"' if music else "" + return textwrap.dedent(f""" + <div id="app"{style}> + <nav class="nav"><div class="nav-left"><a class="nav-brand" href="#/">MeshBay</a></div> + <div class="nav-right"><button class="nav-btn">someone</button></div></nav> + <div class="layout"> + <aside class="sidebar open"> + <div class="sidebar-section"><div class="sidebar-heading">My groups</div>{groups}</div> + <a class="sidebar-legal" href="/legal/">Legal information</a> + </aside> + <main class="main"><h2>A long list</h2><div style="height:3000px"></div></main> + </div> + {bar} + </div> + """) + + +def _measure(tmp_path_factory, music: bool) -> dict: + fragment = tmp_path_factory.mktemp("sidebar") / "fragment.html" + fragment.write_text(_page(music), encoding="utf-8") + proc = subprocess.run( + ["python3", str(HARNESS), ",".join(str(w) for w in WIDTHS), + str(fragment), ".sidebar-legal", ".sidebar", ".music-player-bar"], + capture_output=True, text=True, timeout=180) + assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}" + out = json.loads(proc.stdout) + assert "error" not in out, f"no measurement: {out}" + return out + + +@pytest.fixture(scope="module") +def plain(tmp_path_factory): + return _measure(tmp_path_factory, music=False) + + +@pytest.fixture(scope="module") +def playing(tmp_path_factory): + return _measure(tmp_path_factory, music=True) + + +@pytest.mark.parametrize("width", WIDTHS) +def test_the_link_is_at_the_bottom_of_the_window(plain, width): + """Not at the bottom of a 3000px page, where nobody would find it.""" + link = plain[str(width)]["boxes"][".sidebar-legal"] + assert link is not None, "no legal link" + bottom = link["top"] + link["height"] + assert WINDOW_H - 8 <= bottom <= WINDOW_H, ( + f"at {width} px the link ends at {bottom}, the window at {WINDOW_H}") + assert link["offLeft"] == 0 and link["offRight"] == 0 + + +@pytest.mark.parametrize("width", WIDTHS) +def test_the_music_bar_does_not_cover_it(playing, width): + link = playing[str(width)]["boxes"][".sidebar-legal"] + bar = playing[str(width)]["boxes"][".music-player-bar"] + assert link is not None and bar is not None + bottom = link["top"] + link["height"] + assert bottom <= bar["top"] + 1, ( + f"at {width} px the link ends at {bottom} and the music bar starts at {bar['top']}") + assert bottom >= bar["top"] - 8, ( + f"at {width} px the link floats {bar['top'] - bottom} px above the music bar") |