diff --git a/crates/ely_app/src/shell/history.rs b/crates/ely_app/src/shell/history.rs new file mode 100644 index 0000000..091afa6 --- /dev/null +++ b/crates/ely_app/src/shell/history.rs @@ -0,0 +1,30 @@ +use gpui::Context; + +use super::{ElyShell, ShellState}; + +impl ElyShell { + pub(super) fn request_clear_history_confirmation(&mut self, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && let Ok(snapshot) = core.snapshot() + { + self.history_clear_confirmation = Some(snapshot.active_profile_id); + cx.notify(); + } + } + + pub(super) fn cancel_clear_history_confirmation(&mut self, cx: &mut Context) { + self.history_clear_confirmation = None; + cx.notify(); + } + + pub(super) fn clear_active_profile_history(&mut self, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && let Ok(snapshot) = core.snapshot() + && self.history_clear_confirmation.as_ref() == Some(&snapshot.active_profile_id) + && core.clear_active_profile_history().is_ok() + { + self.history_clear_confirmation = None; + cx.notify(); + } + } +} diff --git a/crates/ely_app/src/shell/internal_pages/privacy_security.rs b/crates/ely_app/src/shell/internal_pages/privacy_security.rs index 1a1ce0e..96e92ea 100644 --- a/crates/ely_app/src/shell/internal_pages/privacy_security.rs +++ b/crates/ely_app/src/shell/internal_pages/privacy_security.rs @@ -1,6 +1,7 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; use ely_domain::HistoryRecordingPolicy; +use gpui::prelude::FluentBuilder; use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb}; use gpui_component::{ IconName, Selectable, Sizable, StyledExt, @@ -16,6 +17,9 @@ impl ElyShell { snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { + let confirming_clear = + self.history_clear_confirmation.as_ref() == Some(&snapshot.active_profile_id); + render_canvas_surface( div() .size_full() @@ -25,6 +29,9 @@ impl ElyShell { .gap_5() .child(render_privacy_header(snapshot)) .child(render_history_summary(snapshot)) + .when(snapshot.active_profile_history_entry_count > 0, |this| { + this.child(render_history_clear_controls(confirming_clear, cx)) + }) .child(render_history_policy_rows(snapshot.history_recording_policy, cx)), ) } @@ -120,7 +127,91 @@ fn render_history_summary(snapshot: &BrowserSnapshot) -> AnyElement { .text_xs() .font_semibold() .text_color(rgb(colors::MUTED)) - .child(format!("{} visible entries", snapshot.history_entries.len())), + .child(format!("{} Profile entries", snapshot.active_profile_history_entry_count)), + ) + .into_any_element() +} + +fn render_history_clear_controls(confirming_clear: bool, cx: &mut Context) -> AnyElement { + if confirming_clear { + return render_clear_history_confirmation(cx); + } + + div() + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .text_sm() + .text_color(rgb(colors::MUTED)) + .child("Clear all history saved for this Profile."), + ) + .child( + Button::new("request-clear-history") + .danger() + .xsmall() + .label("Clear History") + .tooltip("Clear Profile History") + .on_click(cx.listener(|shell, _, _, cx| { + shell.request_clear_history_confirmation(cx); + })), + ) + .into_any_element() +} + +fn render_clear_history_confirmation(cx: &mut Context) -> AnyElement { + div() + .rounded_md() + .border_1() + .border_color(rgb(colors::ERROR)) + .bg(rgb(colors::CANVAS_SOFT)) + .px_4() + .py_3() + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child("Confirm history clearing"), + ) + .child( + div() + .text_xs() + .text_color(rgb(colors::MUTED)) + .child("This removes Profile history across every Space."), + ), + ) + .child( + div() + .flex() + .items_center() + .gap_2() + .child( + Button::new("cancel-clear-history").ghost().xsmall().label("Cancel").on_click( + cx.listener(|shell, _, _, cx| { + shell.cancel_clear_history_confirmation(cx); + }), + ), + ) + .child( + Button::new("confirm-clear-history").danger().xsmall().label("Clear").on_click( + cx.listener(|shell, _, _, cx| { + shell.clear_active_profile_history(cx); + }), + ), + ), ) .into_any_element() } diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index d7de6d3..d1b2e61 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -1,4 +1,5 @@ mod downloads; +mod history; mod internal_pages; mod plugins; mod render; @@ -36,6 +37,7 @@ pub struct ElyShell { download_action_error: Option, download_clear_confirmation: bool, download_security_confirmation: Option, + history_clear_confirmation: Option, site_permissions_clear_confirmation: Option, plugin_install_error: Option, pending_plugin_install: Option, @@ -98,6 +100,7 @@ impl ElyShell { download_action_error: None, download_clear_confirmation: false, download_security_confirmation: None, + history_clear_confirmation: None, site_permissions_clear_confirmation: None, plugin_install_error: None, pending_plugin_install: None, diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 139efc6..3e07a17 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -55,6 +55,7 @@ pub struct BrowserSnapshot { pub site_permission_audit_events: Vec, pub download_entries: Vec, pub history_entries: Vec, + pub active_profile_history_entry_count: usize, pub split_layouts: Vec, pub installed_plugins: Vec, pub plugin_audit_events: Vec, @@ -291,6 +292,7 @@ impl BrowserCore { site_permission_audit_events: self.visible_site_permission_audit_events(), download_entries: self.visible_downloads(), history_entries: self.visible_history(), + active_profile_history_entry_count: self.active_profile_history_count(), split_layouts: self.visible_split_layouts(), installed_plugins: self.installed_plugins.clone(), plugin_audit_events: self.plugin_audit_events.clone(), diff --git a/crates/ely_browser_core/src/state/history.rs b/crates/ely_browser_core/src/state/history.rs index 7e7c3a0..bf03759 100644 --- a/crates/ely_browser_core/src/state/history.rs +++ b/crates/ely_browser_core/src/state/history.rs @@ -7,6 +7,11 @@ use crate::navigation::records_history; use super::BrowserCore; impl BrowserCore { + pub fn clear_active_profile_history(&mut self) -> Result { + let profile_id = self.active_profile_id.clone(); + self.clear_profile_history(&profile_id) + } + pub(super) fn record_history_entry(&mut self, tab: &BrowserTab) { if !self.history_recording_policy.records_history() || !records_history(tab.url()) @@ -50,6 +55,23 @@ impl BrowserCore { .collect() } + pub(super) fn active_profile_history_count(&self) -> usize { + self.history_entries + .iter() + .filter(|entry| entry.profile_id() == &self.active_profile_id) + .count() + } + + fn clear_profile_history(&mut self, profile_id: &ProfileId) -> Result { + if !self.profiles.iter().any(|profile| profile.id() == profile_id) { + return Err(crate::CoreError::ProfileNotFound { id: profile_id.clone() }); + } + + let original_count = self.history_entries.len(); + self.history_entries.retain(|entry| entry.profile_id() != profile_id); + Ok(original_count - self.history_entries.len()) + } + fn profile_records_history(&self, profile_id: &ProfileId) -> bool { match self.profiles.iter().find(|profile| profile.id() == profile_id) { Some(profile) => profile.kind() == &ProfileKind::Standard, diff --git a/crates/ely_browser_core/tests/history.rs b/crates/ely_browser_core/tests/history.rs index 0f918d7..dbaa0b0 100644 --- a/crates/ely_browser_core/tests/history.rs +++ b/crates/ely_browser_core/tests/history.rs @@ -45,6 +45,46 @@ fn paused_history_recording_skips_new_history_entries() -> Result<(), Box Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let default_profile_id = core.snapshot()?.active_profile_id; + + core.open_tab(UrlText::parse("https://example.com/work")?); + core.create_space("Research", "R", 0xf54e00)?; + core.open_tab(UrlText::parse("https://example.com/research")?); + + let personal_profile_id = core.create_profile("Personal", 0x26251e, ProfileKind::Standard)?; + core.open_tab(UrlText::parse("https://example.com/personal")?); + core.select_profile(&default_profile_id)?; + + let removed_count = core.clear_active_profile_history()?; + let default_snapshot = core.snapshot()?; + assert_eq!(removed_count, 2); + assert_eq!(default_snapshot.active_profile_history_entry_count, 0); + assert!(default_snapshot.history_entries.is_empty()); + + core.select_profile(&personal_profile_id)?; + let personal_snapshot = core.snapshot()?; + assert_eq!(personal_snapshot.active_profile_history_entry_count, 1); + assert_eq!(personal_snapshot.history_entries.len(), 1); + assert_eq!(personal_snapshot.history_entries[0].url().as_str(), "https://example.com/personal"); + Ok(()) +} + +#[test] +fn clear_active_profile_history_without_entries_is_empty_change() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + + let removed_count = core.clear_active_profile_history()?; + let snapshot = core.snapshot()?; + + assert_eq!(removed_count, 0); + assert_eq!(snapshot.active_profile_history_entry_count, 0); + assert!(snapshot.history_entries.is_empty()); + Ok(()) +} + #[test] fn history_scoped_search_opens_recent_matching_entry() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;