nip4e: catch up psstpsst (#70)

Reviewed-on: #70
This commit was merged in pull request #70.
This commit is contained in:
2026-11-04 03:12:52 +00:00
parent 690dc37fc5
commit 74d548e5c6
12 changed files with 888 additions and 185 deletions
+20 -2
View File
@@ -22,7 +22,7 @@ use gpui_kit::component::progress::Progress;
use gpui_kit::component::separator::Separator;
use gpui_kit::component::switch::Switch;
use gpui_kit::component::{Disableable, Sizable, WindowExt, h_flex, v_flex};
use settings::{AppSettings, AppearanceMode};
use settings::{AppSettings, AppearanceMode, NIP4E_ROTATION_DEFAULT_DAYS};
#[cfg(not(target_arch = "wasm32"))]
use speech::model::{self as speech_model};
use ui::{settings_block, settings_row};
@@ -337,6 +337,7 @@ impl Render for Preferences {
"Corner radius for large elements like dialogs. Empty follows the OS theme.";
const FOCUS_RING: &str = "Draw a ring around focused controls.";
const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
const NIP4E_ROTATION: &str = "Automatically rotate the encryption key every 30 days, keeping the previous key for messages still sealed to it.";
const BACKGROUND: &str = "Show an image behind chat and group panels.";
const MEDIA: &str = "Server for media attachments. Only blossom services are supported.";
@@ -344,6 +345,7 @@ impl Render for Preferences {
let wot_auto_trust = AppSettings::get_wot_auto_trust(cx);
let hide_avatar = AppSettings::get_hide_avatar(cx);
let nip4e = AppSettings::get_nip4e(cx);
let nip4e_rotation_days = AppSettings::get_nip4e_rotation_days(cx);
let appearance_mode = AppSettings::get_appearance_mode(cx);
let theme = AppSettings::get_theme(cx);
let panel_background = AppSettings::get_panel_background(cx);
@@ -355,7 +357,7 @@ impl Render for Preferences {
};
v_flex()
.mt_2()
.my_2()
.gap_4()
.w_full()
.child(
@@ -507,6 +509,22 @@ impl Render for Preferences {
}),
cx,
))
.child(settings_row(
"Rotate Encryption Key",
NIP4E_ROTATION,
Switch::new("nip4e_rotation")
.checked(nip4e_rotation_days.is_some())
.on_click(move |_, _window, cx| {
let enabled = AppSettings::get_nip4e_rotation_days(cx).is_some();
let days = if enabled {
None
} else {
Some(NIP4E_ROTATION_DEFAULT_DAYS)
};
AppSettings::update_nip4e_rotation_days(days, cx);
}),
cx,
))
.child(Separator::horizontal())
.child(self.render_speech(cx))
.child(self.render_dictation_shortcut(window, cx))
+102 -3
View File
@@ -1,18 +1,25 @@
#[cfg(not(target_arch = "wasm32"))]
use auto_update::AutoUpdater;
use gpui::prelude::FluentBuilder;
use gpui::{
Anchor, App, Context, Div, InteractiveElement, ParentElement, SharedString, Stateful, Styled,
Window, div, px,
};
#[cfg(not(target_arch = "wasm32"))]
use gpui::{AnyElement, Entity, IntoElement};
use gpui::{
App, Context, Div, InteractiveElement, ParentElement, Stateful, Styled, Window, div, px,
};
use gpui_kit::base::StyledExt as _;
#[cfg(not(target_arch = "wasm32"))]
use gpui_kit::component::Disableable;
use gpui_kit::component::button::{Button, ButtonVariant, ButtonVariants};
use gpui_kit::component::clipboard::Clipboard;
use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
use gpui_kit::component::popover::Popover;
use gpui_kit::component::separator::Separator;
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex, v_flex};
use nostr_sdk::prelude::*;
use person::shorten_pubkey;
use settings::AppSettings;
use state::{CLIENT_NAME, NostrRegistry};
use ui::{Avatar, IconName};
@@ -31,6 +38,10 @@ impl Sidebar {
let updater = AutoUpdater::try_global(cx);
let profile = self.directory.read(cx).person(current_user, cx);
let avatar = profile.avatar();
let encryption_key = profile
.announcement()
.map(|announcement| announcement.public_key());
let encryption_enabled = AppSettings::get_nip4e(cx);
h_flex()
.id("sidebar-user")
@@ -71,6 +82,11 @@ impl Sidebar {
});
actions
.when(encryption_enabled, |actions| {
actions.when_some(encryption_key, |actions, encryption_key| {
actions.child(self.render_encryption(encryption_key))
})
})
.child(
Button::new("sidebar-search")
.icon(IconName::Search)
@@ -111,6 +127,89 @@ impl Sidebar {
})
}
/// The encryption key popover: public key with copy, backup and reset
fn render_encryption(&self, encryption_key: PublicKey) -> impl IntoElement {
Popover::new("sidebar-encryption")
.anchor(Anchor::TopRight)
.trigger(
Button::new("encryption-key")
.icon(IconName::Lock)
.tooltip("Encryption Key")
.ghost()
.small(),
)
.content(move |_, _, cx| {
let popover = cx.entity();
let Ok(npub) = encryption_key.to_bech32();
v_flex()
.w_64()
.gap_2()
.text_sm()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child("Public key"),
)
.child(
h_flex()
.items_center()
.gap_1()
.child(
div()
.flex_1()
.min_w_0()
.truncate()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(shorten_pubkey(encryption_key, 12))),
)
.child(
Clipboard::new("encryption-key-copy")
.tooltip("Copy public key")
.value(npub),
),
)
.child(Separator::horizontal())
.child(
h_flex()
.justify_end()
.gap_2()
.child(
Button::new("encryption-backup")
.label("Backup")
.outline()
.small()
.on_click({
let popover = popover.clone();
move |_, window, cx| {
popover
.update(cx, |state, cx| state.dismiss(window, cx));
window.dispatch_action(
Box::new(Command::BackupEncryption),
cx,
);
}
}),
)
.child(
Button::new("encryption-reset")
.label("Reset")
.ghost()
.small()
.on_click(move |_, window, cx| {
popover.update(cx, |state, cx| state.dismiss(window, cx));
window.dispatch_action(
Box::new(Command::ResetEncryption),
cx,
);
}),
),
)
})
}
pub(super) fn render_footer(&self, loading: bool, _cx: &mut Context<Self>) -> Div {
v_flex()
.p_2()
+9 -1
View File
@@ -21,6 +21,7 @@ use nostr_sdk::prelude::*;
use person::{Directory, PersonRegistry};
pub(crate) use rows::open_room;
use search::SearchResult;
use settings::AppSettings;
use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry, StateEvent};
use ui::title_bar_drag_handlers;
@@ -104,6 +105,8 @@ impl Sidebar {
let chat = ChatRegistry::global(cx);
let groups = GroupsRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let settings = AppSettings::global(cx);
let directory = Directory::new(cx);
let find_input = cx.new(|cx| {
InputState::new(window, cx)
@@ -206,6 +209,11 @@ impl Sidebar {
},
));
// The user header reads settings and the tracked profiles on render, so
// the encryption button reacts to toggles and announcement arrivals
subscriptions.push(cx.observe(&settings, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&directory, |_, _, cx| cx.notify()));
tasks.push(cx.spawn(async move |this, cx| {
let timer = Duration::from_millis(PROGRESS_TICK_MS);
loop {
@@ -229,7 +237,7 @@ impl Sidebar {
chats_open: None,
contacts_open: false,
contacts: None,
directory: Directory::new(cx),
directory,
selected_contacts: HashSet::new(),
requests_open: false,
new_requests: false,