Rewild Your Web
18
fork

Configure Feed

Select the types of activity you want to include in your feed.

system: move p2p related code to its own file

Signed-off-by: webbeef <me@webbeef.org>

webbeef 6b76e3f5 a5e4ac84

+177 -139
+11 -135
ui/system/index.js
··· 3 3 import { WebView } from "./web_view.js"; 4 4 import { LayoutManager } from "./layout_manager.js"; 5 5 import { MobileLayoutManager } from "./mobile_layout_manager.js"; 6 + import { PairingHandler } from "./p2p.js"; 6 7 import "./system_menu.js"; 7 8 import "./mobile_action_bar.js"; 8 9 import "./mobile_notification_sheet.js"; ··· 173 174 updateNotificationBadge(); 174 175 } 175 176 176 - // P2P pairing request handling 177 - const pendingPairingRequests = new Map(); // id -> peer 178 - const pairingApi = navigator.embedder.pairing; 179 - 180 - pairingApi.addEventListener("peerdiscovered", (e) => { 181 - const name = e.peer.displayName || e.peer.id; 182 - toastManager?.add(`${name} appeared`); 183 - }); 184 - 185 - pairingApi.addEventListener("pairingrequest", (e) => { 186 - const peer = e.peer; 187 - console.log(`[P2P] Pairing request from ${peer.displayName} (${peer.id})`); 188 - pendingPairingRequests.set(peer.id, peer); 189 - 190 - addNotification({ 191 - title: "Pairing Request", 192 - body: `${peer.displayName || peer.id} wants to pair`, 193 - tag: "pairing-requests", 194 - iconUrl: "", 195 - }); 196 - }); 197 - 198 - // Clean up pending requests when pairing completes or the peer leaves. 199 - pairingApi.addEventListener("peerjoined", (e) => { 200 - pendingPairingRequests.delete(e.peer.id); 201 - if (pendingPairingRequests.size === 0) { 202 - removeNotificationByTag("pairing-requests"); 203 - } 204 - }); 205 - 206 - pairingApi.addEventListener("peerleft", (e) => { 207 - pendingPairingRequests.delete(e.peer.id); 208 - if (pendingPairingRequests.size === 0) { 209 - removeNotificationByTag("pairing-requests"); 210 - } 177 + // P2P pairing handler 178 + const pairingHandler = new PairingHandler({ 179 + onNotificationAdd: addNotification, 180 + onNotificationRemove: removeNotificationByTag, 211 181 }); 212 182 213 - function showPairingDialog() { 214 - // Close any existing dialog 215 - const existing = document.getElementById("pairing-dialog"); 216 - if (existing) { 217 - existing.close(); 218 - existing.remove(); 219 - } 220 - 221 - if (pendingPairingRequests.size === 0) return; 222 - 223 - const dialog = document.createElement("dialog"); 224 - dialog.id = "pairing-dialog"; 225 - dialog.innerHTML = ` 226 - <h2>Pairing Requests</h2> 227 - <div class="pairing-request-list"> 228 - ${Array.from(pendingPairingRequests.entries()).map(([id, peer]) => ` 229 - <div class="pairing-request-item" data-id="${id}"> 230 - <div class="pairing-request-info"> 231 - <div class="pairing-request-name">${peer.displayName || id}</div> 232 - <div class="pairing-request-id">${id}</div> 233 - </div> 234 - <div class="pairing-request-buttons"> 235 - <button class="btn-accept" data-id="${id}">Accept</button> 236 - <button class="btn-reject" data-id="${id}">Reject</button> 237 - </div> 238 - </div> 239 - `).join("")} 240 - </div> 241 - <button class="btn-close-dialog">Close</button> 242 - `; 243 - 244 - document.body.appendChild(dialog); 245 - 246 - // Close on backdrop click 247 - dialog.addEventListener("click", (e) => { 248 - if (e.target === dialog) dialog.close(); 249 - }); 250 - dialog.addEventListener("close", () => dialog.remove()); 251 - 252 - dialog.querySelector(".btn-close-dialog").addEventListener("click", () => { 253 - dialog.close(); 254 - }); 255 - 256 - dialog.querySelectorAll(".btn-accept").forEach((btn) => { 257 - btn.addEventListener("click", async () => { 258 - const id = btn.dataset.id; 259 - const peer = pendingPairingRequests.get(id); 260 - if (!peer) return; 261 - btn.disabled = true; 262 - btn.textContent = "Accepting..."; 263 - try { 264 - await pairingApi.acceptPairing(peer); 265 - console.log(`[P2P] Accepted pairing from ${id}`); 266 - pendingPairingRequests.delete(id); 267 - updatePairingDialog(dialog); 268 - } catch (e) { 269 - console.error(`[P2P] Accept failed:`, e); 270 - btn.textContent = "Failed"; 271 - } 272 - }); 273 - }); 274 - 275 - dialog.querySelectorAll(".btn-reject").forEach((btn) => { 276 - btn.addEventListener("click", async () => { 277 - const id = btn.dataset.id; 278 - const peer = pendingPairingRequests.get(id); 279 - if (!peer) return; 280 - btn.disabled = true; 281 - btn.textContent = "Rejecting..."; 282 - try { 283 - await pairingApi.rejectPairing(peer); 284 - console.log(`[P2P] Rejected pairing from ${id}`); 285 - pendingPairingRequests.delete(id); 286 - updatePairingDialog(dialog); 287 - } catch (e) { 288 - console.error(`[P2P] Reject failed:`, e); 289 - btn.textContent = "Failed"; 290 - } 291 - }); 292 - }); 293 - 294 - try { 295 - dialog.showModal(); 296 - } catch (e) { 297 - console.error(`[P2P] showModal() failed:`, e); 298 - } 299 - } 300 - 301 - function updatePairingDialog(dialog) { 302 - if (pendingPairingRequests.size === 0) { 303 - dialog.close(); 304 - removeNotificationByTag("pairing-requests"); 305 - } else { 306 - dialog.close(); 307 - showPairingDialog(); 308 - } 309 - } 310 - 311 183 window.servo = { 312 184 /** 313 185 * Called by Servo when an embedded webview has opened a new embedded webview ··· 792 664 // Handle pairing request notifications specially 793 665 if (notification.tag === "pairing-requests") { 794 666 mobileNotificationSheet.open = false; 795 - if (pendingPairingRequests.size > 0) showPairingDialog(); 667 + if (pairingHandler.hasPendingRequests) { 668 + pairingHandler.showPairingDialog(); 669 + } 796 670 return; 797 671 } 798 672 ··· 986 860 // Handle pairing request notifications specially 987 861 if (notification.tag === "pairing-requests") { 988 862 notificationPanel.open = false; 989 - if (pendingPairingRequests.size > 0) showPairingDialog(); 863 + if (pairingHandler.hasPendingRequests) { 864 + pairingHandler.showPairingDialog(); 865 + } 990 866 return; 991 867 } 992 868
+161
ui/system/p2p.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + /** 4 + * Handles P2P pairing events: peer discovery toasts, incoming pairing 5 + * requests, and the accept/reject dialog. 6 + */ 7 + export class PairingHandler { 8 + #api = navigator.embedder.pairing; 9 + #pending = new Map(); // id -> peer 10 + #onNotificationAdd; 11 + #onNotificationRemove; 12 + 13 + /** 14 + * @param {object} opts 15 + * @param {(notification: object) => void} opts.onNotificationAdd 16 + * @param {(tag: string) => void} opts.onNotificationRemove 17 + */ 18 + constructor({ onNotificationAdd, onNotificationRemove }) { 19 + this.#onNotificationAdd = onNotificationAdd; 20 + this.#onNotificationRemove = onNotificationRemove; 21 + 22 + this.#api.addEventListener("peerdiscovered", (e) => { 23 + const name = e.peer.displayName || e.peer.id; 24 + toastManager?.add(`${name} appeared`); 25 + }); 26 + 27 + this.#api.addEventListener("pairingrequest", (e) => { 28 + const peer = e.peer; 29 + console.log( 30 + `[P2P] Pairing request from ${peer.displayName} (${peer.id})`, 31 + ); 32 + this.#pending.set(peer.id, peer); 33 + this.#onNotificationAdd({ 34 + title: "Pairing Request", 35 + body: `${peer.displayName || peer.id} wants to pair`, 36 + tag: "pairing-requests", 37 + iconUrl: "", 38 + }); 39 + }); 40 + 41 + this.#api.addEventListener("peerjoined", (e) => { 42 + this.#clearPendingPeer(e.peer.id); 43 + }); 44 + 45 + this.#api.addEventListener("peerleft", (e) => { 46 + this.#clearPendingPeer(e.peer.id); 47 + }); 48 + } 49 + 50 + get hasPendingRequests() { 51 + return this.#pending.size > 0; 52 + } 53 + 54 + showPairingDialog() { 55 + // Close any existing dialog. 56 + const existing = document.getElementById("pairing-dialog"); 57 + if (existing) { 58 + existing.close(); 59 + existing.remove(); 60 + } 61 + 62 + if (this.#pending.size === 0) return; 63 + 64 + const dialog = document.createElement("dialog"); 65 + dialog.id = "pairing-dialog"; 66 + dialog.innerHTML = ` 67 + <h2>Pairing Requests</h2> 68 + <div class="pairing-request-list"> 69 + ${Array.from(this.#pending.entries()) 70 + .map( 71 + ([id, peer]) => ` 72 + <div class="pairing-request-item" data-id="${id}"> 73 + <div class="pairing-request-info"> 74 + <div class="pairing-request-name">${peer.displayName || id}</div> 75 + <div class="pairing-request-id">${id}</div> 76 + </div> 77 + <div class="pairing-request-buttons"> 78 + <button class="btn-accept" data-id="${id}">Accept</button> 79 + <button class="btn-reject" data-id="${id}">Reject</button> 80 + </div> 81 + </div> 82 + `, 83 + ) 84 + .join("")} 85 + </div> 86 + <button class="btn-close-dialog">Close</button> 87 + `; 88 + 89 + document.body.appendChild(dialog); 90 + 91 + dialog.addEventListener("click", (e) => { 92 + if (e.target === dialog) dialog.close(); 93 + }); 94 + dialog.addEventListener("close", () => dialog.remove()); 95 + 96 + dialog 97 + .querySelector(".btn-close-dialog") 98 + .addEventListener("click", () => dialog.close()); 99 + 100 + dialog.querySelectorAll(".btn-accept").forEach((btn) => { 101 + btn.addEventListener("click", async () => { 102 + const id = btn.dataset.id; 103 + const peer = this.#pending.get(id); 104 + if (!peer) return; 105 + btn.disabled = true; 106 + btn.textContent = "Accepting..."; 107 + try { 108 + await this.#api.acceptPairing(peer); 109 + console.log(`[P2P] Accepted pairing from ${id}`); 110 + this.#pending.delete(id); 111 + this.#refreshDialog(dialog); 112 + } catch (e) { 113 + console.error(`[P2P] Accept failed:`, e); 114 + btn.textContent = "Failed"; 115 + } 116 + }); 117 + }); 118 + 119 + dialog.querySelectorAll(".btn-reject").forEach((btn) => { 120 + btn.addEventListener("click", async () => { 121 + const id = btn.dataset.id; 122 + const peer = this.#pending.get(id); 123 + if (!peer) return; 124 + btn.disabled = true; 125 + btn.textContent = "Rejecting..."; 126 + try { 127 + await this.#api.rejectPairing(peer); 128 + console.log(`[P2P] Rejected pairing from ${id}`); 129 + this.#pending.delete(id); 130 + this.#refreshDialog(dialog); 131 + } catch (e) { 132 + console.error(`[P2P] Reject failed:`, e); 133 + btn.textContent = "Failed"; 134 + } 135 + }); 136 + }); 137 + 138 + try { 139 + dialog.showModal(); 140 + } catch (e) { 141 + console.error(`[P2P] showModal() failed:`, e); 142 + } 143 + } 144 + 145 + #refreshDialog(dialog) { 146 + if (this.#pending.size === 0) { 147 + dialog.close(); 148 + this.#onNotificationRemove("pairing-requests"); 149 + } else { 150 + dialog.close(); 151 + this.showPairingDialog(); 152 + } 153 + } 154 + 155 + #clearPendingPeer(id) { 156 + this.#pending.delete(id); 157 + if (this.#pending.size === 0) { 158 + this.#onNotificationRemove("pairing-requests"); 159 + } 160 + } 161 + }
+3 -2
ui/system/toasts.css
··· 10 10 flex-direction: column; 11 11 align-items: center; 12 12 pointer-events: none; 13 - padding-top: env(safe-area-inset-top, var(--spacing-sm)); 14 13 } 15 14 16 15 .toast { ··· 26 25 text-align: center; 27 26 transform: translateY(-100%); 28 27 opacity: 0; 29 - transition: transform 0.3s ease, opacity 0.3s ease; 28 + transition: 29 + transform 0.3s ease, 30 + opacity 0.3s ease; 30 31 } 31 32 32 33 .toast.visible {
+2 -2
ui/system/toasts.js
··· 49 49 () => { 50 50 this._toasts = this._toasts.filter((t) => t.id !== id); 51 51 }, 52 - { once: true } 52 + { once: true }, 53 53 ); 54 54 } 55 55 56 56 render() { 57 57 return html`${this._toasts.map( 58 - (t) => html`<div class="toast" data-id="${t.id}">${t.text}</div>` 58 + (t) => html`<div class="toast" data-id="${t.id}">${t.text}</div>`, 59 59 )}`; 60 60 } 61 61 }