feat(ui): dual-ecash wallet settings, buy-peer-files, seed backup, assorted fixes
- Tabbed Wallet Settings modal (Cashu + Fedimint) and dual-balance wallet card - Buy a peer's paid file (ecash / node Lightning / on-chain / external QR) - Recovery-phrase reveal + backup section; onboarding seed retry resilience - NetBird HTTPS launch, remote-control two-finger scroll + external-open - Shared BackButton, single-v version label, mesh Bitcoin header toggles Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bd567cd165
commit
87769cbfbf
@@ -139,6 +139,41 @@ function deepElementFromPoint(x: number, y: number): Element | null {
|
||||
return el
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nearest scrollable ancestor of `el` for the given delta, hopping out
|
||||
* of same-origin iframes when needed. Synthetic WheelEvents are untrusted and
|
||||
* never actually scroll the page, so two-finger scroll must call scrollBy on a
|
||||
* real scroll container — this locates it (e.g. the right-hand app frame). (#7)
|
||||
*/
|
||||
function findScrollable(el: Element | null, dx: number, dy: number): Element | null {
|
||||
let node: Element | null = el
|
||||
let guard = 0
|
||||
while (node && guard++ < 60) {
|
||||
const win = node.ownerDocument?.defaultView
|
||||
const style = win?.getComputedStyle(node)
|
||||
if (style) {
|
||||
const oy = style.overflowY
|
||||
const ox = style.overflowX
|
||||
const isRoot = node === node.ownerDocument?.scrollingElement
|
||||
const canY =
|
||||
(oy === 'auto' || oy === 'scroll' || isRoot) &&
|
||||
node.scrollHeight > node.clientHeight + 1
|
||||
const canX =
|
||||
(ox === 'auto' || ox === 'scroll' || isRoot) &&
|
||||
node.scrollWidth > node.clientWidth + 1
|
||||
if ((dy !== 0 && canY) || (dx !== 0 && canX)) return node
|
||||
}
|
||||
if (node.parentElement) {
|
||||
node = node.parentElement
|
||||
} else if (win?.frameElement) {
|
||||
node = win.frameElement as Element // same-origin iframe → continue in parent doc
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** The actually-focused element, descending through same-origin iframes. */
|
||||
function deepActiveElement(): Element | null {
|
||||
let el: Element | null = document.activeElement
|
||||
@@ -267,10 +302,19 @@ function handleMessage(data: string) {
|
||||
break
|
||||
}
|
||||
case 's': {
|
||||
const dy = msg.y ?? 0
|
||||
document.dispatchEvent(new WheelEvent('wheel', {
|
||||
bubbles: true, deltaY: dy * 100, deltaMode: WheelEvent.DOM_DELTA_PIXEL,
|
||||
}))
|
||||
// Scroll the element under the virtual cursor (incl. inside same-origin
|
||||
// app frames like the right-hand panel), not the top document. A synthetic
|
||||
// wheel event won't scroll — call scrollBy on a real scroll container. (#7)
|
||||
const dy = (msg.y ?? 0) * 100
|
||||
const dx = (msg.x ?? 0) * 100
|
||||
const start = deepElementFromPoint(cursorX, cursorY)
|
||||
const scroller = findScrollable(start, dx, dy)
|
||||
if (scroller) {
|
||||
scroller.scrollBy({ left: dx, top: dy })
|
||||
} else {
|
||||
const win = start?.ownerDocument?.defaultView ?? window
|
||||
win.scrollBy(dx, dy)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -308,6 +352,30 @@ function doConnect() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the companion (phone) to open a URL in its own browser.
|
||||
*
|
||||
* "Open in external browser" apps can't be usefully opened on the kiosk when a
|
||||
* companion is driving it — `window.open` lands on the kiosk, which the phone
|
||||
* user never sees. When a companion is active we forward the URL over the relay
|
||||
* socket ({"t":"o","url"}); the backend routes it to the phone, which opens it.
|
||||
*
|
||||
* Returns true if the request was forwarded (caller should NOT open locally),
|
||||
* false if there's no active companion (caller should open normally).
|
||||
*/
|
||||
export function requestExternalOpen(url: string): boolean {
|
||||
if (!url || !/^https?:\/\//i.test(url)) return false
|
||||
if (!companionActive.value) return false
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return false
|
||||
try {
|
||||
ws.send(JSON.stringify({ t: 'o', url }))
|
||||
if (import.meta.env.DEV) console.log('[RemoteRelay] Forwarded external-open to companion:', url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Start the remote relay listener. Connects to /ws/remote-relay. */
|
||||
export function startRemoteRelay() {
|
||||
shouldReconnect = true
|
||||
|
||||
Reference in New Issue
Block a user