diff --git a/patches/components/constellation/atproto.rs.patch b/patches/components/constellation/atproto.rs.patch index 5beb4bf..5ae1f3c 100644 --- a/patches/components/constellation/atproto.rs.patch +++ b/patches/components/constellation/atproto.rs.patch @@ -1,6 +1,6 @@ --- original +++ modified -@@ -0,0 +1,458 @@ +@@ -0,0 +1,482 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::collections::HashSet; @@ -17,7 +17,7 @@ +use net_traits::{AtProtoSessionState, CoreResourceMsg, CoreResourceThread}; +use parking_lot::Mutex; +use servo_base::generic_channel::GenericCallback; -+use servo_constellation_traits::{AtProtoRequest, AtProtoResult}; ++use servo_constellation_traits::{AtProtoEvent, AtProtoRequest, AtProtoResult}; +use servo_url::ServoUrl; +use sync_wrapper::SyncWrapper; + @@ -29,10 +29,15 @@ + /// DIDs with a sync currently in flight, so we never race two syncs of the + /// same repo. Different DIDs still sync concurrently. + sync_in_flight: Arc>>, ++ /// Outbound channel for events broadcast to interested pipelines. ++ event_sender: crossbeam_channel::Sender, +} + +impl AtProtoManager { -+ pub(crate) fn new(resource_thread: CoreResourceThread) -> Self { ++ pub(crate) fn new( ++ resource_thread: CoreResourceThread, ++ event_sender: crossbeam_channel::Sender, ++ ) -> Self { + let (tx, rx) = channel().expect("Failed to create IPC channel"); + let _ = resource_thread.send(CoreResourceMsg::GetAtProtoSession(tx)); + let session = rx.recv().unwrap_or(None); @@ -55,6 +60,7 @@ + session: Arc::new(Mutex::new(session)), + config_dir, + sync_in_flight: Arc::new(Mutex::new(HashSet::new())), ++ event_sender, + } + } + @@ -99,6 +105,7 @@ + + let resource_thread = SyncWrapper::new(self.resource_thread.clone()); + let sync_in_flight = Arc::clone(&self.sync_in_flight); ++ let event_sender = self.event_sender.clone(); + + spawn_task(async move { + let result = sync_repo(&did, resource_thread.into_inner(), config_dir).await; @@ -106,12 +113,22 @@ + sync_in_flight.lock().remove(&did); + + let message = match result { -+ Ok(outcome) => AtProtoResult::RepoSynced { -+ did: outcome.did, -+ rev: outcome.rev, -+ added: outcome.added, -+ updated: outcome.updated, -+ removed: outcome.removed, ++ Ok(outcome) => { ++ // Notify interested pipelines only when an incremental sync ++ // stored new records. ++ if outcome.incremental && outcome.added > 0 { ++ let _ = event_sender.send(AtProtoEvent::FeedUpdated { ++ did: outcome.did.clone(), ++ added: outcome.added as u32, ++ }); ++ } ++ AtProtoResult::RepoSynced { ++ did: outcome.did, ++ rev: outcome.rev, ++ added: outcome.added, ++ updated: outcome.updated, ++ removed: outcome.removed, ++ } + }, + Err(err) => { + error!("Repo sync for {did} failed: {err:?}"); @@ -252,6 +269,7 @@ + let resource_thread = SyncWrapper::new(self.resource_thread.clone()); + let resource_thread2 = SyncWrapper::new(self.resource_thread.clone()); + let local_session = Arc::clone(&self.session); ++ let event_sender = self.event_sender.clone(); + + spawn_task(async move { + let result = SessionClient::create(&handle, &password, resource_thread).await; @@ -264,6 +282,10 @@ + &resource_thread2.into_inner(), + local_session, + ); ++ let _ = event_sender.send(AtProtoEvent::Login { ++ did: session.did.clone(), ++ handle: session.handle.clone(), ++ }); + } + if let Err(err) = response.send(result) { + error!("Failed to send new session: {err:?}"); @@ -287,6 +309,7 @@ + let session = session.clone(); + let local_session = Arc::clone(&self.session); + let local_session2 = Arc::clone(&self.session); ++ let event_sender = self.event_sender.clone(); + + spawn_task(async move { + let mut result = @@ -329,6 +352,7 @@ + let _ = resource_thread5 + .into_inner() + .send(CoreResourceMsg::UpdateAtProtoSession(None)); ++ let _ = event_sender.send(AtProtoEvent::Logout); + }, + } + diff --git a/patches/components/constellation/constellation.rs.patch b/patches/components/constellation/constellation.rs.patch index d168f4f..9f538a5 100644 --- a/patches/components/constellation/constellation.rs.patch +++ b/patches/components/constellation/constellation.rs.patch @@ -99,7 +99,7 @@ } #[derive(Debug)] -@@ -522,6 +539,37 @@ +@@ -522,6 +539,40 @@ /// to the `UserContents` need to be forwared to all the `ScriptThread`s that host /// the relevant `WebView`. pub(crate) user_contents_for_manager_id: FxHashMap, @@ -129,6 +129,9 @@ + #[cfg(any(target_os = "linux", target_os = "macos"))] + dbus_signal_receiver: crossbeam_channel::Receiver, + ++ /// ATProto events emitted by `AtProtoManager`, broadcast to interested pipelines. ++ atproto_event_receiver: crossbeam_channel::Receiver, ++ + /// Registry of web task providers for the delegation system. + webtask_registry: TaskRegistry, + @@ -137,7 +140,7 @@ } /// State needed to construct a constellation. -@@ -585,6 +633,9 @@ +@@ -585,6 +636,9 @@ /// The wake lock provider for acquiring and releasing OS-level screen wake locks. pub wake_lock_provider: Box, @@ -147,17 +150,19 @@ } /// When we are exiting a pipeline, we can either force exiting or not. A normal exit -@@ -667,6 +718,9 @@ +@@ -667,6 +721,11 @@ let broken_image_icon_data = resources::read_bytes(Resource::BrokenImageIcon); + #[cfg(any(target_os = "linux", target_os = "macos"))] + let (dbus_signal_tx, dbus_signal_rx) = crossbeam_channel::unbounded(); ++ ++ let (atproto_event_tx, atproto_event_rx) = crossbeam_channel::unbounded(); + let mut constellation: Constellation = Constellation { event_loops: Default::default(), namespace_receiver, -@@ -688,7 +742,7 @@ +@@ -688,7 +747,7 @@ script_to_devtools_callback: Default::default(), #[cfg(feature = "bluetooth")] bluetooth_ipc_sender: state.bluetooth_thread, @@ -166,7 +171,7 @@ private_resource_threads: state.private_resource_threads, public_storage_threads: state.public_storage_threads, private_storage_threads: state.private_storage_threads, -@@ -741,6 +795,19 @@ +@@ -741,6 +800,21 @@ pending_viewport_changes: Default::default(), screenshot_readiness_requests: Vec::new(), user_contents_for_manager_id: Default::default(), @@ -180,13 +185,15 @@ + dbus_signal_receiver: dbus_signal_rx, + at_proto: AtProtoManager::new( + state.public_resource_threads.core_thread, ++ atproto_event_tx, + ), ++ atproto_event_receiver: atproto_event_rx, + webtask_registry: TaskRegistry::new(state.config_dir.as_deref()), + pending_webtask_requests: HashMap::new(), }; constellation.run(); -@@ -766,6 +833,18 @@ +@@ -766,6 +840,18 @@ fn clean_up_finished_script_event_loops(&mut self) { self.event_loop_join_handles .retain(|join_handle| !join_handle.is_finished()); @@ -205,7 +212,7 @@ self.event_loops .retain(|event_loop| event_loop.upgrade().is_some()); } -@@ -1053,6 +1132,11 @@ +@@ -1053,6 +1139,11 @@ .get(&webview_id) .and_then(|webview| webview.user_content_manager_id); @@ -217,7 +224,7 @@ let new_pipeline_info = NewPipelineInfo { parent_info: parent_pipeline_id, new_pipeline_id, -@@ -1064,6 +1148,13 @@ +@@ -1064,6 +1155,13 @@ user_content_manager_id, theme, target_snapshot_params, @@ -231,16 +238,17 @@ }; let pipeline = match Pipeline::spawn(new_pipeline_info, event_loop, self, throttled) { Ok(pipeline) => pipeline, -@@ -1229,6 +1320,8 @@ +@@ -1229,6 +1327,9 @@ Script((WebViewId, PipelineId, ScriptToConstellationMessage)), BackgroundHangMonitor(HangMonitorAlert), Embedder(EmbedderToConstellationMessage), + PairingEvent(PairingEvent), + DBusSignal(servo_constellation_traits::DbusSignalEvent), ++ AtProtoEvent(servo_constellation_traits::AtProtoEvent), RemoveProcess(usize), } // Get one incoming request. -@@ -1248,6 +1341,23 @@ +@@ -1248,6 +1349,28 @@ sel.recv(&self.background_hang_monitor_receiver); sel.recv(&self.embedder_to_constellation_receiver); @@ -259,12 +267,17 @@ + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let dbus_index_offset = 0; + -+ let process_base_index = if has_pairing_receiver { 5 } else { 4 } + dbus_index_offset; ++ // ATProto events: always present, registered right after the optional ++ // pairing + dbus receivers. ++ sel.recv(&self.atproto_event_receiver); ++ let atproto_index = (if has_pairing_receiver { 5 } else { 4 }) + dbus_index_offset; ++ ++ let process_base_index = atproto_index + 1; + self.process_manager.register(&mut sel); let request = { -@@ -1274,9 +1384,26 @@ +@@ -1274,9 +1397,30 @@ oper.recv(&self.embedder_to_constellation_receiver) .expect("Unexpected embedder channel panic in constellation"), )), @@ -285,6 +298,10 @@ + unreachable!() + } + }, ++ i if i == atproto_index => Ok(Request::AtProtoEvent( ++ oper.recv(&self.atproto_event_receiver) ++ .expect("Unexpected ATProto event channel panic"), ++ )), _ => { // This can only be a error reading on a closed lifeline receiver. - let process_index = index - 4; @@ -292,7 +309,7 @@ let _ = oper.recv(self.process_manager.receiver_at(process_index)); Ok(Request::RemoveProcess(process_index)) }, -@@ -1299,6 +1426,23 @@ +@@ -1299,6 +1443,37 @@ Request::BackgroundHangMonitor(message) => { self.handle_request_from_background_hang_monitor(message); }, @@ -312,11 +329,25 @@ + } + } + } ++ }, ++ Request::AtProtoEvent(event) => { ++ if let Some(interested) = self ++ .pipeline_interests ++ .get(&ConstellationInterest::AtProtoEvent) ++ { ++ for pipeline_id in interested { ++ if let Some(pipeline) = self.pipelines.get(pipeline_id) { ++ let _ = pipeline ++ .event_loop ++ .send(ScriptThreadMessage::DispatchAtProtoEvent(event.clone())); ++ } ++ } ++ } + }, Request::RemoveProcess(index) => self.process_manager.remove(index), } } -@@ -1522,11 +1666,7 @@ +@@ -1522,11 +1697,7 @@ } }, EmbedderToConstellationMessage::PreferencesUpdated(updates) => { @@ -329,7 +360,7 @@ let _ = event_loop.send(ScriptThreadMessage::PreferencesUpdated( updates .iter() -@@ -1553,6 +1693,18 @@ +@@ -1553,6 +1724,18 @@ EmbedderToConstellationMessage::SetAccessibilityActive(webview_id, active) => { self.set_accessibility_active(webview_id, active); }, @@ -348,7 +379,7 @@ } } -@@ -1750,7 +1902,13 @@ +@@ -1750,7 +1933,13 @@ return warn!("Attempt to add channel name from an unexpected origin."); } self.broadcast_channels @@ -363,7 +394,7 @@ }, ScriptToConstellationMessage::RemoveBroadcastChannelNameInRouter( router_id, -@@ -1764,7 +1922,13 @@ +@@ -1764,7 +1953,13 @@ return warn!("Attempt to remove channel name from an unexpected origin."); } self.broadcast_channels @@ -378,7 +409,7 @@ }, ScriptToConstellationMessage::RemoveBroadcastChannelRouter(router_id, origin) => { if self -@@ -1776,6 +1940,12 @@ +@@ -1776,6 +1971,12 @@ self.broadcast_channels .remove_broadcast_channel_router(router_id); }, @@ -391,7 +422,7 @@ ScriptToConstellationMessage::ScheduleBroadcast(router_id, message) => { if self .check_origin_against_pipeline(&source_pipeline_id, &message.origin) -@@ -1785,8 +1955,15 @@ +@@ -1785,8 +1986,15 @@ "Attempt to schedule broadcast from an origin not matching the origin of the msg." ); } @@ -408,7 +439,7 @@ }, ScriptToConstellationMessage::PipelineExited => { self.handle_pipeline_exited(source_pipeline_id); -@@ -1806,6 +1983,12 @@ +@@ -1806,6 +2014,12 @@ ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info) => { self.handle_script_new_auxiliary(load_info); }, @@ -421,7 +452,7 @@ ScriptToConstellationMessage::ChangeRunningAnimationsState(animation_state) => { self.handle_change_running_animations_state(source_pipeline_id, animation_state) }, -@@ -1857,7 +2040,7 @@ +@@ -1857,7 +2071,7 @@ ScriptToConstellationMessage::SetFinalUrl(final_url) => { // The script may have finished loading after we already started shutting down. if let Some(ref mut pipeline) = self.pipelines.get_mut(&source_pipeline_id) { @@ -430,7 +461,7 @@ } else { warn!("constellation got set final url message for dead pipeline"); } -@@ -2007,6 +2190,29 @@ +@@ -2007,6 +2221,29 @@ new_value, ); }, @@ -460,21 +491,22 @@ ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => { // Unlikely at this point, but we may receive events coming from // different media sessions, so we set the active media session based -@@ -2025,7 +2231,12 @@ +@@ -2025,8 +2262,13 @@ }; self.active_media_session = Some(pipeline_id); self.constellation_to_embedder_proxy.send( - ConstellationToEmbedderMsg::MediaSessionEvent(webview_id, event), + ConstellationToEmbedderMsg::MediaSessionEvent(webview_id, event.clone()), -+ ); + ); + // Also route to embedded webview parent iframe. + self.handle_embedded_webview_notification( + webview_id, + EmbeddedWebViewEventType::MediaSessionEvent(event), - ); ++ ); }, #[cfg(feature = "webgpu")] -@@ -2099,7 +2310,988 @@ + ScriptToConstellationMessage::RequestAdapter(response_sender, options, ids) => self +@@ -2099,7 +2341,988 @@ } }, }, @@ -1463,7 +1495,7 @@ } /// Check the origin of a message against that of the pipeline it came from. -@@ -2418,6 +3610,55 @@ +@@ -2418,6 +3641,55 @@ TransferState::TransferInProgress(queue) => queue.push_back(task), TransferState::CompletionFailed(queue) => queue.push_back(task), TransferState::CompletionRequested(_, queue) => queue.push_back(task), @@ -1519,7 +1551,7 @@ } } -@@ -3325,6 +4566,40 @@ +@@ -3325,6 +4597,40 @@ /// fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { debug!("{webview_id}: Closing"); @@ -1560,7 +1592,7 @@ let browsing_context_id = BrowsingContextId::from(webview_id); // Step 5. Remove traversable from the user agent's top-level traversable set. let browsing_context = -@@ -3601,8 +4876,27 @@ +@@ -3601,8 +4907,27 @@ opener_webview_id, opener_pipeline_id, response_sender, @@ -1588,7 +1620,7 @@ let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { warn!("Failed to create channel"); let _ = response_sender.send(None); -@@ -3701,6 +4995,392 @@ +@@ -3701,6 +5026,392 @@ }); } @@ -1981,7 +2013,7 @@ #[servo_tracing::instrument(skip_all)] fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { let Some(pipeline) = self.pipelines.get(&pipeline_id) else { -@@ -4250,7 +5930,7 @@ +@@ -4250,7 +5961,7 @@ }, }; @@ -1990,7 +2022,7 @@ match self.browsing_contexts.get_mut(&browsing_context_id) { Some(browsing_context) => { let old_pipeline_id = browsing_context.pipeline_id; -@@ -4259,6 +5939,7 @@ +@@ -4259,6 +5970,7 @@ old_pipeline_id, browsing_context.parent_pipeline_id, browsing_context.webview_id, @@ -1998,7 +2030,7 @@ ) }, None => { -@@ -4268,6 +5949,15 @@ +@@ -4268,6 +5980,15 @@ self.unload_document(old_pipeline_id); @@ -2014,7 +2046,7 @@ if let Some(new_pipeline) = self.pipelines.get(&new_pipeline_id) { if let Some(ref chan) = self.devtools_sender { let state = NavigationState::Start(new_pipeline.url.clone()); -@@ -4836,7 +6526,7 @@ +@@ -4836,7 +6557,7 @@ } #[servo_tracing::instrument(skip_all)] @@ -2023,7 +2055,7 @@ // Send a flat projection of the history to embedder. // The final vector is a concatenation of the URLs of the past // entries, the current entry and the future entries. -@@ -4948,9 +6638,22 @@ +@@ -4948,9 +6669,22 @@ self.constellation_to_embedder_proxy .send(ConstellationToEmbedderMsg::HistoryChanged( webview_id, @@ -2047,7 +2079,7 @@ } #[servo_tracing::instrument(skip_all)] -@@ -4969,7 +6672,7 @@ +@@ -4969,7 +6703,7 @@ webview.focused_browsing_context_id = change.browsing_context_id; } @@ -2056,7 +2088,7 @@ match self.browsing_contexts.get_mut(&change.browsing_context_id) { Some(browsing_context) => { debug!("Adding pipeline to existing browsing context."); -@@ -4976,11 +6679,15 @@ +@@ -4976,11 +6710,15 @@ let old_pipeline_id = browsing_context.pipeline_id; browsing_context.pipelines.insert(change.new_pipeline_id); browsing_context.update_current_entry(change.new_pipeline_id); @@ -2074,7 +2106,7 @@ }, }; -@@ -4988,6 +6695,18 @@ +@@ -4988,6 +6726,18 @@ self.unload_document(old_pipeline_id); } diff --git a/patches/components/net/atproto/sync.rs.patch b/patches/components/net/atproto/sync.rs.patch index 0aa5673..32ad2b6 100644 --- a/patches/components/net/atproto/sync.rs.patch +++ b/patches/components/net/atproto/sync.rs.patch @@ -1,6 +1,6 @@ --- original +++ modified -@@ -0,0 +1,259 @@ +@@ -0,0 +1,271 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Syncing a single ATProto repository (by DID) into the local store. @@ -46,6 +46,9 @@ + pub added: usize, + pub updated: usize, + pub removed: usize, ++ /// Whether this repo had been synced before (a `since=` incremental). Lets ++ /// callers tell genuine new content from a first-time backfill. ++ pub incremental: bool, +} + +#[derive(Debug)] @@ -80,7 +83,8 @@ + // blocking pool with a read-only connection. + let prior_rev = read_prior_rev(config_dir, did.to_owned()).await; + -+ match try_sync( ++ let was_incremental = prior_rev.is_some(); ++ let result = match try_sync( + did, + &endpoint, + resource_thread.clone(), @@ -93,7 +97,13 @@ + try_sync(did, &endpoint, resource_thread, None).await + }, + other => other, -+ } ++ }; ++ // `incremental` distinguishes genuine new content (the feed `feedupdated` ++ // event + notifications) from a first-time backfill. ++ result.map(|mut outcome| { ++ outcome.incremental = was_incremental; ++ outcome ++ }) +} + +/// Read the stored rev for `did` off the async worker threads (read-only @@ -258,5 +268,7 @@ + added, + updated, + removed, ++ // Set by `sync_repo` (which knows the prior rev); a sensible default here. ++ incremental: false, + }) +} diff --git a/patches/components/script/dom/atproto.rs.patch b/patches/components/script/dom/atproto.rs.patch index 94ea762..4edabdc 100644 --- a/patches/components/script/dom/atproto.rs.patch +++ b/patches/components/script/dom/atproto.rs.patch @@ -1,6 +1,6 @@ --- original +++ modified -@@ -0,0 +1,238 @@ +@@ -0,0 +1,308 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::rc::Rc; @@ -9,16 +9,23 @@ +use js::context::JSContext; +use js::jsval::UndefinedValue; +use script_bindings::error::Error; -+use script_bindings::reflector::{Reflector, reflect_dom_object}; ++use script_bindings::reflector::reflect_dom_object; +use script_bindings::str::{DOMString, USVString}; -+use servo_constellation_traits::{AtProtoRequest, AtProtoResult, ScriptToConstellationMessage}; ++use servo_constellation_traits::{ ++ AtProtoEvent, AtProtoRequest, AtProtoResult, ScriptToConstellationMessage, ++}; + +use crate::dom::bindings::codegen::Bindings::AtProtoBinding::{ + AtProtoMethods, AtProtoRecordRef, AtProtoRepoStatus, AtProtoSession, AtProtoSyncResult, + AtProtoUrlRef, +}; ++use crate::dom::bindings::codegen::Bindings::CustomEventBinding::CustomEventMethods; ++use crate::dom::bindings::inheritance::Castable; +use crate::dom::bindings::reflector::DomGlobal; +use crate::dom::bindings::root::DomRoot; ++use crate::dom::customevent::CustomEvent; ++use crate::dom::event::Event; ++use crate::dom::eventtarget::EventTarget; +use crate::dom::globalscope::GlobalScope; +use crate::dom::promise::Promise; +use crate::routed_promise::{RoutedPromiseListener, callback_promise}; @@ -26,19 +33,63 @@ + +#[dom_struct] +pub(crate) struct AtProto { -+ reflector_: Reflector, ++ eventtarget: EventTarget, +} + +impl AtProto { + pub fn new_inherited() -> AtProto { + AtProto { -+ reflector_: Reflector::new(), ++ eventtarget: EventTarget::new_inherited(), + } + } + + pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot { + reflect_dom_object(Box::new(AtProto::new_inherited()), global, can_gc) + } ++ ++ /// Fire a `login` / `logout` / `feedupdated` event (the first and last carry ++ /// a CustomEvent `detail`) on this `navigator.atproto`. Called by the script ++ /// thread when the constellation broadcasts an [`AtProtoEvent`] to a pipeline ++ /// that registered interest. ++ #[expect(unsafe_code)] ++ pub(crate) fn dispatch_event(&self, cx: &mut JSContext, event: &AtProtoEvent) { ++ let (name, detail_json): (&str, Option) = match event { ++ AtProtoEvent::Login { did, handle } => ( ++ "login", ++ Some(serde_json::json!({ "did": did, "handle": handle }).to_string()), ++ ), ++ AtProtoEvent::Logout => ("logout", None), ++ AtProtoEvent::FeedUpdated { did, added } => ( ++ "feedupdated", ++ Some(serde_json::json!({ "did": did, "added": added }).to_string()), ++ ), ++ }; ++ ++ rooted!(&in(cx) let mut detail = UndefinedValue()); ++ if let Some(json) = detail_json { ++ unsafe { ++ let utf16: Vec = json.encode_utf16().collect(); ++ let _ = js::jsapi::JS_ParseJSON( ++ cx.raw_cx(), ++ utf16.as_ptr(), ++ utf16.len() as u32, ++ detail.handle_mut().into(), ++ ); ++ } ++ } ++ ++ let custom_event = CustomEvent::new_uninitialized(cx, &self.global()); ++ custom_event.InitCustomEvent( ++ cx.into(), ++ DOMString::from(name), ++ false, ++ false, ++ detail.handle(), ++ ); ++ custom_event ++ .upcast::() ++ .fire(cx, self.upcast::()); ++ } +} + +impl AtProto { @@ -64,6 +115,25 @@ +} + +impl AtProtoMethods for AtProto { ++ registered_event_handler!( ++ servo_constellation_traits::ConstellationInterest::AtProtoEvent, ++ login, ++ GetOnlogin, ++ SetOnlogin ++ ); ++ registered_event_handler!( ++ servo_constellation_traits::ConstellationInterest::AtProtoEvent, ++ logout, ++ GetOnlogout, ++ SetOnlogout ++ ); ++ registered_event_handler!( ++ servo_constellation_traits::ConstellationInterest::AtProtoEvent, ++ feedupdated, ++ GetOnfeedupdated, ++ SetOnfeedupdated ++ ); ++ + /// + fn Login(&self, cx: &mut JSContext, handle: USVString, password: USVString) -> Rc { + self.request( diff --git a/patches/components/script/dom/event/eventtarget.rs.patch b/patches/components/script/dom/event/eventtarget.rs.patch new file mode 100644 index 0000000..ecf3366 --- /dev/null +++ b/patches/components/script/dom/event/eventtarget.rs.patch @@ -0,0 +1,24 @@ +--- original ++++ modified +@@ -31,6 +31,7 @@ + + use crate::conversions::Convert; + use crate::dom::abortsignal::{AbortAlgorithm, RemovableDomEventListener}; ++use crate::dom::atproto::AtProto; + use crate::dom::beforeunloadevent::BeforeUnloadEvent; + use crate::dom::bindings::callback::{CallbackContainer, CallbackFunction, ExceptionHandling}; + use crate::dom::bindings::codegen::Bindings::BeforeUnloadEventBinding::BeforeUnloadEventMethods; +@@ -440,6 +441,13 @@ + if self.is::() && *ty == atom!("storage") { + return Some(ConstellationInterest::StorageEvent); + } ++ if self.is::() && ++ (*ty == Atom::from("login") || ++ *ty == Atom::from("logout") || ++ *ty == Atom::from("feedupdated")) ++ { ++ return Some(ConstellationInterest::AtProtoEvent); ++ } + None + } + diff --git a/patches/components/script/dom/html/htmlmetaelement.rs.patch b/patches/components/script/dom/html/htmlmetaelement.rs.patch index 3984b81..fa18d17 100644 --- a/patches/components/script/dom/html/htmlmetaelement.rs.patch +++ b/patches/components/script/dom/html/htmlmetaelement.rs.patch @@ -26,7 +26,7 @@ + .filter(|value| !value.is_empty()) + { + // Store the theme color on the document, which will notify the parent -+ self.owner_document().set_theme_color(content.into()); ++ self.owner_document().set_theme_color(content); + } + } + diff --git a/patches/components/script/dom/navigator/navigator.rs.patch b/patches/components/script/dom/navigator/navigator.rs.patch index a08f5f9..4643ba4 100644 --- a/patches/components/script/dom/navigator/navigator.rs.patch +++ b/patches/components/script/dom/navigator/navigator.rs.patch @@ -90,7 +90,7 @@ } } -@@ -180,6 +197,11 @@ +@@ -180,6 +197,16 @@ self.xr.get() } @@ -98,11 +98,16 @@ + pub(crate) fn get_embedder(&self) -> Option> { + self.embedder.get() + } ++ ++ /// Returns the AtProto object if it has been created, without creating one. ++ pub(crate) fn get_atproto(&self) -> Option> { ++ self.at_proto.get() ++ } + #[cfg(feature = "gamepad")] pub(crate) fn get_gamepad(&self, index: usize) -> Option> { self.gamepads.borrow().get(index).and_then(|g| g.get()) -@@ -620,6 +642,18 @@ +@@ -620,6 +647,18 @@ .or_init(|| ServoInternals::new(&self.global(), CanGc::deprecated_note())) } @@ -121,7 +126,7 @@ /// fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> { // Step 1. Let (normalizedScheme, normalizedURLString) be the result of -@@ -668,6 +702,210 @@ +@@ -668,6 +707,210 @@ fn WakeLock(&self, cx: &mut js::context::JSContext) -> DomRoot { self.wake_lock.or_init(|| WakeLock::new(cx, &self.global())) } @@ -332,7 +337,7 @@ } struct BeaconFetchListener { -@@ -728,3 +966,31 @@ +@@ -728,3 +971,31 @@ self.global.root() } } diff --git a/patches/components/script/messaging.rs.patch b/patches/components/script/messaging.rs.patch index cc888c1..e389077 100644 --- a/patches/components/script/messaging.rs.patch +++ b/patches/components/script/messaging.rs.patch @@ -1,6 +1,6 @@ --- original +++ modified -@@ -110,6 +110,15 @@ +@@ -110,6 +110,16 @@ ScriptThreadMessage::UpdatePinchZoomInfos(id, _) => Some(*id), ScriptThreadMessage::SetAccessibilityActive(..) => None, ScriptThreadMessage::TriggerGarbageCollection => None, @@ -8,6 +8,7 @@ + ScriptThreadMessage::DispatchServoError(..) => None, + ScriptThreadMessage::DispatchPairingEvent(..) => None, + ScriptThreadMessage::DispatchDBusSignal(..) => None, ++ ScriptThreadMessage::DispatchAtProtoEvent(..) => None, + ScriptThreadMessage::DispatchPeerStream(..) => None, + ScriptThreadMessage::SetSpatialNavigation(pipeline_id, ..) => Some(*pipeline_id), + ScriptThreadMessage::ShowTaskChooser(..) => None, diff --git a/patches/components/script/script_thread.rs.patch b/patches/components/script/script_thread.rs.patch index c907d0b..38feb1d 100644 --- a/patches/components/script/script_thread.rs.patch +++ b/patches/components/script/script_thread.rs.patch @@ -57,7 +57,7 @@ use crate::dom::servoparser::{ParserContext, ServoParser}; use crate::dom::types::DebuggerGlobalScope; #[cfg(feature = "webgpu")] -@@ -1965,12 +1973,47 @@ +@@ -1965,11 +1973,46 @@ self.handle_refresh_cursor(pipeline_id); }, ScriptThreadMessage::PreferencesUpdated(updates) => { @@ -97,7 +97,7 @@ + &providers_json, + default_provider.as_deref(), + ); - }, ++ }, + ScriptThreadMessage::OpenTaskProvider(request_id, url, title) => { + // Dispatch an embedder event so the system UI opens the provider webview. + self.dispatch_open_task_provider(cx, &request_id, &url, &title); @@ -105,11 +105,10 @@ + ScriptThreadMessage::TaskProvidersUpdate(request_id, providers_json) => { + // Dispatch an embedder event to update the chooser with remote providers. + self.dispatch_task_providers_update(cx, &request_id, &providers_json); -+ }, + }, ScriptThreadMessage::ForwardKeyboardScroll(pipeline_id, scroll) => { if let Some(document) = self.documents.borrow().find_document(pipeline_id) { - document.event_handler().do_keyboard_scroll(cx, scroll); -@@ -2002,6 +2045,43 @@ +@@ -2002,6 +2045,46 @@ ScriptThreadMessage::TriggerGarbageCollection => unsafe { JS_GC(*GlobalScope::get_cx(), GCReason::API); }, @@ -129,6 +128,9 @@ + ScriptThreadMessage::DispatchDBusSignal(event) => { + self.handle_dispatch_dbus_signal(cx, event); + }, ++ ScriptThreadMessage::DispatchAtProtoEvent(event) => { ++ self.handle_dispatch_atproto_event(cx, event); ++ }, + ScriptThreadMessage::DispatchPeerStream( + peer_id, + remote_port_id_bytes, @@ -153,7 +155,7 @@ } } -@@ -3055,6 +3135,9 @@ +@@ -3055,6 +3138,9 @@ .documents .borrow() .find_iframe(parent_pipeline_id, browsing_context_id); @@ -163,7 +165,7 @@ let Some(frame_element) = frame_element else { return; }; -@@ -3079,6 +3162,7 @@ +@@ -3079,6 +3165,7 @@ // is no need to pass along existing opener information that // will be discarded. None, @@ -171,7 +173,7 @@ ); } -@@ -3365,6 +3449,170 @@ +@@ -3365,6 +3452,183 @@ } } @@ -240,6 +242,19 @@ + } + } + ++ fn handle_dispatch_atproto_event( ++ &self, ++ cx: &mut js::context::JSContext, ++ event: servo_constellation_traits::AtProtoEvent, ++ ) { ++ for (_, document) in self.documents.borrow().iter() { ++ if let Some(atproto) = document.window().Navigator().get_atproto() { ++ let _ac = enter_realm(&*atproto); ++ atproto.dispatch_event(cx, &event); ++ } ++ } ++ } ++ + /// Handle an incoming peer stream: create a local MessagePort and fire "peerstream" on Window. + /// Only fires on documents whose URL matches the target_url specified by the sender. + /// If preventDefault() is called on the event, deny the offer. @@ -342,7 +357,7 @@ fn ask_constellation_for_top_level_info( &self, sender_webview_id: WebViewId, -@@ -3472,7 +3720,13 @@ +@@ -3472,7 +3736,13 @@ self.senders.pipeline_to_embedder_sender.clone(), self.senders.constellation_sender.clone(), incomplete.pipeline_id, @@ -357,7 +372,7 @@ incomplete.viewport_details, origin.clone(), final_url.clone(), -@@ -3494,6 +3748,8 @@ +@@ -3494,6 +3764,8 @@ #[cfg(feature = "webgpu")] self.gpu_id_hub.clone(), incomplete.load_data.inherited_secure_context, @@ -366,7 +381,7 @@ incomplete.theme, self.this.clone(), ); -@@ -3599,6 +3855,8 @@ +@@ -3599,6 +3871,8 @@ ); } @@ -375,7 +390,7 @@ document.set_ready_state(cx, DocumentReadyState::Loading); self.documents -@@ -3605,8 +3863,6 @@ +@@ -3605,8 +3879,6 @@ .borrow_mut() .insert(incomplete.pipeline_id, &document); @@ -384,7 +399,7 @@ // Initialize the browsing context for the window. let window_proxy = self.window_proxies.local_window_proxy( cx, -@@ -3617,6 +3873,7 @@ +@@ -3617,6 +3889,7 @@ incomplete.webview_id, incomplete.parent_info, incomplete.opener, @@ -392,7 +407,7 @@ ); if window_proxy.parent().is_some() { // https://html.spec.whatwg.org/multipage/#navigating-across-documents:delaying-load-events-mode-2 -@@ -4355,10 +4612,84 @@ +@@ -4355,10 +4628,84 @@ document.event_handler().handle_refresh_cursor(); } @@ -477,7 +492,7 @@ fn handle_request_screenshot_readiness( &self, webview_id: WebViewId, -@@ -4399,7 +4730,7 @@ +@@ -4399,7 +4746,7 @@ can_gc: CanGc, ) { let Some(window) = self.documents.borrow().find_window(pipeline_id) else { diff --git a/patches/components/script_bindings/webidls/AtProto.webidl.patch b/patches/components/script_bindings/webidls/AtProto.webidl.patch index fc94f13..d24d4fe 100644 --- a/patches/components/script_bindings/webidls/AtProto.webidl.patch +++ b/patches/components/script_bindings/webidls/AtProto.webidl.patch @@ -1,6 +1,6 @@ --- original +++ modified -@@ -0,0 +1,101 @@ +@@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ @@ -46,7 +46,7 @@ + +[Exposed=Window, +Func="Embedder::is_allowed_to_embed"] -+interface AtProto { ++interface AtProto : EventTarget { + // Tries to login with the submitted credentials. + // Resolves with the new session if successful, rejects otherwise. + Promise login(USVString handle, USVString password); @@ -96,6 +96,13 @@ + // writes / DROP / ATTACH fail; SQLite's json1 (`json_extract(json, + // '$.field')`) is available. Capped at 1000 rows. + Promise queryStore(USVString sql, optional sequence params = []); ++ ++ // Fired on a successful login. CustomEvent; detail = { did, handle }. ++ attribute EventHandler onlogin; ++ // Fired when the session is logged out. Plain event (no detail). ++ attribute EventHandler onlogout; ++ // Fired when a syncRepo() stored new records. CustomEvent; detail = { did, added }. ++ attribute EventHandler onfeedupdated; +}; + +partial interface Navigator { diff --git a/patches/components/shared/constellation/from_script_message.rs.patch b/patches/components/shared/constellation/from_script_message.rs.patch index 36fe8e7..54aaaff 100644 --- a/patches/components/shared/constellation/from_script_message.rs.patch +++ b/patches/components/shared/constellation/from_script_message.rs.patch @@ -87,14 +87,16 @@ /// Specifies the information required to load an iframe. #[derive(Debug, Deserialize, Serialize)] pub struct IFrameLoadInfo { -@@ -596,8 +633,136 @@ +@@ -596,8 +633,138 @@ pub enum ConstellationInterest { /// Interest in `storage` events (fired when another same-origin pipeline modifies storage). StorageEvent, + /// Interest in D-Bus signal events. + DBusSignal, -+} -+ ++ /// Interest in ATProto events (login / logout / feedupdated). ++ AtProtoEvent, + } + +#[derive(Deserialize, Serialize)] +pub enum AtProtoRequest { + /// User, Password @@ -178,8 +180,8 @@ + pub did: String, + pub active: bool, + pub status: Option, - } - ++} ++ +#[derive(Debug, Deserialize, Serialize)] +pub enum AtProtoResult { + NewSession(AtProtoNewSession, ServoUrl), // (session, endpoint_url) @@ -224,7 +226,7 @@ /// Messages from the script to the constellation. #[derive(Deserialize, IntoStaticStr, Serialize)] pub enum ScriptToConstellationMessage { -@@ -643,6 +808,10 @@ +@@ -643,6 +810,10 @@ NewBroadcastChannelNameInRouter(BroadcastChannelRouterId, String, ImmutableOrigin), /// A global stopped managing broadcast channels for a given channel-name. RemoveBroadcastChannelNameInRouter(BroadcastChannelRouterId, String, ImmutableOrigin), @@ -235,7 +237,7 @@ /// Broadcast a message to all same-origin broadcast channels, /// excluding the source of the broadcast. ScheduleBroadcast(BroadcastChannelRouterId, BroadcastChannelMsg), -@@ -661,6 +830,9 @@ +@@ -661,6 +832,9 @@ Option, Option, ), @@ -245,7 +247,7 @@ /// Indicates whether this pipeline is currently running animations. ChangeRunningAnimationsState(AnimationState), /// Requests that a new 2D canvas thread be created. (This is done in the constellation because -@@ -748,6 +920,10 @@ +@@ -748,6 +922,10 @@ ScriptNewIFrame(IFrameLoadInfoWithData), /// Script has opened a new auxiliary browsing context. CreateAuxiliaryWebView(AuxiliaryWebViewCreationRequest), @@ -256,7 +258,7 @@ /// Mark a new document as active ActivateDocument, /// Set the document state for a pipeline (used by screenshot / reftests) -@@ -803,6 +979,177 @@ +@@ -803,6 +981,177 @@ /// aggregate lock count and notify the provider only when the count transitions from N to 0. /// ReleaseWakeLock(WakeLockType), diff --git a/patches/components/shared/constellation/lib.rs.patch b/patches/components/shared/constellation/lib.rs.patch index 48e7a74..fbf0248 100644 --- a/patches/components/shared/constellation/lib.rs.patch +++ b/patches/components/shared/constellation/lib.rs.patch @@ -19,7 +19,7 @@ }; pub use from_script_message::*; use malloc_size_of_derive::MallocSizeOf; -@@ -30,15 +32,173 @@ +@@ -30,15 +32,185 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use servo_base::cross_process_instant::CrossProcessInstant; @@ -124,6 +124,18 @@ + pub args_json: serde_json::Value, +} + ++/// An ATProto event broadcasted to the pipelines that registered ++/// `ConstellationInterest::AtProtoEvent`. ++#[derive(Clone, Debug, Deserialize, Serialize)] ++pub enum AtProtoEvent { ++ /// A login succeeded. ++ Login { did: String, handle: String }, ++ /// The session was logged out. ++ Logout, ++ /// A repo sync stored new records. ++ FeedUpdated { did: String, added: u32 }, ++} ++ +/// Information about a remote P2P peer. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PeerInfo { @@ -195,7 +207,7 @@ /// Messages to the Constellation from the embedding layer, whether from `ServoRenderer` or /// from `libservo` itself. #[derive(IntoStaticStr)] -@@ -116,6 +276,9 @@ +@@ -116,6 +288,9 @@ UpdatePinchZoomInfos(PipelineId, PinchZoomInfos), /// Activate or deactivate accessibility features for the given `WebView`. SetAccessibilityActive(WebViewId, bool), diff --git a/patches/components/shared/script/lib.rs.patch b/patches/components/shared/script/lib.rs.patch index db71751..da2cba2 100644 --- a/patches/components/shared/script/lib.rs.patch +++ b/patches/components/shared/script/lib.rs.patch @@ -53,7 +53,7 @@ /// Notify the `ScriptThread` that the Servo renderer is no longer waiting on /// asynchronous image uploads for the given `Pipeline`. These are mainly used /// by canvas to perform uploads while the display list is being built. -@@ -329,6 +346,28 @@ +@@ -329,6 +346,30 @@ SetAccessibilityActive(PipelineId, bool, Epoch), /// Force a garbage collection in this script thread. TriggerGarbageCollection, @@ -72,8 +72,10 @@ + DispatchServoError(ServoErrorType, String), + /// Dispatch a pairing event to all `navigator.embedder.pairing` instances in this script thread. + DispatchPairingEvent(PairingEvent), -+ /// Dispatch a D-Bus signal event to script. ++ /// Dispatch a D-Bus signal event in this script thread. + DispatchDBusSignal(servo_constellation_traits::DbusSignalEvent), ++ /// Dispatch an ATProto event to `navigator.atproto` instances in this script thread. ++ DispatchAtProtoEvent(servo_constellation_traits::AtProtoEvent), + /// Dispatch a peer stream event — a remote peer is offering a MessagePort. + /// Contains (peer_id, serialized remote port_id bytes, stream_id, from_peer_id, target_url). + DispatchPeerStream(String, Vec, String, String, String),