Rewild Your Web
18
fork

Configure Feed

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

p2p: share current page with peers

webbeef 358bc4e1 62b5141e

+345 -88
+42 -1
patches/components/constellation/broadcastchannel.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -127,4 +127,21 @@ 3 + @@ -7,7 +7,7 @@ 4 + use base::id::BroadcastChannelRouterId; 5 + use constellation_traits::BroadcastChannelMsg; 6 + use ipc_channel::ipc::IpcSender; 7 + -use log::warn; 8 + +use log::{debug, warn}; 9 + use rustc_hash::FxHashMap; 10 + use servo_url::ImmutableOrigin; 11 + 12 + @@ -127,4 +127,53 @@ 4 13 ); 5 14 } 6 15 } ··· 10 19 + pub fn broadcast_to_all(&self, message: &BroadcastChannelMsg) { 11 20 + if let Some(channels) = self.channels.get(&message.origin) { 12 21 + let Some(routers) = channels.get(&message.channel_name) else { 22 + + debug!( 23 + + "[P2P BC] broadcast_to_all: no routers for channel {:?} at origin {:?}. Known channels for this origin: {:?}", 24 + + message.channel_name, 25 + + message.origin, 26 + + channels.keys().collect::<Vec<_>>() 27 + + ); 13 28 + return; 14 29 + }; 30 + + debug!( 31 + + "[P2P BC] broadcast_to_all: dispatching to {} routers for {:?} / {:?}", 32 + + routers.len(), 33 + + message.origin, 34 + + message.channel_name 35 + + ); 15 36 + for router in routers { 16 37 + if let Some(broadcast_ipc_sender) = self.routers.get(router) { 17 38 + if broadcast_ipc_sender.send(message.clone()).is_err() { ··· 19 40 + } 20 41 + } 21 42 + } 43 + + } else { 44 + + debug!( 45 + + "[P2P BC] broadcast_to_all: no channels registered for origin {:?}. Known origins: {:?}", 46 + + message.origin, 47 + + self.channels.keys().collect::<Vec<_>>() 48 + + ); 22 49 + } 50 + + } 51 + + 52 + + /// Returns all currently open channel `(origin, name)` pairs 53 + + /// that have at least one active router. 54 + + pub fn open_channels(&self) -> Vec<(ImmutableOrigin, String)> { 55 + + let mut result = Vec::new(); 56 + + for (origin, channels) in &self.channels { 57 + + for (name, routers) in channels { 58 + + if !routers.is_empty() { 59 + + result.push((origin.clone(), name.clone())); 60 + + } 61 + + } 62 + + } 63 + + result 23 64 + } 24 65 }
+43 -10
patches/components/constellation/constellation.rs.patch
··· 308 308 ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => { 309 309 // Unlikely at this point, but we may receive events coming from 310 310 // different media sessions, so we set the active media session based 311 - @@ -2057,7 +2185,359 @@ 311 + @@ -2057,6 +2185,392 @@ 312 312 let _ = event_loop.send(ScriptThreadMessage::TriggerGarbageCollection); 313 313 } 314 314 }, ··· 455 455 + }, 456 456 + ScriptToConstellationMessage::PairingAcceptPairing(id, callback) => { 457 457 + self.pairing.accept_pairing(&id, callback); 458 + + // The peer is already connected (they initiated the request), 459 + + // so sync our open broadcast channels to them now. 460 + + let channels: Vec<_> = self 461 + + .broadcast_channels 462 + + .open_channels() 463 + + .into_iter() 464 + + .map(|(origin, name)| (origin.ascii_serialization(), name)) 465 + + .collect(); 466 + + if !channels.is_empty() { 467 + + self.pairing.sync_channels_to_peer(&id, &channels); 468 + + } 458 469 + }, 459 470 + ScriptToConstellationMessage::PairingRejectPairing(id, callback) => { 460 471 + self.pairing.reject_pairing(&id, callback); ··· 539 550 + ref name, 540 551 + data: ref serialized, 541 552 + } => { 553 + + debug!( 554 + + "[P2P BC] Received BroadcastChannelMessage from {from}: {origin} / {name} ({} bytes)", 555 + + serialized.len() 556 + + ); 542 557 + let Some(origin) = 543 558 + servo_url::ServoUrl::parse(origin).ok().map(|u| u.origin()) 544 559 + else { ··· 658 673 + // Handle peer disconnect: clean up remote channel state. 659 674 + if let constellation_traits::PairingEvent::PeerExpired { ref id } = event { 660 675 + self.pairing.clear_remote_peer(id); 661 - } 676 + + } 677 + + 678 + + // When a peer connects or reconnects, sync our open broadcast channels to it. 679 + + if let constellation_traits::PairingEvent::PeerDiscovered { ref id, .. } | 680 + + constellation_traits::PairingEvent::PairingAccepted { ref id } = event 681 + + { 682 + + let channels: Vec<_> = self 683 + + .broadcast_channels 684 + + .open_channels() 685 + + .into_iter() 686 + + .map(|(origin, name)| (origin.ascii_serialization(), name)) 687 + + .collect(); 688 + + warn!( 689 + + "[P2P BC] Peer {id} connected/discovered, {} open channels to sync", 690 + + channels.len() 691 + + ); 692 + + if !channels.is_empty() { 693 + + self.pairing.sync_channels_to_peer(id, &channels); 694 + + } 695 + + } 662 696 + 663 697 + for event_loop in self.event_loops() { 664 698 + if self.embedder_error_listeners.contains(&event_loop.id()) { 665 699 + let _ = event_loop.send(ScriptThreadMessage::DispatchPairingEvent(event.clone())); 666 700 + } 667 - + } 701 + } 668 702 } 669 703 670 - /// Check the origin of a message against that of the pipeline it came from. 671 - @@ -2376,6 +2856,29 @@ 704 + @@ -2376,6 +2890,29 @@ 672 705 TransferState::TransferInProgress(queue) => queue.push_back(task), 673 706 TransferState::CompletionFailed(queue) => queue.push_back(task), 674 707 TransferState::CompletionRequested(_, queue) => queue.push_back(task), ··· 698 731 } 699 732 } 700 733 701 - @@ -3246,6 +3749,13 @@ 734 + @@ -3246,6 +3783,13 @@ 702 735 /// <https://html.spec.whatwg.org/multipage/#destroy-a-top-level-traversable> 703 736 fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { 704 737 debug!("{webview_id}: Closing"); ··· 712 745 let browsing_context_id = BrowsingContextId::from(webview_id); 713 746 // Step 5. Remove traversable from the user agent's top-level traversable set. 714 747 let browsing_context = 715 - @@ -3522,8 +4032,27 @@ 748 + @@ -3522,8 +4066,27 @@ 716 749 opener_webview_id, 717 750 opener_pipeline_id, 718 751 response_sender, ··· 740 773 let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { 741 774 warn!("Failed to create channel"); 742 775 let _ = response_sender.send(None); 743 - @@ -3622,6 +4151,361 @@ 776 + @@ -3622,6 +4185,361 @@ 744 777 }); 745 778 } 746 779 ··· 1102 1135 #[servo_tracing::instrument(skip_all)] 1103 1136 fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { 1104 1137 let Some(pipeline) = self.pipelines.get(&pipeline_id) else { 1105 - @@ -4747,7 +5631,7 @@ 1138 + @@ -4747,7 +5665,7 @@ 1106 1139 } 1107 1140 1108 1141 #[servo_tracing::instrument(skip_all)] ··· 1111 1144 // Send a flat projection of the history to embedder. 1112 1145 // The final vector is a concatenation of the URLs of the past 1113 1146 // entries, the current entry and the future entries. 1114 - @@ -4850,9 +5734,23 @@ 1147 + @@ -4850,9 +5768,23 @@ 1115 1148 ); 1116 1149 self.embedder_proxy.send(EmbedderMsg::HistoryChanged( 1117 1150 webview_id,
+43 -5
patches/components/constellation/pairing.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -0,0 +1,732 @@ 3 + @@ -0,0 +1,770 @@ 4 4 +// SPDX-License-Identifier: AGPL-3.0-or-later 5 5 + 6 6 +//! P2P pairing service integration with the constellation. ··· 18 18 +use constellation_traits::{LocalPeerInfo, PairingEvent, PeerInfo, PeerStatus}; 19 19 +use iroh::EndpointId; 20 20 +use iroh::address_lookup::DiscoveryEvent; 21 - +use log::{error, info, warn}; 21 + +use log::{debug, error, info, warn}; 22 22 +use serde::{Deserialize, Serialize}; 23 23 +use tokio::sync::Mutex; 24 24 + ··· 471 471 + } 472 472 + }; 473 473 + let peers = mgr.peers().await; 474 + + let connected: Vec<_> = peers 475 + + .iter() 476 + + .filter(|p| p.status == beaver_p2p::EndpointStatus::PairedConnected) 477 + + .collect(); 478 + + debug!( 479 + + "[P2P BC] broadcast_message: {} total peers, {} connected", 480 + + peers.len(), 481 + + connected.len() 482 + + ); 474 483 + for peer in peers { 475 484 + if peer.status == beaver_p2p::EndpointStatus::PairedConnected { 476 485 + if let Err(e) = mgr.send_message(&peer.id, &bytes).await { ··· 483 492 + 484 493 + /// Notify all paired peers that a broadcast channel was opened locally. 485 494 + pub(crate) fn on_broadcast_channel_open(&self, origin: &str, name: &str) { 495 + + debug!("[P2P BC] Notifying peers of channel open: {origin} / {name}"); 486 496 + self.broadcast_message(&P2pMessage::BroadcastChannelOpen { 487 497 + origin: origin.to_owned(), 488 498 + name: name.to_owned(), ··· 491 501 + 492 502 + /// Notify all paired peers that a broadcast channel was closed locally. 493 503 + pub(crate) fn on_broadcast_channel_close(&self, origin: &str, name: &str) { 504 + + debug!("[P2P BC] Notifying peers of channel close: {origin} / {name}"); 494 505 + self.broadcast_message(&P2pMessage::BroadcastChannelClose { 495 506 + origin: origin.to_owned(), 496 507 + name: name.to_owned(), ··· 499 510 + 500 511 + /// Forward a local broadcast to paired peers that have the same channel active. 501 512 + pub(crate) fn on_local_broadcast(&self, origin: &str, name: &str, data: &[u8]) { 513 + + debug!( 514 + + "[P2P BC] Local broadcast: {origin} / {name} ({} bytes)", 515 + + data.len() 516 + + ); 502 517 + let msg = P2pMessage::BroadcastChannelMessage { 503 518 + origin: origin.to_owned(), 504 519 + name: name.to_owned(), ··· 526 541 + } 527 542 + }; 528 543 + let channels = remote_channels.lock().await; 529 - + let key = (origin, name); 544 + + let key = (origin.clone(), name.clone()); 545 + + warn!( 546 + + "[P2P BC] Forwarding broadcast: looking for ({origin}, {name}) in {} peers, remote_channels={:?}", 547 + + channels.len(), 548 + + channels 549 + + ); 530 550 + for (peer_id, peer_channels) in channels.iter() { 531 551 + if peer_channels.contains(&key) { 532 552 + if let Ok(endpoint_id) = peer_id.parse() { ··· 556 576 + 557 577 + match &message { 558 578 + P2pMessage::BroadcastChannelOpen { origin, name } => { 559 - + info!("Remote channel open from {from}: {origin} / {name}"); 579 + + debug!("[P2P BC] Received remote channel open from {from}: {origin} / {name}"); 560 580 + let remote_channels = self.remote_channels.clone(); 561 581 + let from = from.to_owned(); 562 582 + let origin = origin.clone(); ··· 572 592 + None 573 593 + }, 574 594 + P2pMessage::BroadcastChannelClose { origin, name } => { 575 - + info!("Remote channel close from {from}: {origin} / {name}"); 595 + + debug!("[P2P BC] Received remote channel close from {from}: {origin} / {name}"); 576 596 + let remote_channels = self.remote_channels.clone(); 577 597 + let from = from.to_owned(); 578 598 + let origin = origin.clone(); ··· 610 630 + net::async_runtime::spawn_task(async move { 611 631 + remote_channels.lock().await.remove(&peer_id); 612 632 + }); 633 + + } 634 + + 635 + + /// Send `BroadcastChannelOpen` for each locally open channel to a specific peer. 636 + + /// Called when a paired peer connects or reconnects so it knows about our channels. 637 + + pub(crate) fn sync_channels_to_peer(&self, peer_id: &str, channels: &[(String, String)]) { 638 + + debug!( 639 + + "[P2P BC] Syncing {} channels to peer {peer_id}", 640 + + channels.len() 641 + + ); 642 + + for (origin, name) in channels { 643 + + self.send_message( 644 + + peer_id, 645 + + &P2pMessage::BroadcastChannelOpen { 646 + + origin: origin.clone(), 647 + + name: name.clone(), 648 + + }, 649 + + ); 650 + + } 613 651 + } 614 652 +} 615 653 +
+6 -3
ui/system/index.css
··· 275 275 } 276 276 277 277 /* Pairing dialog */ 278 - #pairing-dialog { 278 + #pairing-dialog, 279 + #p2p-open-dialog { 279 280 position: fixed; 280 281 z-index: 10000; 281 282 background: var(--bg-menu); ··· 288 289 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); 289 290 } 290 291 291 - #pairing-dialog::backdrop { 292 + #pairing-dialog::backdrop, 293 + #p2p-open-dialog::backdrop { 292 294 background: rgba(0, 0, 0, 0.6); 293 295 z-index: 9999; 294 296 } 295 297 296 - #pairing-dialog h2 { 298 + #pairing-dialog h2, 299 + #p2p-open-dialog h2 { 297 300 font-size: 1.1rem; 298 301 margin: 0 0 1rem; 299 302 }
+48
ui/system/index.js
··· 4 4 import { LayoutManager } from "./layout_manager.js"; 5 5 import { MobileLayoutManager } from "./mobile_layout_manager.js"; 6 6 import { PairingHandler } from "./p2p.js"; 7 + import { P2PBroadcaster } from "./p2p_broadcaster.js"; 7 8 import "./system_menu.js"; 8 9 import "./mobile_action_bar.js"; 9 10 import "./mobile_notification_sheet.js"; ··· 179 180 onNotificationAdd: addNotification, 180 181 onNotificationRemove: removeNotificationByTag, 181 182 }); 183 + 184 + // P2P broadcaster for cross-device system features 185 + const p2pBroadcaster = new P2PBroadcaster(); 182 186 183 187 window.servo = { 184 188 /** ··· 939 943 mobileRadialMenu.show(e.detail.x, e.detail.y, e.detail.contextMenu); 940 944 } 941 945 }); 946 + 947 + // Send current page URL to a peer when "Open in <peer>" is selected. 948 + document.getElementById("root").addEventListener("p2p-open-in", (e) => { 949 + p2pBroadcaster.postMessage("open-view", { url: e.detail.url }); 950 + toastManager?.add("Sent to peer"); 951 + }); 952 + 953 + // Receive "open-view" from a peer and prompt the user. 954 + p2pBroadcaster.addEventListener("open-view", (e) => { 955 + const { payload, peer } = e.detail; 956 + const peerName = peer?.displayName || "A peer"; 957 + const url = payload?.url; 958 + if (!url) { 959 + return; 960 + } 961 + 962 + // Show a confirmation dialog. 963 + // TODO: don't inline styles. 964 + const dialog = document.createElement("dialog"); 965 + dialog.id = "p2p-open-dialog"; 966 + dialog.innerHTML = ` 967 + <h2>Open page from ${peerName}?</h2> 968 + <p style="word-break: break-all; opacity:0.8;f ont-size:0.9em">${url}</p> 969 + <div style="display:flex; gap:0.5em; justify-content:flex-end; margin-top: 1em"> 970 + <button class="btn-cancel">Decline</button> 971 + <button class="btn-accept">Open</button> 972 + </div> 973 + `; 974 + document.body.appendChild(dialog); 975 + dialog.addEventListener("close", () => dialog.remove()); 976 + dialog.querySelector(".btn-cancel").addEventListener("click", () => { 977 + dialog.close(); 978 + }); 979 + dialog.querySelector(".btn-accept").addEventListener("click", () => { 980 + dialog.close(); 981 + const webView = new WebView(url, "", {}); 982 + layoutManager.addWebView(webView); 983 + }); 984 + try { 985 + dialog.showModal(); 986 + } catch { 987 + dialog.remove(); 988 + } 989 + }); 942 990 943 991 const params = new URLSearchParams(window.location.search); 944 992 const openValue = params.get("open");
+51
ui/system/p2p_broadcaster.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + /** 4 + * A P2P broadcaster that wraps a BroadcastChannel for cross-device 5 + * communication between paired peers. 6 + * 7 + * Outgoing: p2pBroadcaster.postMessage("open-view", { url: "..." }) 8 + * → sends { name: "open-view", payload: { url: "..." }, peer: { id, displayName } } 9 + * 10 + * Incoming: dispatches a CustomEvent named after the message name, 11 + * with detail: { payload, peer } 12 + */ 13 + export class P2PBroadcaster extends EventTarget { 14 + #channel; 15 + #localPeerPromise; 16 + 17 + constructor(channelName = "beaver-system") { 18 + super(); 19 + this.#channel = new BroadcastChannel(channelName); 20 + this.#localPeerPromise = null; 21 + 22 + this.#channel.onmessage = (event) => { 23 + const { name, payload, peer } = event.data; 24 + if (!name) { 25 + return; 26 + } 27 + this.dispatchEvent( 28 + new CustomEvent(name, { detail: { payload, peer } }), 29 + ); 30 + }; 31 + } 32 + 33 + async postMessage(name, payload) { 34 + if (!this.#localPeerPromise) { 35 + this.#localPeerPromise = navigator.embedder.pairing.local().catch(() => ({ 36 + id: "unknown", 37 + displayName: "unknown", 38 + })); 39 + } 40 + const local = await this.#localPeerPromise; 41 + this.#channel.postMessage({ 42 + name, 43 + payload, 44 + peer: { id: local.id, displayName: local.displayName }, 45 + }); 46 + } 47 + 48 + close() { 49 + this.#channel.close(); 50 + } 51 + }
+112 -69
ui/system/web_view.js
··· 226 226 detail.controlType === "contextmenu" && 227 227 detail.contextMenuParameters?.items 228 228 ) { 229 - const params = detail.contextMenuParameters; 230 - 231 - // Map action IDs to icons (matching title bar icons) 232 - const actionIcons = { 233 - GoBack: "arrow-left", 234 - GoForward: "arrow-right", 235 - Reload: "rotate-ccw", 236 - }; 237 - 238 - // Enrich items with icons 239 - const items = params.items.map((item) => ({ 240 - ...item, 241 - icon: actionIcons[item.id] || item.icon, 242 - })); 243 - 244 - // In mobile mode, show the radial menu instead of the regular context menu 245 - if (document.body.classList.contains("mobile-mode")) { 246 - // Extract navigation state from context menu items 247 - const navState = { 248 - canGoBack: params.items.some( 249 - (item) => item.id === "GoBack" && !item.disabled, 250 - ), 251 - canGoForward: params.items.some( 252 - (item) => item.id === "GoForward" && !item.disabled, 253 - ), 254 - }; 255 - 256 - // Filter items to remove actions that are part of radial menu 257 - const filteredItems = items.filter( 258 - (item) => 259 - item.id !== "GoBack" && 260 - item.id !== "GoForward" && 261 - item.id !== "Reload", 262 - ); 263 - 264 - // Store pending context menu for later 265 - this.pendingContextMenu = { 266 - controlId: detail.controlId, 267 - items: filteredItems, 268 - x: detail.position?.x || 0, 269 - y: detail.position?.y || 0, 270 - }; 271 - 272 - // Dispatch event to show radial menu at the touch position 273 - this.dispatchEvent( 274 - new CustomEvent("webview-show-radial-menu", { 275 - bubbles: true, 276 - composed: true, 277 - detail: { 278 - x: detail.position?.x || 0, 279 - y: detail.position?.y || 0, 280 - canGoBack: navState.canGoBack, 281 - canGoForward: navState.canGoForward, 282 - contextMenu: this.pendingContextMenu, 283 - }, 284 - }), 285 - ); 286 - 287 - // Don't respond yet - radial menu will handle it 288 - return; 289 - } 290 - 291 - // Show the context menu 292 - this.contextMenu = { 293 - controlId: detail.controlId, 294 - items, 295 - x: detail.position?.x || 0, 296 - y: detail.position?.y || 0, 297 - }; 229 + this.buildContextMenu(detail); 298 230 } else if ( 299 231 detail.controlType === "permission" && 300 232 detail.permissionParameters ··· 500 432 this.requestUpdate(); 501 433 } 502 434 435 + async buildContextMenu(detail) { 436 + const params = detail.contextMenuParameters; 437 + 438 + // Map action IDs to icons (matching title bar icons) 439 + const actionIcons = { 440 + GoBack: "arrow-left", 441 + GoForward: "arrow-right", 442 + Reload: "rotate-ccw", 443 + }; 444 + 445 + // Enrich items with icons 446 + const items = params.items.map((item) => ({ 447 + ...item, 448 + icon: actionIcons[item.id] || item.icon, 449 + })); 450 + 451 + // Add "Open in <peer>" items for connected paired peers. 452 + try { 453 + const peers = await navigator.embedder.pairing.peers(); 454 + const connected = peers.filter((p) => p.status === "paired-connected"); 455 + if (connected.length === 1) { 456 + items.push({ 457 + id: `p2p_open_in:${connected[0].id}`, 458 + label: `Open in ${connected[0].displayName}`, 459 + icon: "send", 460 + disabled: false, 461 + }); 462 + } else if (connected.length > 1) { 463 + for (const peer of connected) { 464 + items.push({ 465 + id: `p2p_open_in:${peer.id}`, 466 + label: `Open in ${peer.displayName}`, 467 + icon: "send", 468 + disabled: false, 469 + }); 470 + } 471 + } 472 + } catch { 473 + // Pairing service may not be running — skip the items. 474 + } 475 + 476 + // In mobile mode, show the radial menu instead of the regular context menu 477 + if (document.body.classList.contains("mobile-mode")) { 478 + // Extract navigation state from context menu items 479 + const navState = { 480 + canGoBack: params.items.some( 481 + (item) => item.id === "GoBack" && !item.disabled, 482 + ), 483 + canGoForward: params.items.some( 484 + (item) => item.id === "GoForward" && !item.disabled, 485 + ), 486 + }; 487 + 488 + // Filter items to remove actions that are part of radial menu 489 + const filteredItems = items.filter( 490 + (item) => 491 + item.id !== "GoBack" && 492 + item.id !== "GoForward" && 493 + item.id !== "Reload", 494 + ); 495 + 496 + // Store pending context menu for later 497 + this.pendingContextMenu = { 498 + controlId: detail.controlId, 499 + items: filteredItems, 500 + x: detail.position?.x || 0, 501 + y: detail.position?.y || 0, 502 + }; 503 + 504 + // Dispatch event to show radial menu at the touch position 505 + this.dispatchEvent( 506 + new CustomEvent("webview-show-radial-menu", { 507 + bubbles: true, 508 + composed: true, 509 + detail: { 510 + x: detail.position?.x || 0, 511 + y: detail.position?.y || 0, 512 + canGoBack: navState.canGoBack, 513 + canGoForward: navState.canGoForward, 514 + contextMenu: this.pendingContextMenu, 515 + }, 516 + }), 517 + ); 518 + 519 + // Don't respond yet - radial menu will handle it 520 + return; 521 + } 522 + 523 + // Show the context menu 524 + this.contextMenu = { 525 + controlId: detail.controlId, 526 + items, 527 + x: detail.position?.x || 0, 528 + y: detail.position?.y || 0, 529 + }; 530 + } 531 + 503 532 handleContextMenuAction(e) { 504 533 const { action, controlId } = e.detail; 505 534 console.log( ··· 510 539 ); 511 540 512 541 this.ensureIframe(); 542 + 543 + // Handle P2P "Open in <peer>" action locally. 544 + if (action.startsWith("p2p_open_in:")) { 545 + this.iframe.respondToContextMenu(controlId, null); 546 + this.contextMenu = null; 547 + this.dispatchEvent( 548 + new CustomEvent("p2p-open-in", { 549 + bubbles: true, 550 + composed: true, 551 + detail: { url: this.currentUrl }, 552 + }), 553 + ); 554 + return; 555 + } 513 556 514 557 // Send the action back to the embedded webview for handling 515 558 // The embedded webview will process the action (GoBack, Copy, Paste, etc.)