diff --git a/ui/system/index.js b/ui/system/index.js index 1843f18..bad50ca 100644 --- a/ui/system/index.js +++ b/ui/system/index.js @@ -3,6 +3,7 @@ import { WebView } from "./web_view.js"; import { LayoutManager } from "./layout_manager.js"; import { MobileLayoutManager } from "./mobile_layout_manager.js"; +import { PairingHandler } from "./p2p.js"; import "./system_menu.js"; import "./mobile_action_bar.js"; import "./mobile_notification_sheet.js"; @@ -173,141 +174,12 @@ function clearAllNotifications() { updateNotificationBadge(); } -// P2P pairing request handling -const pendingPairingRequests = new Map(); // id -> peer -const pairingApi = navigator.embedder.pairing; - -pairingApi.addEventListener("peerdiscovered", (e) => { - const name = e.peer.displayName || e.peer.id; - toastManager?.add(`${name} appeared`); -}); - -pairingApi.addEventListener("pairingrequest", (e) => { - const peer = e.peer; - console.log(`[P2P] Pairing request from ${peer.displayName} (${peer.id})`); - pendingPairingRequests.set(peer.id, peer); - - addNotification({ - title: "Pairing Request", - body: `${peer.displayName || peer.id} wants to pair`, - tag: "pairing-requests", - iconUrl: "", - }); +// P2P pairing handler +const pairingHandler = new PairingHandler({ + onNotificationAdd: addNotification, + onNotificationRemove: removeNotificationByTag, }); -// Clean up pending requests when pairing completes or the peer leaves. -pairingApi.addEventListener("peerjoined", (e) => { - pendingPairingRequests.delete(e.peer.id); - if (pendingPairingRequests.size === 0) { - removeNotificationByTag("pairing-requests"); - } -}); - -pairingApi.addEventListener("peerleft", (e) => { - pendingPairingRequests.delete(e.peer.id); - if (pendingPairingRequests.size === 0) { - removeNotificationByTag("pairing-requests"); - } -}); - -function showPairingDialog() { - // Close any existing dialog - const existing = document.getElementById("pairing-dialog"); - if (existing) { - existing.close(); - existing.remove(); - } - - if (pendingPairingRequests.size === 0) return; - - const dialog = document.createElement("dialog"); - dialog.id = "pairing-dialog"; - dialog.innerHTML = ` -

Pairing Requests

-
- ${Array.from(pendingPairingRequests.entries()).map(([id, peer]) => ` -
-
-
${peer.displayName || id}
-
${id}
-
-
- - -
-
- `).join("")} -
- - `; - - document.body.appendChild(dialog); - - // Close on backdrop click - dialog.addEventListener("click", (e) => { - if (e.target === dialog) dialog.close(); - }); - dialog.addEventListener("close", () => dialog.remove()); - - dialog.querySelector(".btn-close-dialog").addEventListener("click", () => { - dialog.close(); - }); - - dialog.querySelectorAll(".btn-accept").forEach((btn) => { - btn.addEventListener("click", async () => { - const id = btn.dataset.id; - const peer = pendingPairingRequests.get(id); - if (!peer) return; - btn.disabled = true; - btn.textContent = "Accepting..."; - try { - await pairingApi.acceptPairing(peer); - console.log(`[P2P] Accepted pairing from ${id}`); - pendingPairingRequests.delete(id); - updatePairingDialog(dialog); - } catch (e) { - console.error(`[P2P] Accept failed:`, e); - btn.textContent = "Failed"; - } - }); - }); - - dialog.querySelectorAll(".btn-reject").forEach((btn) => { - btn.addEventListener("click", async () => { - const id = btn.dataset.id; - const peer = pendingPairingRequests.get(id); - if (!peer) return; - btn.disabled = true; - btn.textContent = "Rejecting..."; - try { - await pairingApi.rejectPairing(peer); - console.log(`[P2P] Rejected pairing from ${id}`); - pendingPairingRequests.delete(id); - updatePairingDialog(dialog); - } catch (e) { - console.error(`[P2P] Reject failed:`, e); - btn.textContent = "Failed"; - } - }); - }); - - try { - dialog.showModal(); - } catch (e) { - console.error(`[P2P] showModal() failed:`, e); - } -} - -function updatePairingDialog(dialog) { - if (pendingPairingRequests.size === 0) { - dialog.close(); - removeNotificationByTag("pairing-requests"); - } else { - dialog.close(); - showPairingDialog(); - } -} - window.servo = { /** * Called by Servo when an embedded webview has opened a new embedded webview @@ -792,7 +664,9 @@ document.addEventListener("DOMContentLoaded", () => { // Handle pairing request notifications specially if (notification.tag === "pairing-requests") { mobileNotificationSheet.open = false; - if (pendingPairingRequests.size > 0) showPairingDialog(); + if (pairingHandler.hasPendingRequests) { + pairingHandler.showPairingDialog(); + } return; } @@ -986,7 +860,9 @@ document.addEventListener("DOMContentLoaded", () => { // Handle pairing request notifications specially if (notification.tag === "pairing-requests") { notificationPanel.open = false; - if (pendingPairingRequests.size > 0) showPairingDialog(); + if (pairingHandler.hasPendingRequests) { + pairingHandler.showPairingDialog(); + } return; } diff --git a/ui/system/p2p.js b/ui/system/p2p.js new file mode 100644 index 0000000..4d516ad --- /dev/null +++ b/ui/system/p2p.js @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/** + * Handles P2P pairing events: peer discovery toasts, incoming pairing + * requests, and the accept/reject dialog. + */ +export class PairingHandler { + #api = navigator.embedder.pairing; + #pending = new Map(); // id -> peer + #onNotificationAdd; + #onNotificationRemove; + + /** + * @param {object} opts + * @param {(notification: object) => void} opts.onNotificationAdd + * @param {(tag: string) => void} opts.onNotificationRemove + */ + constructor({ onNotificationAdd, onNotificationRemove }) { + this.#onNotificationAdd = onNotificationAdd; + this.#onNotificationRemove = onNotificationRemove; + + this.#api.addEventListener("peerdiscovered", (e) => { + const name = e.peer.displayName || e.peer.id; + toastManager?.add(`${name} appeared`); + }); + + this.#api.addEventListener("pairingrequest", (e) => { + const peer = e.peer; + console.log( + `[P2P] Pairing request from ${peer.displayName} (${peer.id})`, + ); + this.#pending.set(peer.id, peer); + this.#onNotificationAdd({ + title: "Pairing Request", + body: `${peer.displayName || peer.id} wants to pair`, + tag: "pairing-requests", + iconUrl: "", + }); + }); + + this.#api.addEventListener("peerjoined", (e) => { + this.#clearPendingPeer(e.peer.id); + }); + + this.#api.addEventListener("peerleft", (e) => { + this.#clearPendingPeer(e.peer.id); + }); + } + + get hasPendingRequests() { + return this.#pending.size > 0; + } + + showPairingDialog() { + // Close any existing dialog. + const existing = document.getElementById("pairing-dialog"); + if (existing) { + existing.close(); + existing.remove(); + } + + if (this.#pending.size === 0) return; + + const dialog = document.createElement("dialog"); + dialog.id = "pairing-dialog"; + dialog.innerHTML = ` +

Pairing Requests

+
+ ${Array.from(this.#pending.entries()) + .map( + ([id, peer]) => ` +
+
+
${peer.displayName || id}
+
${id}
+
+
+ + +
+
+ `, + ) + .join("")} +
+ + `; + + document.body.appendChild(dialog); + + dialog.addEventListener("click", (e) => { + if (e.target === dialog) dialog.close(); + }); + dialog.addEventListener("close", () => dialog.remove()); + + dialog + .querySelector(".btn-close-dialog") + .addEventListener("click", () => dialog.close()); + + dialog.querySelectorAll(".btn-accept").forEach((btn) => { + btn.addEventListener("click", async () => { + const id = btn.dataset.id; + const peer = this.#pending.get(id); + if (!peer) return; + btn.disabled = true; + btn.textContent = "Accepting..."; + try { + await this.#api.acceptPairing(peer); + console.log(`[P2P] Accepted pairing from ${id}`); + this.#pending.delete(id); + this.#refreshDialog(dialog); + } catch (e) { + console.error(`[P2P] Accept failed:`, e); + btn.textContent = "Failed"; + } + }); + }); + + dialog.querySelectorAll(".btn-reject").forEach((btn) => { + btn.addEventListener("click", async () => { + const id = btn.dataset.id; + const peer = this.#pending.get(id); + if (!peer) return; + btn.disabled = true; + btn.textContent = "Rejecting..."; + try { + await this.#api.rejectPairing(peer); + console.log(`[P2P] Rejected pairing from ${id}`); + this.#pending.delete(id); + this.#refreshDialog(dialog); + } catch (e) { + console.error(`[P2P] Reject failed:`, e); + btn.textContent = "Failed"; + } + }); + }); + + try { + dialog.showModal(); + } catch (e) { + console.error(`[P2P] showModal() failed:`, e); + } + } + + #refreshDialog(dialog) { + if (this.#pending.size === 0) { + dialog.close(); + this.#onNotificationRemove("pairing-requests"); + } else { + dialog.close(); + this.showPairingDialog(); + } + } + + #clearPendingPeer(id) { + this.#pending.delete(id); + if (this.#pending.size === 0) { + this.#onNotificationRemove("pairing-requests"); + } + } +} diff --git a/ui/system/toasts.css b/ui/system/toasts.css index d750c72..0114ec2 100644 --- a/ui/system/toasts.css +++ b/ui/system/toasts.css @@ -10,7 +10,6 @@ flex-direction: column; align-items: center; pointer-events: none; - padding-top: env(safe-area-inset-top, var(--spacing-sm)); } .toast { @@ -26,7 +25,9 @@ text-align: center; transform: translateY(-100%); opacity: 0; - transition: transform 0.3s ease, opacity 0.3s ease; + transition: + transform 0.3s ease, + opacity 0.3s ease; } .toast.visible { diff --git a/ui/system/toasts.js b/ui/system/toasts.js index 0e99246..095a7e5 100644 --- a/ui/system/toasts.js +++ b/ui/system/toasts.js @@ -49,13 +49,13 @@ class ToastManager extends LitElement { () => { this._toasts = this._toasts.filter((t) => t.id !== id); }, - { once: true } + { once: true }, ); } render() { return html`${this._toasts.map( - (t) => html`
${t.text}
` + (t) => html`
${t.text}
`, )}`; } }