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

103
Cargo.lock generated
View File

@@ -772,6 +772,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.2"
@@ -1189,7 +1198,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"indexmap",
"rustc-hash 2.1.1",
@@ -1318,6 +1327,7 @@ dependencies = [
"chat",
"chat_ui",
"common",
"core-text",
"device",
"futures",
"gpui",
@@ -1400,19 +1410,6 @@ dependencies = [
"libc",
]
[[package]]
name = "core-graphics"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
dependencies = [
"bitflags 2.11.0",
"core-foundation 0.10.0",
"core-graphics-types 0.2.0",
"foreign-types",
"libc",
]
[[package]]
name = "core-graphics-helmer-fork"
version = "0.24.0"
@@ -1463,13 +1460,14 @@ dependencies = [
[[package]]
name = "core-text"
version = "21.1.0"
version = "21.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fce32d657e17d6e4a8e70fe2ae6875218015f320620a78e5949d228bc76622bd"
checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130"
dependencies = [
"core-foundation 0.10.0",
"core-graphics 0.25.0",
"core-graphics 0.24.0",
"foreign-types",
"libc",
]
[[package]]
@@ -1646,7 +1644,7 @@ dependencies = [
[[package]]
name = "derive_refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"proc-macro2",
"quote",
@@ -1670,6 +1668,8 @@ dependencies = [
"smallvec",
"smol",
"state",
"theme",
"ui",
]
[[package]]
@@ -1730,6 +1730,18 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.11.0",
"block2",
"libc",
"objc2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -2587,7 +2599,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -2666,7 +2678,7 @@ dependencies = [
[[package]]
name = "gpui_linux"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"as-raw-xcb-connection",
@@ -2714,11 +2726,10 @@ dependencies = [
[[package]]
name = "gpui_macos"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"async-task",
"bindgen",
"block",
"cbindgen",
"cocoa 0.26.0",
@@ -2730,6 +2741,7 @@ dependencies = [
"core-video",
"ctor",
"derive_more",
"dispatch2",
"etagere",
"foreign-types",
"futures",
@@ -2750,12 +2762,13 @@ dependencies = [
"strum",
"util",
"uuid",
"zed-font-kit",
]
[[package]]
name = "gpui_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2766,7 +2779,7 @@ dependencies = [
[[package]]
name = "gpui_platform"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"console_error_panic_hook",
"gpui",
@@ -2779,7 +2792,7 @@ dependencies = [
[[package]]
name = "gpui_tokio"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"gpui",
@@ -2790,7 +2803,7 @@ dependencies = [
[[package]]
name = "gpui_util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"log",
@@ -2799,7 +2812,7 @@ dependencies = [
[[package]]
name = "gpui_web"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"console_error_panic_hook",
@@ -2822,7 +2835,7 @@ dependencies = [
[[package]]
name = "gpui_wgpu"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"bytemuck",
@@ -2850,7 +2863,7 @@ dependencies = [
[[package]]
name = "gpui_windows"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"collections",
@@ -3094,7 +3107,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"async-compression",
@@ -3119,7 +3132,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"rustls",
"rustls-platform-verifier",
@@ -3880,7 +3893,7 @@ dependencies = [
[[package]]
name = "media"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"bindgen",
@@ -4624,7 +4637,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "perf"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"collections",
"serde",
@@ -4742,9 +4755,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand 2.3.0",
@@ -5305,7 +5318,7 @@ dependencies = [
[[package]]
name = "refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"derive_refineable",
]
@@ -5404,7 +5417,7 @@ dependencies = [
[[package]]
name = "reqwest_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"bytes",
@@ -5459,7 +5472,7 @@ dependencies = [
[[package]]
name = "rope"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"arrayvec",
"log",
@@ -5721,7 +5734,7 @@ dependencies = [
[[package]]
name = "scheduler"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"async-task",
"backtrace",
@@ -6315,7 +6328,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"arrayvec",
"log",
@@ -7258,7 +7271,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"async-fs",
@@ -7297,7 +7310,7 @@ dependencies = [
[[package]]
name = "util_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"perf",
"quote",
@@ -9100,7 +9113,7 @@ dependencies = [
[[package]]
name = "zlog"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"anyhow",
"chrono",
@@ -9117,7 +9130,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "ztracing"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
dependencies = [
"tracing",
"tracing-subscriber",
@@ -9128,7 +9141,7 @@ dependencies = [
[[package]]
name = "ztracing_macro"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1123140e40f47ad7b12815c16de0d49e42e36617"
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
[[package]]
name = "zune-core"

View File

@@ -9,17 +9,14 @@ edition = "2021"
publish = false
[workspace.dependencies]
# GPUI
gpui = { git = "https://github.com/zed-industries/zed" }
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["screen-capture", "x11", "wayland", "runtime_shaders"] }
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "screen-capture", "x11", "wayland", "runtime_shaders"] }
gpui_linux = { git = "https://github.com/zed-industries/zed" }
gpui_windows = { git = "https://github.com/zed-industries/zed" }
gpui_macos = { git = "https://github.com/zed-industries/zed" }
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" }
# TODO: remove after fixed, issue: https://github.com/zed-industries/zed/issues/47168
core-text = "=21.0.0"
# Nostr
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }

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(

View File

@@ -8,6 +8,8 @@ publish.workspace = true
common = { path = "../common" }
state = { path = "../state" }
person = { path = "../person" }
ui = { path = "../ui" }
theme = { path = "../theme" }
gpui.workspace = true
nostr-sdk.workspace = true

View File

@@ -1,16 +1,28 @@
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
use gpui::{
div, App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString,
Styled, Subscription, Task, Window,
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::{
app_name, Announcement, DeviceState, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT,
};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::notification::Notification;
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, WindowExtension};
const IDENTIFIER: &str = "coop:device";
const MSG: &str = "You've requested an encryption key from another device. \
Approve to allow Coop to share with it.";
pub fn init(window: &mut Window, cx: &mut App) {
DeviceRegistry::set_global(cx.new(|cx| DeviceRegistry::new(window, cx)), cx);
@@ -25,9 +37,6 @@ impl Global for GlobalDeviceRegistry {}
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
#[derive(Debug)]
pub struct DeviceRegistry {
/// Request for encryption key from other devices
pub requests: Vec<Event>,
/// Device state
state: DeviceState,
@@ -64,19 +73,18 @@ impl DeviceRegistry {
);
// Run at the end of current cycle
cx.defer_in(window, |this, _window, cx| {
this.handle_notifications(cx);
cx.defer_in(window, |this, window, cx| {
this.handle_notifications(window, cx);
});
Self {
requests: vec![],
state: DeviceState::default(),
tasks: vec![],
_subscriptions: subscriptions,
}
}
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let (tx, rx) = flume::bounded::<Event>(100);
@@ -117,17 +125,19 @@ impl DeviceRegistry {
self.tasks.push(
// Update GPUI states
cx.spawn(async move |this, cx| {
cx.spawn_in(window, async move |this, cx| {
while let Ok(event) = rx.recv_async().await {
match event.kind {
// New request event
Kind::Custom(4454) => {
this.update(cx, |this, cx| {
this.add_request(event, cx);
this.update_in(cx, |this, window, cx| {
this.ask_for_approval(event, window, cx);
})?;
}
// New response event
Kind::Custom(4455) => {
this.update(cx, |this, cx| {
this.parse_response(event, cx);
this.extract_encryption(event, cx);
})?;
}
_ => {}
@@ -151,7 +161,7 @@ impl DeviceRegistry {
}
/// Set the decoupled encryption key for the current user
fn set_signer<S>(&mut self, new: S, cx: &mut Context<Self>)
pub fn set_signer<S>(&mut self, new: S, cx: &mut Context<Self>)
where
S: NostrSigner + 'static,
{
@@ -174,27 +184,9 @@ impl DeviceRegistry {
/// Reset the device state
fn reset(&mut self, cx: &mut Context<Self>) {
self.state = DeviceState::Idle;
self.requests.clear();
cx.notify();
}
/// Add a request for device keys
fn add_request(&mut self, request: Event, cx: &mut Context<Self>) {
self.requests.push(request);
cx.notify();
}
/// Remove a request for device keys
pub fn remove_request(&mut self, id: &EventId, cx: &mut Context<Self>) {
self.requests.retain(|r| r.id != *id);
cx.notify();
}
/// Check if there are any pending requests
pub fn has_requests(&self) -> bool {
!self.requests.is_empty()
}
/// Get all messages for encryption keys
fn get_messages(&mut self, cx: &mut Context<Self>) {
let task = self.subscribe_to_giftwrap_events(cx);
@@ -242,7 +234,7 @@ impl DeviceRegistry {
}
/// Get device announcement for current user
fn get_announcement(&mut self, cx: &mut Context<Self>) {
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
@@ -307,8 +299,8 @@ impl DeviceRegistry {
}));
}
/// Create a new device signer and announce it
fn announce(&mut self, cx: &mut Context<Self>) {
/// Create new encryption keys
pub fn create_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
@@ -323,7 +315,7 @@ impl DeviceRegistry {
let secret = keys.secret_key().to_secret_hex();
let n = keys.public_key();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
cx.background_spawn(async move {
let urls = write_relays.await;
// Construct an announcement event
@@ -340,23 +332,29 @@ impl DeviceRegistry {
// Save device keys to the database
set_keys(&client, &secret).await?;
Ok(())
});
Ok(keys)
})
}
/// Create a new device signer and announce it
fn announce(&mut self, cx: &mut Context<Self>) {
let task = self.create_encryption(cx);
self.tasks.push(cx.spawn(async move |this, cx| {
if task.await.is_ok() {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.listen_request(cx);
})?;
}
let keys = task.await?;
// Update signer
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.listen_request(cx);
})?;
Ok(())
}));
}
/// Initialize device signer (decoupled encryption key) for the current user
fn new_signer(&mut self, event: &Event, cx: &mut Context<Self>) {
pub fn new_signer(&mut self, event: &Event, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
@@ -375,31 +373,29 @@ impl DeviceRegistry {
}
});
cx.spawn(async move |this, cx| {
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.listen_request(cx);
})
.ok();
})?;
}
Err(e) => {
log::warn!("Failed to initialize device signer: {e}");
this.update(cx, |this, cx| {
this.request(cx);
this.listen_approval(cx);
})
.ok();
log::warn!("Failed to initialize device signer: {e}");
})?;
}
};
})
.detach();
Ok(())
}));
}
/// Listen for device key requests on user's write relays
fn listen_request(&mut self, cx: &mut Context<Self>) {
pub fn listen_request(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
@@ -518,30 +514,29 @@ impl DeviceRegistry {
}
});
cx.spawn(async move |this, cx| {
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(Some(keys)) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
})
.ok();
})?;
}
Ok(None) => {
this.update(cx, |this, cx| {
this.set_state(DeviceState::Requesting, cx);
})
.ok();
})?;
}
Err(e) => {
log::error!("Failed to request the encryption key: {e}");
}
};
})
.detach();
Ok(())
}));
}
/// Parse the response event for device keys from other devices
fn parse_response(&mut self, event: Event, cx: &mut Context<Self>) {
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let app_keys = nostr.read(cx).app_keys().clone();
@@ -575,7 +570,7 @@ impl DeviceRegistry {
}
/// Approve requests for device keys from other devices
pub fn approve(&self, event: &Event, cx: &App) -> Task<Result<(), Error>> {
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
@@ -586,8 +581,9 @@ impl DeviceRegistry {
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let event = event.clone();
let id: SharedString = event.id.to_hex().into();
cx.background_spawn(async move {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Get device keys
@@ -609,18 +605,145 @@ impl DeviceRegistry {
//
// P tag: the current device's public key
// p tag: the requester's public key
let event = client
.sign_event_builder(EventBuilder::new(Kind::Custom(4455), payload).tags(vec![
Tag::custom(TagKind::custom("P"), vec![keys.public_key()]),
Tag::public_key(target),
]))
.await?;
let builder = EventBuilder::new(Kind::Custom(4455), payload).tags(vec![
Tag::custom(TagKind::custom("P"), vec![keys.public_key()]),
Tag::public_key(target),
]);
// Sign the builder
let event = client.sign_event_builder(builder).await?;
// Send the response event to the user's relay list
client.send_event(&event).to(urls).await?;
Ok(())
});
cx.spawn_in(window, async move |_this, cx| {
match task.await {
Ok(_) => {
cx.update(|window, cx| {
window.clear_notification(id, cx);
})
.ok();
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
})
.ok();
}
};
})
.detach();
}
/// Handle encryption request
fn ask_for_approval(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
let notification = self.notification(event, cx);
cx.spawn_in(window, async move |_this, cx| {
cx.update(|window, cx| {
window.push_notification(notification, cx);
})
.ok();
})
.detach();
}
/// Build a notification for the encryption request.
fn notification(&self, event: Event, cx: &Context<Self>) -> Notification {
let request = Announcement::from(&event);
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&request.public_key(), cx);
let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false));
Notification::new()
.custom_id(SharedString::from(event.id.to_hex()))
.autohide(false)
.icon(IconName::UserKey)
.title(SharedString::from("New request"))
.content(move |_window, cx| {
v_flex()
.gap_2()
.text_sm()
.child(SharedString::from(MSG))
.child(
v_flex()
.gap_2()
.child(
v_flex()
.gap_1()
.text_sm()
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Requester:")),
)
.child(
div()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(
h_flex()
.gap_2()
.child(Avatar::new(profile.avatar()).xsmall())
.child(profile.name()),
),
),
)
.child(
v_flex()
.gap_1()
.text_sm()
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Client:")),
)
.child(
div()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(request.client_name()),
),
),
)
.into_any_element()
})
.action(move |_window, _cx| {
let view = entity.clone();
let event = event.clone();
Button::new("approve")
.label("Approve")
.small()
.primary()
.loading(loading.get())
.disabled(loading.get())
.on_click({
let loading = Rc::clone(&loading);
move |_ev, window, cx| {
// Set loading state to true
loading.set(true);
// Process to approve the request
view.update(cx, |this, cx| {
this.approve(&event, window, cx);
})
.ok();
}
})
})
}
}

View File

@@ -343,8 +343,8 @@ impl RelayAuth {
.px_1p5()
.rounded_sm()
.text_xs()
.bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground)
.bg(cx.theme().elevated_surface_background)
.text_color(cx.theme().text_accent)
.child(url.clone()),
)
.into_any_element()
@@ -361,11 +361,9 @@ impl RelayAuth {
.disabled(loading.get())
.on_click({
let loading = Rc::clone(&loading);
move |_ev, window, cx| {
// Set loading state to true
loading.set(true);
// Process to approve the request
view.update(cx, |this, cx| {
this.response(&req, window, cx);

View File

@@ -1,3 +1,5 @@
use std::fmt::Display;
use gpui::SharedString;
use nostr_sdk::prelude::*;
@@ -9,6 +11,16 @@ pub enum DeviceState {
Set,
}
impl Display for DeviceState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeviceState::Idle => write!(f, "Idle"),
DeviceState::Requesting => write!(f, "Wait for approval"),
DeviceState::Set => write!(f, "Encryption Key is ready"),
}
}
}
impl DeviceState {
pub fn idle(&self) -> bool {
matches!(self, DeviceState::Idle)