feat: add setting for relay authentication (#133)

* remember auth relay

* .

* .
This commit is contained in:
reya
2025-08-31 18:06:04 +07:00
committed by GitHub
parent 807851518a
commit f2be8fca08
5 changed files with 174 additions and 137 deletions

View File

@@ -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<Self>,
) {
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<Self>,
) -> 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")

View File

@@ -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> {
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<Self> {
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<Self>) {
@@ -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);
}),
),
),

View File

@@ -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] = [

View File

@@ -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<RelayUrl>,
}
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>) -> Self {
let setting_values = Settings::default();
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
subscriptions.push(cx.observe_new::<Self>(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<Self>) {
let task: Task<Result<Settings, anyhow::Error>> = 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<Self>) {
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<RelayUrl> {
self.setting_values.authenticated_relays.clone()
}
pub fn push_auth_relay(&mut self, relay_url: RelayUrl, cx: &mut Context<Self>) {
self.setting_values.authenticated_relays.push(relay_url);
cx.notify();
}
}

View File

@@ -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: