| … | … | @@ -1251,12 +1251,51 @@ document.querySelectorAll('[data-ota]').forEach(function (b) { |
| 1251 | 1251 | </section>` |
| 1252 | 1252 | } |
| 1253 | 1253 | |
| 1254 | // Pairing, on a handset, from the page instead of an overlay. |
| 1255 | // |
| 1256 | // ⛔ WHAT THIS OPENS IS NOT A LOGIN. The ceremony hands this phone the DID's SHARED SIGNING KEY, after |
| 1257 | // which it authors as that identity on its own, offline — and removing a device later stops it being |
| 1258 | // ADMITTED but cannot un-know a key it already holds. The full warning lives on the native screen this |
| 1259 | // button opens (app/Pair.jsx, where the wording is explicitly part of the security model), so this says |
| 1260 | // enough to set expectations and does not paraphrase it into something softer. |
| 1261 | // |
| 1262 | // It lives here because it always should have: favela-android's own note says "the old UI reached |
| 1263 | // pairing from settings for exactly this reason", and it only became a floating button when the native |
| 1264 | // settings screen went away with the move to a WebView. On the handset that overlay sat on top of the |
| 1265 | // bottom nav's Home item, so tapping Home opened the pairing screen — reported 2026-08-25. |
| 1266 | // |
| 1267 | // Gated on `shell.pair`, a SEPARATE capability from `shell.ota`: the page arrives by OTA and the shell |
| 1268 | // by APK, so a page that lands on a shell which cannot service this must render nothing at all rather |
| 1269 | // than a button that silently does nothing. |
| 1270 | export function pairSection (shell = null) { |
| 1271 | if (!shell || !shell.pair) return '' |
| 1272 | return `<section class="authbox pairsec"> |
| 1273 | <h3>Connect this phone</h3> |
| 1274 | <p class="empty">Paste an invite minted by a device that already holds your identity — favela's |
| 1275 | <em>Generate a device invite</em>, above or on your desktop. Use this when setting the phone up, or |
| 1276 | when it holds your identity but posts are being refused because it is not on the room's member list.</p> |
| 1277 | <p class="empty"><strong>It receives the key that signs your posts.</strong> From then on this phone can |
| 1278 | post as you, by itself. The next screen says so in full before anything happens.</p> |
| 1279 | <p><button class="devbtn" data-ota="pair">open the pairing screen</button></p> |
| 1280 | <script> |
| 1281 | document.querySelectorAll('[data-ota="pair"]').forEach(function (b) { |
| 1282 | b.addEventListener('click', function () { |
| 1283 | var bridge = window.ReactNativeWebView |
| 1284 | if (!bridge) return |
| 1285 | b.disabled = true |
| 1286 | bridge.postMessage(JSON.stringify({ t: 'app-update', op: 'pair' })) |
| 1287 | }) |
| 1288 | }) |
| 1289 | </script> |
| 1290 | </section>` |
| 1291 | } |
| 1292 | |
| 1254 | 1293 | export function settingsPage ({ viewer = null, profile = null, did = null, devices = [], theme = null, shell = null } = {}) { |
| 1255 | 1294 | if (!viewer) { |
| 1256 | 1295 | // Appearance still renders here — it is not an account setting. So does App updates: a restart is |
| 1257 | 1296 | // what repairs a session this page has forgotten, and hiding it behind sign-in hides it exactly when |
| 1258 | 1297 | // it is needed. |
| 1259 | | return page({ title: 'settings · favela', viewer, active: null, theme, body: '<div class="viewhdr">Settings <a href="/">← back</a></div>' + themeControl(theme) + versionNote() + appUpdateSection(shell) + '<p class="empty">Sign in to manage your profile. <a href="/login">sign in →</a></p>' }) |
| 1298 | return page({ title: 'settings · favela', viewer, active: null, theme, body: '<div class="viewhdr">Settings <a href="/">← back</a></div>' + themeControl(theme) + versionNote() + appUpdateSection(shell) + pairSection(shell) + '<p class="empty">Sign in to manage your profile. <a href="/login">sign in →</a></p>' }) |
| 1260 | 1299 | } |
| 1261 | 1300 | const cid = profile && profile.avatar && blobCid(profile.avatar.ref) |
| 1262 | 1301 | const current = (cid && did && isSafeCid(cid)) |
| … | … | @@ -1282,6 +1321,7 @@ export function settingsPage ({ viewer = null, profile = null, did = null, devic |
| 1282 | 1321 | ${themeControl(theme)} |
| 1283 | 1322 | ${versionNote()} |
| 1284 | 1323 | ${appUpdateSection(shell)} |
| 1324 | ${pairSection(shell)} |
| 1285 | 1325 | <section class="authbox"> |
| 1286 | 1326 | <h3>Profile picture</h3> |
| 1287 | 1327 | <p class="empty">Signed in as <strong>${esc(viewer.did)}</strong>${profile && profile.displayName ? ' · ' + esc(profile.displayName) : ''}</p> |
| … | … | @@ -1522,7 +1562,14 @@ export function page ({ title = 'favela', heading, viewer = null, active = 'time |
| 1522 | 1562 | // attribute = follow the system, which is the default and the only state that existed before. |
| 1523 | 1563 | const themeAttr = (theme === 'dark' || theme === 'light') ? ` data-theme="${theme}"` : '' |
| 1524 | 1564 | return `<!doctype html><html lang="en"${themeAttr}><head><meta charset="utf-8"> |
| 1525 | | <meta name="viewport" content="width=device-width,initial-scale=1"> |
| 1565 | <!-- viewport-fit=cover is what makes env(safe-area-inset-*) resolve to anything. Without it those |
| 1566 | insets are INERT — they compute to 0 — and .bottomnav's padding-bottom plus body's |
| 1567 | calc(64px + env(safe-area-inset-bottom)) were both written expecting them to work. On the handset |
| 1568 | the effect was a bottom bar floating ABOVE the gesture strip with page content sliding underneath |
| 1569 | it, because the WebView paints edge-to-edge while the layout viewport stops at the safe area. |
| 1570 | Observed on a moto g power, 2026-08-25. The rules were already correct; this is the token that |
| 1571 | switches them on. --> |
| 1572 | <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> |
| 1526 | 1573 | <title>${esc(title)}</title> |
| 1527 | 1574 | <link rel="alternate" type="application/atom+xml" title="favela timeline" href="/feed.xml"> |
| 1528 | 1575 | <meta name="favela-pds" content="${esc(pds)}"> |