feat: refactor encryption panel (#13)
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m52s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled

Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
2026-02-28 11:25:02 +00:00
parent 2423cdca19
commit 3fecda175b
10 changed files with 374 additions and 437 deletions

View File

@@ -65,3 +65,7 @@ webbrowser.workspace = true
indexset = "0.12.3"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
[target.'cfg(target_os = "macos")'.dependencies]
# Temporary workaround https://github.com/zed-industries/zed/issues/47168
core-text = "=21.0.0"

View File

@@ -1,292 +0,0 @@
use anyhow::Error;
use device::DeviceRegistry;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
};
use nostr_sdk::prelude::*;
use person::{shorten_pubkey, PersonRegistry};
use state::Announcement;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::notification::Notification;
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
const MSG: &str =
"Encryption Key is a special key that used to encrypt and decrypt your messages. \
Your identity is completely decoupled from all encryption processes to protect your privacy.";
const NOTICE: &str = "By resetting your encryption key, you will lose access to \
all your encrypted messages before. This action cannot be undone.";
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<EncryptionPanel> {
cx.new(|cx| EncryptionPanel::new(public_key, window, cx))
}
#[derive(Debug)]
pub struct EncryptionPanel {
name: SharedString,
focus_handle: FocusHandle,
/// User's public key
public_key: PublicKey,
/// Whether the panel is loading
loading: bool,
/// Tasks
tasks: Vec<Task<Result<(), Error>>>,
}
impl EncryptionPanel {
fn new(public_key: PublicKey, _window: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
name: "Encryption".into(),
focus_handle: cx.focus_handle(),
public_key,
loading: false,
tasks: vec![],
}
}
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
self.loading = status;
cx.notify();
}
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
let device = DeviceRegistry::global(cx);
let task = device.read(cx).approve(event, cx);
let id = event.id;
// Update loading status
self.set_loading(true, cx);
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
match task.await {
Ok(_) => {
this.update_in(cx, |this, window, cx| {
// Reset loading status
this.set_loading(false, cx);
// Remove request
device.update(cx, |this, cx| {
this.remove_request(&id, cx);
});
window.push_notification("Approved", cx);
})?;
}
Err(e) => {
this.update_in(cx, |this, window, cx| {
this.set_loading(false, cx);
window.push_notification(Notification::error(e.to_string()), cx);
})?;
}
}
Ok(())
}));
}
fn render_requests(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
const TITLE: &str = "You've requested for the Encryption Key from:";
let device = DeviceRegistry::global(cx);
let requests = device.read(cx).requests.clone();
let mut items = Vec::new();
for event in requests.into_iter() {
let request = Announcement::from(&event);
let client_name = request.client_name();
let target = request.public_key();
items.push(
v_flex()
.gap_2()
.text_sm()
.child(SharedString::from(TITLE))
.child(
v_flex()
.h_12()
.items_center()
.justify_center()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground)
.child(client_name.clone()),
)
.child(
h_flex()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(SharedString::from(target.to_hex())),
)
.child(
h_flex().justify_end().gap_2().child(
Button::new("approve")
.label("Approve")
.ghost()
.small()
.disabled(self.loading)
.loading(self.loading)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.approve(&event, window, cx);
})),
),
),
)
}
items
}
}
impl Panel for EncryptionPanel {
fn panel_id(&self) -> SharedString {
self.name.clone()
}
fn title(&self, _cx: &App) -> AnyElement {
self.name.clone().into_any_element()
}
}
impl EventEmitter<PanelEvent> for EncryptionPanel {}
impl Focusable for EncryptionPanel {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for EncryptionPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let device = DeviceRegistry::global(cx);
let state = device.read(cx).state();
let has_requests = device.read(cx).has_requests();
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&self.public_key, cx);
let Some(announcement) = profile.announcement() else {
return div();
};
let pubkey = SharedString::from(shorten_pubkey(announcement.public_key(), 16));
let client_name = announcement.client_name();
v_flex()
.p_3()
.gap_3()
.w_full()
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from(MSG)),
)
.child(divider(cx))
.child(
v_flex()
.gap_3()
.text_sm()
.child(
v_flex()
.gap_1p5()
.child(
div()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Device Name:")),
)
.child(
h_flex()
.h_12()
.items_center()
.justify_center()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(client_name.clone()),
),
)
.child(
v_flex()
.gap_1p5()
.child(
div()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Encryption Public Key:")),
)
.child(
h_flex()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(pubkey),
),
),
)
.when(has_requests, |this| {
this.child(divider(cx)).child(
v_flex()
.gap_1p5()
.w_full()
.child(
div()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Requests:")),
)
.child(
v_flex()
.gap_2()
.flex_1()
.w_full()
.children(self.render_requests(cx)),
),
)
})
.child(divider(cx))
.when(state.requesting(), |this| {
this.child(
h_flex()
.h_8()
.justify_center()
.text_xs()
.text_center()
.text_color(cx.theme().text_accent)
.bg(cx.theme().elevated_surface_background)
.rounded(cx.theme().radius)
.child(SharedString::from(
"Please open other device and approve the request",
)),
)
})
.child(
v_flex()
.gap_1()
.child(
Button::new("reset")
.icon(IconName::Reset)
.label("Reset")
.warning()
.small()
.font_semibold(),
)
.child(
div()
.italic()
.text_size(px(10.))
.text_color(cx.theme().text_muted)
.child(SharedString::from(NOTICE)),
),
)
}
}

View File

@@ -1,7 +1,6 @@
pub mod backup;
pub mod connect;
pub mod contact_list;
pub mod encryption_key;
pub mod greeter;
pub mod import;
pub mod messaging_relays;

View File

@@ -2,6 +2,7 @@ use std::sync::Arc;
use ::settings::AppSettings;
use chat::{ChatEvent, ChatRegistry, InboxState};
use device::DeviceRegistry;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
@@ -19,14 +20,20 @@ use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification;
use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
use crate::dialogs::settings;
use crate::panels::{
backup, contact_list, encryption_key, greeter, messaging_relays, profile, relay_list,
};
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
use crate::sidebar;
const ENC_MSG: &str =
"Encryption Key is a special key that used to encrypt and decrypt your messages. \
Your identity is completely decoupled from all encryption processes to protect your privacy.";
const ENC_WARN: &str = "By resetting your encryption key, you will lose access to \
all your encrypted messages before. This action cannot be undone.";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
cx.new(|cx| Workspace::new(window, cx))
}
@@ -36,12 +43,13 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
enum Command {
ToggleTheme,
RefreshEncryption,
RefreshRelayList,
RefreshMessagingRelays,
ResetEncryption,
ShowRelayList,
ShowMessaging,
ShowEncryption,
ShowProfile,
ShowSettings,
ShowBackup,
@@ -238,21 +246,6 @@ impl Workspace {
);
});
}
Command::ShowEncryption => {
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
if let Some(public_key) = signer.public_key() {
self.dock.update(cx, |this, cx| {
this.add_panel(
Arc::new(encryption_key::init(public_key, window, cx)),
DockPlacement::Right,
window,
cx,
);
});
}
}
Command::ShowMessaging => {
self.dock.update(cx, |this, cx| {
this.add_panel(
@@ -273,12 +266,21 @@ impl Workspace {
);
});
}
Command::RefreshEncryption => {
let device = DeviceRegistry::global(cx);
device.update(cx, |this, cx| {
this.get_announcement(cx);
});
}
Command::RefreshRelayList => {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| {
this.ensure_relay_list(cx);
});
}
Command::ResetEncryption => {
self.confirm_reset_encryption(window, cx);
}
Command::RefreshMessagingRelays => {
let chat = ChatRegistry::global(cx);
chat.update(cx, |this, cx| {
@@ -291,6 +293,54 @@ impl Workspace {
}
}
fn confirm_reset_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, |this, _window, cx| {
this.confirm()
.show_close(true)
.title("Reset Encryption Keys")
.child(
v_flex()
.gap_1()
.text_sm()
.child(SharedString::from(ENC_MSG))
.child(
div()
.italic()
.text_color(cx.theme().warning_active)
.child(SharedString::from(ENC_WARN)),
),
)
.on_ok(move |_ev, window, cx| {
let device = DeviceRegistry::global(cx);
let task = device.read(cx).create_encryption(cx);
window
.spawn(cx, async move |cx| {
let result = task.await;
cx.update(|window, cx| match result {
Ok(keys) => {
device.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.listen_request(cx);
});
window.close_modal(cx);
}
Err(e) => {
window
.push_notification(Notification::error(e.to_string()), cx);
}
})
.ok();
})
.detach();
// false to keep modal open
false
})
});
}
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, move |this, _window, cx| {
let registry = ThemeRegistry::global(cx);
@@ -471,8 +521,39 @@ impl Workspace {
.tooltip("Decoupled encryption key")
.small()
.ghost()
.on_click(|_ev, window, cx| {
window.dispatch_action(Box::new(Command::ShowEncryption), cx);
.dropdown_menu(move |this, _window, cx| {
let device = DeviceRegistry::global(cx);
let state = device.read(cx).state();
this.min_w(px(260.))
.item(PopupMenuItem::element(move |_window, _cx| {
h_flex()
.px_1()
.w_full()
.gap_2()
.text_sm()
.child(
div()
.size_1p5()
.rounded_full()
.when(state.set(), |this| this.bg(gpui::green()))
.when(state.requesting(), |this| {
this.bg(gpui::yellow())
}),
)
.child(SharedString::from(state.to_string()))
}))
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshEncryption),
)
.menu_with_icon(
"Reset",
IconName::Warning,
Box::new(Command::ResetEncryption),
)
}),
)
.child(