diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 9a3a9f7..658eb05 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -520,10 +520,19 @@ impl ChatSpace { IngesterSignal::Auth(req) => { let relay_url = &req.url; let challenge = &req.challenge; + let auth_auth = AppSettings::get_auto_auth(cx); + let auth_relays = AppSettings::read_global(cx).auth_relays(); view.update(cx, |this, cx| { this.push_auth_request(challenge, relay_url, cx); - this.open_auth_request(challenge, relay_url, window, cx); + + if auth_auth && auth_relays.contains(relay_url) { + // Automatically authenticate if the relay is authenticated before + this.auth(challenge, relay_url, window, cx); + } else { + // Otherwise open the auth request popup + this.open_auth_request(challenge, relay_url, window, cx); + } }) .ok(); } @@ -783,6 +792,7 @@ impl ChatSpace { window: &mut Window, cx: &mut Context, ) { + let settings = AppSettings::global(cx); let challenge = challenge.to_string(); let url = url.to_owned(); let challenge_clone = challenge.clone(); @@ -841,8 +851,15 @@ impl ChatSpace { cx.update(|window, cx| { this.update(cx, |this, cx| { this.remove_auth_request(&challenge, cx); + + // Save the authenticated relay to automatically authenticate future requests + settings.update(cx, |this, cx| { + this.push_auth_relay(url.clone(), cx); + }); + // Clear the current notification window.clear_notification_by_id(SharedString::from(challenge), cx); + // Push a new notification after current cycle cx.defer_in(window, move |_, window, cx| { window @@ -1039,7 +1056,7 @@ impl ChatSpace { window.open_modal(cx, move |modal, _window, _cx| { modal .title(shared_t!("common.preferences")) - .width(px(480.)) + .width(px(580.)) .child(view.clone()) }); } @@ -1201,6 +1218,7 @@ impl ChatSpace { cx: &mut Context, ) -> impl IntoElement { let proxy = AppSettings::get_proxy_user_avatars(cx); + let is_auto_auth = AppSettings::read_global(cx).is_auto_auth(); let updating = AutoUpdater::read_global(cx).status.is_updating(); let updated = AutoUpdater::read_global(cx).status.is_updated(); let auth_requests = self.auth_requests.len(); @@ -1239,7 +1257,7 @@ impl ChatSpace { }), ) }) - .when(auth_requests > 0, |this| { + .when(auth_requests > 0 && !is_auto_auth, |this| { this.child( h_flex() .id("requests") diff --git a/crates/coop/src/views/preferences.rs b/crates/coop/src/views/preferences.rs index 2bb0504..e398080 100644 --- a/crates/coop/src/views/preferences.rs +++ b/crates/coop/src/views/preferences.rs @@ -4,22 +4,22 @@ use gpui::{ div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, }; -use i18n::t; +use i18n::{shared_t, t}; use nostr_sdk::prelude::*; use registry::Registry; use settings::AppSettings; use theme::ActiveTheme; use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; +use ui::button::{Button, ButtonRounded, ButtonVariants}; use ui::input::{InputState, TextInput}; use ui::modal::ModalButtonProps; use ui::switch::Switch; -use ui::{v_flex, ContextModal, IconName, Sizable, Size, StyledExt}; +use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, Size, StyledExt}; use crate::views::{edit_profile, setup_relay}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { - Preferences::new(window, cx) + cx.new(|cx| Preferences::new(window, cx)) } pub struct Preferences { @@ -27,17 +27,15 @@ pub struct Preferences { } impl Preferences { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - let media_server = AppSettings::get_media_server(cx).to_string(); - let media_input = cx.new(|cx| { - InputState::new(window, cx) - .default_value(media_server.clone()) - .placeholder(media_server) - }); + pub fn new(window: &mut Window, cx: &mut App) -> Self { + let media_server = AppSettings::get_media_server(cx).to_string(); + let media_input = cx.new(|cx| { + InputState::new(window, cx) + .default_value(media_server.clone()) + .placeholder(media_server) + }); - Self { media_input } - }) + Self { media_input } } fn open_edit_profile(&self, window: &mut Window, cx: &mut Context) { @@ -117,58 +115,50 @@ impl Render for Preferences { let input_state = self.media_input.downgrade(); let profile = Registry::read_global(cx).identity(cx); - let backup_messages = AppSettings::get_backup_messages(cx); + let auto_auth = AppSettings::get_auto_auth(cx); + let backup = AppSettings::get_backup_messages(cx); let screening = AppSettings::get_screening(cx); - let contact_bypass = AppSettings::get_contact_bypass(cx); - let proxy_avatar = AppSettings::get_proxy_user_avatars(cx); - let hide_avatar = AppSettings::get_hide_user_avatars(cx); + let bypass = AppSettings::get_contact_bypass(cx); + let proxy = AppSettings::get_proxy_user_avatars(cx); + let hide = AppSettings::get_hide_user_avatars(cx); v_flex() .child( v_flex() - .py_2() + .pb_2() .gap_2() .child( div() .text_sm() .text_color(cx.theme().text_placeholder) .font_semibold() - .child(SharedString::new(t!("preferences.account_header"))), + .child(shared_t!("preferences.account_header")), ) .child( - div() + h_flex() .w_full() - .flex() .justify_between() - .items_center() .child( - div() - .id("current-user") - .flex() - .items_center() + h_flex() + .id("user") .gap_2() - .child( - Avatar::new(profile.avatar_url(proxy_avatar)) - .size(rems(2.4)), - ) + .child(Avatar::new(profile.avatar_url(proxy)).size(rems(2.4))) .child( div() .flex_1() .text_sm() .child( div() - .line_height(relative(1.3)) .font_semibold() + .line_height(relative(1.3)) .child(profile.display_name()), ) .child( div() - .line_height(relative(1.3)) .text_xs() .text_color(cx.theme().text_muted) - .child(SharedString::new(t!( - "preferences.see_your_profile" - ))), + .line_height(relative(1.3)) + .child(shared_t!("preferences.account_btn")), ), ) .on_click(cx.listener(move |this, _e, window, cx| { @@ -178,8 +168,9 @@ impl Render for Preferences { .child( Button::new("relays") .label("Messaging Relays") - .ghost() - .small() + .xsmall() + .ghost_alt() + .rounded(ButtonRounded::Full) .on_click(cx.listener(move |this, _e, window, cx| { this.open_relays(window, cx); })), @@ -196,39 +187,48 @@ impl Render for Preferences { .text_sm() .text_color(cx.theme().text_placeholder) .font_semibold() - .child(SharedString::new(t!("preferences.media_server_header"))), + .child(shared_t!("preferences.relay_and_media")), ) .child( - div() + v_flex() .my_1() - .flex() - .items_start() .gap_1() - .child(TextInput::new(&self.media_input).xsmall()) .child( - Button::new("update") - .icon(IconName::CheckCircleFill) - .ghost() - .with_size(Size::Size(px(26.))) - .on_click(move |_, window, cx| { - if let Some(input) = input_state.upgrade() { - let Ok(url) = Url::parse(input.read(cx).value()) else { - window.push_notification( - t!("preferences.url_not_valid"), - cx, - ); - return; - }; - AppSettings::update_media_server(url, cx); - } - }), + h_flex() + .gap_1() + .child(TextInput::new(&self.media_input).xsmall()) + .child( + Button::new("update") + .icon(IconName::Check) + .ghost() + .with_size(Size::Size(px(26.))) + .on_click(move |_, _window, cx| { + if let Some(input) = input_state.upgrade() { + let Ok(url) = + Url::parse(input.read(cx).value()) + else { + return; + }; + AppSettings::update_media_server(url, cx); + } + }), + ), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(shared_t!("preferences.media_description")), ), ) .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::new(t!("preferences.media_description"))), + Switch::new("auth") + .label(t!("preferences.auto_auth")) + .description(t!("preferences.auto_auth_description")) + .checked(auto_auth) + .on_click(move |_, _window, cx| { + AppSettings::update_auto_auth(!auto_auth, cx); + }), ), ) .child( @@ -242,7 +242,7 @@ impl Render for Preferences { .text_sm() .text_color(cx.theme().text_placeholder) .font_semibold() - .child(SharedString::new(t!("preferences.messages_header"))), + .child(shared_t!("preferences.messages_header")), ) .child( v_flex() @@ -260,18 +260,18 @@ impl Render for Preferences { Switch::new("bypass") .label(t!("preferences.bypass_label")) .description(t!("preferences.bypass_description")) - .checked(contact_bypass) + .checked(bypass) .on_click(move |_, _window, cx| { - AppSettings::update_contact_bypass(!contact_bypass, cx); + AppSettings::update_contact_bypass(!bypass, cx); }), ) .child( - Switch::new("backup_messages") - .label(t!("preferences.backup_messages_label")) + Switch::new("backup") + .label(t!("preferences.backup_label")) .description(t!("preferences.backup_description")) - .checked(backup_messages) + .checked(backup) .on_click(move |_, _window, cx| { - AppSettings::update_backup_messages(!backup_messages, cx); + AppSettings::update_backup_messages(!backup, cx); }), ), ), @@ -287,27 +287,27 @@ impl Render for Preferences { .text_sm() .text_color(cx.theme().text_placeholder) .font_semibold() - .child(SharedString::new(t!("preferences.display_header"))), + .child(shared_t!("preferences.display_header")), ) .child( v_flex() .gap_2() .child( - Switch::new("hide_user_avatars") + Switch::new("hide_avatar") .label(t!("preferences.hide_avatars_label")) .description(t!("preferences.hide_avatar_description")) - .checked(hide_avatar) + .checked(hide) .on_click(move |_, _window, cx| { - AppSettings::update_hide_user_avatars(!hide_avatar, cx); + AppSettings::update_hide_user_avatars(!hide, cx); }), ) .child( - Switch::new("proxy_user_avatars") + Switch::new("proxy_avatar") .label(t!("preferences.proxy_avatars_label")) .description(t!("preferences.proxy_description")) - .checked(proxy_avatar) + .checked(proxy) .on_click(move |_, _window, cx| { - AppSettings::update_proxy_user_avatars(!proxy_avatar, cx); + AppSettings::update_proxy_user_avatars(!proxy, cx); }), ), ), diff --git a/crates/global/src/constants.rs b/crates/global/src/constants.rs index 0c7a43a..5755010 100644 --- a/crates/global/src/constants.rs +++ b/crates/global/src/constants.rs @@ -5,7 +5,7 @@ pub const APP_UPDATER_ENDPOINT: &str = "https://coop-updater.reya.su/"; pub const KEYRING_URL: &str = "Coop Safe Storage"; pub const ACCOUNT_IDENTIFIER: &str = "coop:user"; -pub const SETTINGS_D: &str = "coop:settings"; +pub const SETTINGS_IDENTIFIER: &str = "coop:settings"; /// Bootstrap Relays. pub const BOOTSTRAP_RELAYS: [&str; 5] = [ diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 7d86798..7379ed6 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,5 +1,5 @@ use anyhow::anyhow; -use global::constants::SETTINGS_D; +use global::constants::SETTINGS_IDENTIFIER; use global::nostr_client; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_sdk::prelude::*; @@ -11,7 +11,7 @@ pub fn init(cx: &mut App) { // Observe for state changes and save settings to database state.update(cx, |this, cx| { - this.subscriptions + this._subscriptions .push(cx.observe(&state, |this, _state, cx| { this.set_settings(cx); })); @@ -49,6 +49,7 @@ setting_accessors! { pub screening: bool, pub contact_bypass: bool, pub auto_login: bool, + pub auto_auth: bool, } #[derive(Serialize, Deserialize)] @@ -60,6 +61,8 @@ pub struct Settings { pub screening: bool, pub contact_bypass: bool, pub auto_login: bool, + pub auto_auth: bool, + pub authenticated_relays: Vec, } impl Default for Settings { @@ -72,6 +75,8 @@ impl Default for Settings { screening: true, contact_bypass: true, auto_login: false, + auto_auth: true, + authenticated_relays: vec![], } } } @@ -88,8 +93,8 @@ impl Global for GlobalAppSettings {} pub struct AppSettings { setting_values: Settings, - #[allow(dead_code)] - subscriptions: SmallVec<[Subscription; 1]>, + _subscriptions: SmallVec<[Subscription; 1]>, + _tasks: SmallVec<[Task<()>; 1]>, } impl AppSettings { @@ -110,54 +115,53 @@ impl AppSettings { fn new(cx: &mut Context) -> Self { let setting_values = Settings::default(); - let mut subscriptions = smallvec![]; + let mut tasks = smallvec![]; - subscriptions.push(cx.observe_new::(move |this, _window, cx| { - this.get_settings_from_db(cx); - })); - - Self { - setting_values, - subscriptions, - } - } - - pub(crate) fn get_settings_from_db(&self, cx: &mut Context) { let task: Task> = cx.background_spawn(async move { + let client = nostr_client(); let filter = Filter::new() .kind(Kind::ApplicationSpecificData) - .identifier(SETTINGS_D) + .identifier(SETTINGS_IDENTIFIER) .limit(1); - if let Some(event) = nostr_client().database().query(filter).await?.first_owned() { - log::info!("Successfully loaded settings from database"); + if let Some(event) = client.database().query(filter).await?.first_owned() { Ok(serde_json::from_str(&event.content).unwrap_or(Settings::default())) } else { Err(anyhow!("Not found")) } }); - cx.spawn(async move |this, cx| { - if let Ok(settings) = task.await { - this.update(cx, |this, cx| { - this.setting_values = settings; - cx.notify(); - }) - .ok(); - } - }) - .detach(); + tasks.push( + // Load settings from database + cx.spawn(async move |this, cx| { + if let Ok(settings) = task.await { + this.update(cx, |this, cx| { + this.setting_values = settings; + cx.notify(); + }) + .ok(); + } + }), + ); + + Self { + setting_values, + _subscriptions: smallvec![], + _tasks: tasks, + } } pub(crate) fn set_settings(&self, cx: &mut Context) { if let Ok(content) = serde_json::to_string(&self.setting_values) { cx.background_spawn(async move { - if let Ok(event) = EventBuilder::new(Kind::ApplicationSpecificData, content) - .tags(vec![Tag::identifier(SETTINGS_D)]) + let client = nostr_client(); + let builder = EventBuilder::new(Kind::ApplicationSpecificData, content) + .tags(vec![Tag::identifier(SETTINGS_IDENTIFIER)]) .sign(&Keys::generate()) - .await - { - if let Err(e) = nostr_client().database().save_event(&event).await { + .await; + + if let Ok(event) = builder { + if let Err(e) = client.database().save_event(&event).await { log::error!("Failed to save user settings: {e}"); } else { log::info!("New settings have been saved successfully"); @@ -167,4 +171,17 @@ impl AppSettings { .detach(); } } + + pub fn is_auto_auth(&self) -> bool { + !self.setting_values.authenticated_relays.is_empty() && self.setting_values.auto_auth + } + + pub fn auth_relays(&self) -> Vec { + self.setting_values.authenticated_relays.clone() + } + + pub fn push_auth_relay(&mut self, relay_url: RelayUrl, cx: &mut Context) { + self.setting_values.authenticated_relays.push(relay_url); + cx.notify(); + } } diff --git a/locales/app.yml b/locales/app.yml index 6fc1f22..dadf71f 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -257,40 +257,42 @@ profile: en: "No bio." preferences: - media_description: - en: "Coop currently only supports NIP-96 media servers." - backup_description: - en: "When you send a message, Coop will also forward it to your configured Messaging Relays. Disabling this will cause all messages sent during the current session to disappear when the app is closed." - screening_description: - en: "When opening a chat request, Coop will show a popup to help you verify the sender." - bypass_description: - en: "Requests from your contacts will automatically go to inbox." - hide_avatar_description: - en: "Unload all avatar pictures to improve performance and reduce memory usage." - proxy_description: - en: "Use wsrv.nl to resize and downscale avatar pictures (saves ~50MB of data)." account_header: en: "Account" - see_your_profile: + account_btn: en: "See your profile" - media_server_header: - en: "Media Server" - url_not_valid: - en: "URL is not valid" - messages_header: - en: "Messages" - backup_messages_label: + relay_and_media: + en: "Relay and Media" + media_description: + en: "Coop currently only supports NIP-96 media servers." + auto_auth: + en: "Automatically authenticate for known relays" + auto_auth_description: + en: "After you approve the authentication request, Coop will automatically complete this step next time." + backup_label: en: "Backup messages" + backup_description: + en: "When you send a message, Coop will also forward it to your configured Messaging Relays. Disabling this will cause all messages sent during the current session to disappear when the app is closed." screening_label: en: "Screening" + screening_description: + en: "When opening a chat request, Coop will show a popup to help you verify the sender." bypass_label: en: "Skip screening for contacts" - display_header: - en: "Display" + bypass_description: + en: "Requests from your contacts will automatically go to inbox." hide_avatars_label: en: "Hide user avatars" + hide_avatar_description: + en: "Unload all avatar pictures to improve performance and reduce memory usage." proxy_avatars_label: en: "Proxy user avatars" + proxy_description: + en: "Use wsrv.nl to resize and downscale avatar pictures (saves ~50MB of data)." + messages_header: + en: "Messages" + display_header: + en: "Display" compose: placeholder_npub: