diff --git a/app/static/js/repeaters.js b/app/static/js/repeaters.js index e5bbe5b..35fec42 100644 --- a/app/static/js/repeaters.js +++ b/app/static/js/repeaters.js @@ -91,6 +91,15 @@ function esc(s) { .replaceAll('"', '"'); } +// 'AB→CD→EF' offers the browser no line-break opportunity, so a multi-hop path +// runs off a narrow screen. A zero-width space after each arrow lets it wrap by +// hop. Apply after esc() — the result is HTML. +const RPT_ZWSP = String.fromCharCode(0x200B); + +function wrapPath(s) { + return String(s ?? '').replaceAll('→', '→' + RPT_ZWSP); +} + // Relative time comes from datetime-utils.js as formatTimeAgo(). The local copy this // replaced also had a "Never" branch for a falsy timestamp, which was unreachable — the // single call site already guards it. contacts.js and dm.js have since been folded in @@ -165,11 +174,13 @@ function renderRepeaterRows() { ? '' : ''; + // The path line wraps (no text-truncate) so a long path stays readable + // on a phone; the other two variants are short and stay on one line. const statusLine = isLoggingIn - ? `${tHtml('repeaters.row.logging_in')}` + ? `${tHtml('repeaters.row.logging_in')}` : (r.on_device - ? `${esc(r.path_or_mode || '—')}` - : `${tHtml('repeaters.row.not_on_device')}`); + ? `${wrapPath(esc(r.path_or_mode || '—'))}` + : `${tHtml('repeaters.row.not_on_device')}`); // "last login" goes on its own line so a long path keeps the full row // width to itself (on narrow phones it otherwise gets truncated hard). const roleLine = (r.on_device && !isLoggingIn && r.last_login_at) @@ -181,7 +192,7 @@ function renderRepeaterRows() { ${icon}
${esc(r.name || r.public_key.substring(0, 12))}
- ${statusLine} + ${statusLine} ${roleLine}
@@ -461,7 +472,7 @@ function renderDeviceRepeaterList() {
${esc(c.name || c.public_key_prefix)}
- ${esc(c.public_key_prefix)} · ${esc(c.path_or_mode || '')} + ${esc(c.public_key_prefix)} · ${wrapPath(esc(c.path_or_mode || ''))}
${added ? `${tHtml('repeaters.added_badge')}` @@ -503,7 +514,7 @@ function openPathModal(pubkey) { if (!r) return; _pathPubkey = pubkey; document.getElementById('pathModalName').textContent = r.name || pubkey.substring(0, 12); - document.getElementById('pathModalCurrent').textContent = r.path_or_mode || '—'; + document.getElementById('pathModalCurrent').textContent = wrapPath(r.path_or_mode || '—'); _pathModal.show(); renderPathList(pubkey); } @@ -514,7 +525,7 @@ async function refreshDevicePathDisplay() { await loadRepeaters(); const r = findRepeater(_pathPubkey); const el = document.getElementById('pathModalCurrent'); - if (el && r) el.textContent = r.path_or_mode || '—'; + if (el && r) el.textContent = wrapPath(r.path_or_mode || '—'); } async function renderPathList(pubkey) { diff --git a/app/templates/repeaters.html b/app/templates/repeaters.html index 275f4a5..33ca517 100644 --- a/app/templates/repeaters.html +++ b/app/templates/repeaters.html @@ -87,10 +87,18 @@ opacity: 0.65; } + /* Paths wrap per hop (zero-width space from wrapPath()); this is the + fallback for a single hop token too long for the line. */ .rpt-path { font-size: 0.8rem; } + .rpt-path, + .rpt-path-wrap, + #pathModalCurrent { + overflow-wrap: anywhere; + } + .empty-state { text-align: center; color: var(--text-secondary, #6c757d);