summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js10
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-player.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css24
-rw-r--r--packages/meshbay-hub/tests/test_sidebar_legal_measured.py101
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")