feat: nip4e (#188)

* encryption keys

* .

* .

* move nip4e to device crate

* .

* .

* use i18n for device crate

* refactor

* refactor

* .

* add reset button

* send message with encryption keys

* clean up

* .

* choose signer

* fix

* update i18n

* fix sending
This commit is contained in:
reya
2025-10-26 18:10:40 +07:00
committed by GitHub
parent 83687e5448
commit 15bbe82a87
29 changed files with 1856 additions and 851 deletions

View File

@@ -33,6 +33,7 @@ title_bar = { path = "../title_bar" }
theme = { path = "../theme" }
common = { path = "../common" }
states = { path = "../states" }
key_store = { path = "../key_store" }
registry = { path = "../registry" }
settings = { path = "../settings" }
auto_update = { path = "../auto_update" }

View File

@@ -1,8 +1,9 @@
use std::sync::Mutex;
use gpui::{actions, App, AppContext};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use nostr_connect::prelude::*;
use registry::keystore::KeyItem;
use registry::Registry;
use states::app_state;
@@ -48,7 +49,7 @@ pub fn load_embedded_fonts(cx: &App) {
pub fn reset(cx: &mut App) {
let registry = Registry::global(cx);
let keystore = registry.read(cx).keystore();
let backend = KeyStore::global(cx).read(cx).backend();
cx.spawn(async move |cx| {
cx.background_spawn(async move {
@@ -57,12 +58,12 @@ pub fn reset(cx: &mut App) {
})
.await;
keystore
backend
.delete_credentials(&KeyItem::User.to_string(), cx)
.await
.ok();
keystore
backend
.delete_credentials(&KeyItem::Bunker.to_string(), cx)
.await
.ok();

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use anyhow::{anyhow, Error};
use auto_update::AutoUpdater;
use common::display::RenderedProfile;
use common::display::{shorten_pubkey, RenderedProfile};
use common::event::EventUtils;
use gpui::prelude::FluentBuilder;
use gpui::{
@@ -14,14 +14,15 @@ use gpui::{
};
use i18n::{shared_t, t};
use itertools::Itertools;
use key_store::backend::KeyItem;
use key_store::KeyStore;
use nostr_connect::prelude::*;
use nostr_sdk::prelude::*;
use registry::keystore::KeyItem;
use registry::{Registry, RegistryEvent};
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use states::constants::{BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
use states::state::{AuthRequest, SignalKind, UnwrappingStatus};
use states::state::{Announcement, AuthRequest, Response, SignalKind, UnwrappingStatus};
use states::{app_state, default_nip17_relays, default_nip65_relays};
use theme::{ActiveTheme, Theme, ThemeMode};
use title_bar::TitleBar;
@@ -74,7 +75,7 @@ pub struct ChatSpace {
nip65_ready: bool,
/// All subscriptions for observing the app state
_subscriptions: SmallVec<[Subscription; 4]>,
_subscriptions: SmallVec<[Subscription; 3]>,
/// All long running tasks
_tasks: SmallVec<[Task<()>; 5]>,
@@ -83,7 +84,7 @@ pub struct ChatSpace {
impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let registry = Registry::global(cx);
let status = registry.read(cx).unwrapping_status.clone();
let keystore = KeyStore::global(cx);
let title_bar = cx.new(|_| TitleBar::new());
let dock = cx.new(|cx| DockArea::new(window, cx));
@@ -100,57 +101,38 @@ impl ChatSpace {
);
subscriptions.push(
// Observe the keystore
cx.observe_in(&registry, window, |this, registry, window, cx| {
let has_keyring = registry.read(cx).initialized_keystore;
let use_filestore = registry.read(cx).is_using_file_keystore();
let not_logged_in = registry.read(cx).signer_pubkey().is_none();
// Observe device changes
cx.observe_in(&keystore, window, move |this, state, window, cx| {
if state.read(cx).initialized {
let backend = state.read(cx).backend();
if use_filestore && not_logged_in {
this.render_keyring_installation(window, cx);
}
if state.read(cx).initialized {
if state.read(cx).is_using_file_keystore() {
this.render_keyring_installation(window, cx);
}
if has_keyring && not_logged_in {
let keystore = registry.read(cx).keystore();
cx.spawn_in(window, async move |this, cx| {
let result = backend
.read_credentials(&KeyItem::User.to_string(), cx)
.await;
cx.spawn_in(window, async move |this, cx| {
let result = keystore
.read_credentials(&KeyItem::User.to_string(), cx)
.await;
this.update_in(cx, |this, window, cx| {
match result {
Ok(Some((user, secret))) => {
let public_key = PublicKey::parse(&user).unwrap();
let secret = String::from_utf8(secret).unwrap();
this.update_in(cx, |this, window, cx| {
match result {
Ok(Some((user, secret))) => {
let public_key = PublicKey::parse(&user).unwrap();
let secret = String::from_utf8(secret).unwrap();
this.set_account_layout(public_key, secret, window, cx);
}
_ => {
this.set_onboarding_layout(window, cx);
}
};
this.set_account_layout(public_key, secret, window, cx);
}
_ => {
this.set_onboarding_layout(window, cx);
}
};
})
.ok();
})
.ok();
})
.detach();
}
}),
);
subscriptions.push(
// Observe the global registry's events
cx.observe_in(&status, window, move |this, status, window, cx| {
let status = status.read(cx);
let all_panels = this.get_all_panel_ids(cx);
if matches!(
status,
UnwrappingStatus::Processing | UnwrappingStatus::Complete
) {
Registry::global(cx).update(cx, |this, cx| {
this.load_rooms(window, cx);
this.refresh_rooms(all_panels, cx);
});
.detach();
}
}
}),
);
@@ -238,9 +220,21 @@ impl ChatSpace {
let settings = AppSettings::global(cx);
match signal {
SignalKind::EncryptionNotSet => {
this.init_encryption(window, cx);
}
SignalKind::EncryptionSet(announcement) => {
this.load_encryption(announcement, window, cx);
}
SignalKind::EncryptionRequest(announcement) => {
this.render_request(announcement, window, cx);
}
SignalKind::EncryptionResponse(response) => {
this.receive_encryption(response, window, cx);
}
SignalKind::SignerSet(public_key) => {
// Close the latest modal if it exists
window.close_modal(cx);
// Close all opened modals
window.close_all_modals(cx);
// Load user's settings
settings.update(cx, |this, cx| {
@@ -256,15 +250,6 @@ impl ChatSpace {
// Setup the default layout for current workspace
this.set_default_layout(window, cx);
}
SignalKind::SignerUnset => {
// Clear all current chat rooms
registry.update(cx, |this, cx| {
this.reset(cx);
});
// Setup the onboarding layout for current workspace
this.set_onboarding_layout(window, cx);
}
SignalKind::Auth(req) => {
let url = &req.url;
let auto_auth = AppSettings::get_auto_auth(cx);
@@ -281,10 +266,19 @@ impl ChatSpace {
this.open_auth_request(req, window, cx);
}
}
SignalKind::GiftWrapStatus(status) => {
registry.update(cx, |this, cx| {
this.set_unwrapping_status(status, cx);
});
SignalKind::GiftWrapStatus(s) => {
if matches!(s, UnwrappingStatus::Processing | UnwrappingStatus::Complete) {
let all_panels = this.get_all_panel_ids(cx);
registry.update(cx, |this, cx| {
this.load_rooms(window, cx);
this.refresh_rooms(all_panels, cx);
if s == UnwrappingStatus::Complete {
this.set_loading(false, cx);
}
});
}
}
SignalKind::NewProfile(profile) => {
registry.update(cx, |this, cx| {
@@ -309,6 +303,92 @@ impl ChatSpace {
}
}
fn init_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
cx.spawn_in(window, async move |this, cx| {
let result = app_state().init_encryption_keys().await;
this.update_in(cx, |_, window, cx| {
match result {
Ok(_) => {
window.push_notification(t!("encryption.notice"), cx);
}
Err(e) => {
// TODO: ask user to confirm re-running if failed
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
})
.detach();
}
fn load_encryption(&self, ann: Announcement, window: &Window, cx: &Context<Self>) {
log::info!("Loading encryption keys: {ann:?}");
cx.spawn_in(window, async move |this, cx| {
let state = app_state();
let result = state.load_encryption_keys(&ann).await;
this.update_in(cx, |this, window, cx| {
match result {
Ok(_) => {
window.push_notification(t!("encryption.reinit"), cx);
}
Err(_) => {
this.request_encryption(ann, window, cx);
}
};
})
.ok();
})
.detach();
}
fn request_encryption(&self, ann: Announcement, window: &Window, cx: &Context<Self>) {
cx.spawn_in(window, async move |this, cx| {
let result = app_state().request_encryption_keys().await;
this.update_in(cx, |this, window, cx| {
match result {
Ok(wait_for_approval) => {
if wait_for_approval {
this.render_pending(ann, window, cx);
} else {
window.push_notification(t!("encryption.success"), cx);
}
}
Err(e) => {
// TODO: ask user to confirm re-running if failed
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
})
.detach();
}
fn receive_encryption(&self, res: Response, window: &Window, cx: &Context<Self>) {
cx.spawn_in(window, async move |this, cx| {
let result = app_state().receive_encryption_keys(res).await;
this.update_in(cx, |_, window, cx| {
match result {
Ok(_) => {
window.push_notification(t!("encryption.success"), cx);
}
Err(e) => {
// TODO: ask user to confirm re-running if failed
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
})
.detach();
}
fn auth(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context<Self>) {
let settings = AppSettings::global(cx);
@@ -730,6 +810,132 @@ impl ChatSpace {
});
}
fn render_request(&mut self, ann: Announcement, window: &mut Window, cx: &mut Context<Self>) {
let client_name = SharedString::from(ann.client().to_string());
let target = ann.public_key();
let note = Notification::new()
.custom_id(SharedString::from(ann.id().to_hex()))
.autohide(false)
.icon(IconName::Info)
.title(shared_t!("request_encryption.label"))
.content(move |_window, cx| {
v_flex()
.gap_2()
.text_sm()
.child(shared_t!("request_encryption.body"))
.child(
v_flex()
.py_1()
.px_1p5()
.rounded_sm()
.text_xs()
.bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground)
.child(client_name.clone()),
)
.into_any_element()
})
.action(move |_window, _cx| {
Button::new("approve")
.label(t!("common.approve"))
.small()
.primary()
.loading(false)
.disabled(false)
.on_click(move |_ev, _window, cx| {
cx.background_spawn(async move {
let state = app_state();
state.response_encryption_keys(target).await.ok();
})
.detach();
})
});
window.push_notification(note, cx);
}
fn render_pending(&mut self, ann: Announcement, window: &mut Window, cx: &mut Context<Self>) {
let client_name = SharedString::from(ann.client().to_string());
let public_key = shorten_pubkey(ann.public_key(), 8);
let view = cx.entity().downgrade();
window.open_modal(cx, move |this, _window, cx| {
let view = view.clone();
this.overlay_closable(false)
.show_close(false)
.keyboard(false)
.confirm()
.width(px(460.))
.button_props(
ModalButtonProps::default()
.cancel_text(t!("common.reset"))
.ok_text(t!("common.hide")),
)
.title(shared_t!("pending_encryption.label"))
.child(
v_flex()
.gap_2()
.text_sm()
.child(
v_flex()
.justify_center()
.items_center()
.text_center()
.h_16()
.w_full()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.font_semibold()
.child(client_name.clone())
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from(&public_key)),
),
)
.child(shared_t!("pending_encryption.body_1", c = client_name))
.child(shared_t!("pending_encryption.body_2"))
.child(
div()
.text_xs()
.text_color(cx.theme().warning_foreground)
.child(shared_t!("pending_encryption.body_3")),
),
)
.on_cancel(move |_ev, window, cx| {
_ = view.update(cx, |this, cx| {
this.render_reset(window, cx);
});
// false to keep modal open
false
})
});
}
fn render_reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
cx.spawn_in(window, async move |this, cx| {
let state = app_state();
let result = state.init_encryption_keys().await;
this.update_in(cx, |_, window, cx| {
match result {
Ok(_) => {
window.push_notification(t!("encryption.success"), cx);
window.close_all_modals(cx);
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
})
.detach();
}
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
let relays = default_nip65_relays();
@@ -937,15 +1143,15 @@ impl ChatSpace {
_window: &mut Window,
cx: &Context<Self>,
) -> impl IntoElement {
let registry = Registry::read_global(cx);
let status = registry.unwrapping_status.read(cx);
let registry = Registry::global(cx);
let status = registry.read(cx).loading;
h_flex()
.gap_2()
.h_6()
.w_full()
.child(compose_button())
.when(status != &UnwrappingStatus::Complete, |this| {
.when(status, |this| {
this.child(deferred(
h_flex()
.px_2()

View File

@@ -7,7 +7,7 @@ use gpui::{
WindowOptions,
};
use states::app_state;
use states::constants::{APP_ID, APP_NAME};
use states::constants::{APP_ID, CLIENT_NAME};
use ui::Root;
use crate::actions::{load_embedded_fonts, quit, Quit};
@@ -63,7 +63,7 @@ fn main() {
kind: WindowKind::Normal,
app_id: Some(APP_ID.to_owned()),
titlebar: Some(TitlebarOptions {
title: Some(SharedString::new_static(APP_NAME)),
title: Some(SharedString::new_static(CLIENT_NAME)),
traffic_light_position: Some(point(px(9.0), px(9.0))),
appears_transparent: true,
}),
@@ -86,6 +86,9 @@ fn main() {
// Initialize app registry
registry::init(cx);
// Initialize backend for credentials storage
key_store::init(cx);
// Initialize settings
settings::init(cx);

View File

@@ -9,8 +9,9 @@ use gpui::{
Window,
};
use i18n::{shared_t, t};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use nostr_connect::prelude::*;
use registry::keystore::KeyItem;
use registry::Registry;
use smallvec::{smallvec, SmallVec};
use states::app_state;
@@ -116,7 +117,7 @@ impl Account {
window: &mut Window,
cx: &mut Context<Self>,
) {
let keystore = Registry::global(cx).read(cx).keystore();
let keystore = KeyStore::global(cx).read(cx).backend();
// Handle connection in the background
cx.spawn_in(window, async move |this, cx| {

View File

@@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use std::time::Duration;
use common::display::{RenderedProfile, RenderedTimestamp};
@@ -17,7 +17,7 @@ use indexset::{BTreeMap, BTreeSet};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use registry::message::{Message, RenderedMessage};
use registry::room::{Room, RoomKind, RoomSignal, SendReport};
use registry::room::{Room, RoomKind, RoomSignal, SendOptions, SendReport, SignerKind};
use registry::Registry;
use serde::Deserialize;
use settings::AppSettings;
@@ -47,6 +47,10 @@ mod subject;
#[action(namespace = chat, no_json)]
pub struct SeenOn(pub EventId);
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = chat, no_json)]
pub struct SetSigner(pub SignerKind);
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> {
cx.new(|cx| Chat::new(room, window, cx))
}
@@ -54,7 +58,6 @@ pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Cha
pub struct Chat {
// Chat Room
room: Entity<Room>,
relays: Entity<HashMap<PublicKey, Vec<RelayUrl>>>,
// Messages
list_state: ListState,
@@ -64,6 +67,7 @@ pub struct Chat {
// New Message
input: Entity<InputState>,
options: Entity<SendOptions>,
replies_to: Entity<HashSet<EventId>>,
// Media Attachment
@@ -75,20 +79,12 @@ pub struct Chat {
focus_handle: FocusHandle,
image_cache: Entity<RetainAllImageCache>,
_subscriptions: SmallVec<[Subscription; 4]>,
_subscriptions: SmallVec<[Subscription; 3]>,
_tasks: SmallVec<[Task<()>; 2]>,
}
impl Chat {
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let attachments = cx.new(|_| vec![]);
let replies_to = cx.new(|_| HashSet::new());
let relays = cx.new(|_| {
let this: HashMap<PublicKey, Vec<RelayUrl>> = HashMap::new();
this
});
let input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(t!("chat.placeholder"))
@@ -97,11 +93,16 @@ impl Chat {
.clean_on_escape()
});
let attachments = cx.new(|_| vec![]);
let replies_to = cx.new(|_| HashSet::new());
let options = cx.new(|_| SendOptions::default());
let id = room.read(cx).id.to_string().into();
let messages = BTreeSet::from([Message::system()]);
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
let connect = room.read(cx).connect(cx);
let load_messages = room.read(cx).load_messages(cx);
let get_messages = room.read(cx).get_messages(cx);
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
@@ -109,7 +110,7 @@ impl Chat {
tasks.push(
// Load all messages belonging to this room
cx.spawn_in(window, async move |this, cx| {
let result = load_messages.await;
let result = get_messages.await;
this.update_in(cx, |this, window, cx| {
match result {
@@ -126,24 +127,11 @@ impl Chat {
);
tasks.push(
// Get messaging relays for all members
cx.spawn_in(window, async move |this, cx| {
let result = connect.await;
this.update_in(cx, |this, _window, cx| {
match result {
Ok(relays) => {
this.relays.update(cx, |this, cx| {
this.extend(relays);
cx.notify();
});
}
Err(e) => {
this.insert_warning(e.to_string(), cx);
}
};
})
.ok();
// Get messaging relays and encryption keys announcement for all members
cx.background_spawn(async move {
if let Err(e) = connect.await {
log::error!("Failed to initialize room: {e}");
}
}),
);
@@ -189,23 +177,6 @@ impl Chat {
}),
);
subscriptions.push(
// Observe the messaging relays of the room's members
cx.observe_in(&relays, window, |this, entity, _window, cx| {
let registry = Registry::global(cx);
let relays = entity.read(cx).clone();
for (public_key, urls) in relays.iter() {
if urls.is_empty() {
let profile = registry.read(cx).get_person(public_key, cx);
let content = t!("chat.nip17_not_found", u = profile.name());
this.insert_warning(content, cx);
}
}
}),
);
subscriptions.push(
// Observe when user close chat panel
cx.on_release_in(window, move |this, window, cx| {
@@ -219,19 +190,19 @@ impl Chat {
);
Self {
id: room.read(cx).id.to_string().into(),
image_cache: RetainAllImageCache::new(cx),
focus_handle: cx.focus_handle(),
rendered_texts_by_id: BTreeMap::new(),
reports_by_id: BTreeMap::new(),
relays,
id,
messages,
room,
list_state,
input,
replies_to,
attachments,
options,
rendered_texts_by_id: BTreeMap::new(),
reports_by_id: BTreeMap::new(),
uploading: false,
image_cache: RetainAllImageCache::new(cx),
focus_handle: cx.focus_handle(),
_subscriptions: subscriptions,
_tasks: tasks,
}
@@ -239,12 +210,12 @@ impl Chat {
/// Load all messages belonging to this room
fn load_messages(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let load_messages = self.room.read(cx).load_messages(cx);
let get_messages = self.room.read(cx).get_messages(cx);
self._tasks.push(
// Run the task in the background
cx.spawn_in(window, async move |this, cx| {
let result = load_messages.await;
let result = get_messages.await;
this.update_in(cx, |this, window, cx| {
match result {
@@ -303,9 +274,6 @@ impl Chat {
this.set_value("", window, cx);
});
// Get the backup setting
let backup = AppSettings::get_backup_messages(cx);
// Get replies_to if it's present
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
@@ -317,14 +285,17 @@ impl Chat {
let rumor_id = rumor.id.unwrap();
// Create a task for sending the message in the background
let send_message = room.send_message(rumor.clone(), backup, cx);
let opts = self.options.read(cx);
let send_message = room.send_message(&rumor, opts, cx);
// Optimistically update message list
cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
let delay = Duration::from_millis(100);
// Wait for the delay
cx.background_executor().timer(delay).await;
// Update the message list and reset the states
this.update_in(cx, |this, window, cx| {
this.insert_message(Message::user(rumor), true, cx);
this.remove_all_replies(cx);
@@ -339,37 +310,39 @@ impl Chat {
})
.detach();
// Continue sending the message in the background
cx.spawn_in(window, async move |this, cx| {
let result = send_message.await;
self._tasks.push(
// Continue sending the message in the background
cx.spawn_in(window, async move |this, cx| {
let result = send_message.await;
this.update_in(cx, |this, window, cx| {
match result {
Ok(reports) => {
this.room.update(cx, |this, cx| {
if this.kind != RoomKind::Ongoing {
// Update the room kind to ongoing
// But keep the room kind if send failed
if reports.iter().all(|r| !r.is_sent_success()) {
this.kind = RoomKind::Ongoing;
cx.notify();
this.update_in(cx, |this, window, cx| {
match result {
Ok(reports) => {
// Update room's status
this.room.update(cx, |this, cx| {
if this.kind != RoomKind::Ongoing {
// Update the room kind to ongoing,
// but keep the room kind if send failed
if reports.iter().all(|r| !r.is_sent_success()) {
this.kind = RoomKind::Ongoing;
cx.notify();
}
}
}
});
});
// Insert the sent reports
this.reports_by_id.insert(rumor_id, reports);
// Insert the sent reports
this.reports_by_id.insert(rumor_id, reports);
cx.notify();
cx.notify();
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
}
})
.ok();
})
.detach();
})
.ok();
}),
);
}
/// Resend a failed message
@@ -432,6 +405,7 @@ impl Chat {
}
/// Insert a warning message into the chat panel
#[allow(dead_code)]
fn insert_warning(&mut self, content: impl Into<String>, cx: &mut Context<Self>) {
let m = Message::warning(content.into());
self.insert_message(m, true, cx);
@@ -473,6 +447,10 @@ impl Chat {
registry.get_person(public_key, cx)
}
fn signer_kind(&self, cx: &App) -> SignerKind {
self.options.read(cx).signer_kind
}
fn scroll_to(&self, id: EventId) {
if let Some(ix) = self.messages.iter().position(|m| {
if let Message::User(msg) = m {
@@ -543,29 +521,24 @@ impl Chat {
})
.ok();
match Flatten::flatten(task.await.map_err(|e| e.into())) {
Ok(Some(url)) => {
this.update(cx, |this, cx| {
let result = Flatten::flatten(task.await.map_err(|e| e.into()));
this.update_in(cx, |this, window, cx| {
match result {
Ok(Some(url)) => {
this.add_attachment(url, cx);
this.set_uploading(false, cx);
})
.ok();
}
Ok(None) => {
this.update_in(cx, |this, window, cx| {
window.push_notification("Failed to upload file", cx);
}
Ok(None) => {
this.set_uploading(false, cx);
})
.ok();
}
Err(e) => {
this.update_in(cx, |this, window, cx| {
}
Err(e) => {
window.push_notification(Notification::error(e.to_string()), cx);
this.set_uploading(false, cx);
})
.ok();
}
}
}
};
})
.ok();
}
Some(())
@@ -911,6 +884,27 @@ impl Chat {
),
)
})
.when(report.device_not_found, |this| {
this.child(
h_flex()
.flex_wrap()
.justify_center()
.p_2()
.h_20()
.w_full()
.text_sm()
.rounded(cx.theme().radius)
.bg(cx.theme().danger_background)
.text_color(cx.theme().danger_foreground)
.child(
div()
.flex_1()
.w_full()
.text_center()
.child(shared_t!("chat.device_not_found", u = name)),
),
)
})
.when_some(report.error.clone(), |this, error| {
this.child(
h_flex()
@@ -1291,6 +1285,13 @@ impl Chat {
})
.detach();
}
fn on_set_encryption(&mut self, ev: &SetSigner, _: &mut Window, cx: &mut Context<Self>) {
self.options.update(cx, move |this, cx| {
this.signer_kind = ev.0;
cx.notify();
});
}
}
impl Panel for Chat {
@@ -1334,8 +1335,11 @@ impl Focusable for Chat {
impl Render for Chat {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let kind = self.signer_kind(cx);
v_flex()
.on_action(cx.listener(Self::on_open_seen_on))
.on_action(cx.listener(Self::on_set_encryption))
.image_cache(self.image_cache.clone())
.size_full()
.child(
@@ -1384,9 +1388,7 @@ impl Render for Chat {
.items_end()
.gap_2p5()
.child(
div()
.flex()
.items_center()
h_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.child(
@@ -1408,7 +1410,31 @@ impl Render for Chat {
.large(),
),
)
.child(TextInput::new(&self.input)),
.child(TextInput::new(&self.input))
.child(
Button::new("options")
.icon(IconName::Settings)
.ghost()
.large()
.popup_menu(move |this, _window, _cx| {
this.title("Encrypt by:")
.menu_with_check(
"Encryption Key",
matches!(kind, SignerKind::Encryption),
Box::new(SetSigner(SignerKind::Encryption)),
)
.menu_with_check(
"User's Identity",
matches!(kind, SignerKind::User),
Box::new(SetSigner(SignerKind::User)),
)
.menu_with_check(
"Auto",
matches!(kind, SignerKind::Auto),
Box::new(SetSigner(SignerKind::Auto)),
)
}),
),
),
),
)

View File

@@ -7,9 +7,9 @@ use gpui::{
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
};
use i18n::{shared_t, t};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use nostr_connect::prelude::*;
use registry::keystore::KeyItem;
use registry::Registry;
use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::BUNKER_TIMEOUT;
@@ -174,7 +174,7 @@ impl Login {
window: &mut Window,
cx: &mut Context<Self>,
) {
let keystore = Registry::global(cx).read(cx).keystore();
let keystore = KeyStore::global(cx).read(cx).backend();
let username = keys.public_key().to_hex();
let secret = keys.secret_key().to_secret_bytes();
let mut clean_uri = uri.to_string();
@@ -263,7 +263,7 @@ impl Login {
}
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
let keystore = Registry::global(cx).read(cx).keystore();
let keystore = KeyStore::global(cx).read(cx).backend();
let username = keys.public_key().to_hex();
let secret = keys.secret_key().to_secret_hex().into_bytes();

View File

@@ -7,9 +7,9 @@ use gpui::{
};
use gpui_tokio::Tokio;
use i18n::{shared_t, t};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use nostr_sdk::prelude::*;
use registry::keystore::KeyItem;
use registry::Registry;
use settings::AppSettings;
use smol::fs;
use states::constants::BOOTSTRAP_RELAYS;
@@ -106,7 +106,7 @@ impl NewAccount {
}
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
let keystore = Registry::global(cx).read(cx).keystore();
let keystore = KeyStore::global(cx).read(cx).backend();
let keys = self.temp_keys.read(cx).clone();
let username = keys.public_key().to_hex();

View File

@@ -9,12 +9,12 @@ use gpui::{
SharedString, StatefulInteractiveElement, Styled, Task, Window,
};
use i18n::{shared_t, t};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use nostr_connect::prelude::*;
use registry::keystore::KeyItem;
use registry::Registry;
use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
use states::constants::{CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
@@ -81,7 +81,7 @@ impl Onboarding {
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME);
let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], CLIENT_NAME);
let qr_code = uri.to_string().to_qr();
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
@@ -126,7 +126,7 @@ impl Onboarding {
window: &mut Window,
cx: &mut Context<Self>,
) {
let keystore = Registry::global(cx).read(cx).keystore();
let keystore = KeyStore::global(cx).read(cx).backend();
let username = self.app_keys.public_key().to_hex();
let secret = self.app_keys.secret_key().to_secret_bytes();
let mut clean_uri = uri.to_string();

View File

@@ -52,8 +52,8 @@ impl RoomListItem {
self
}
pub fn public_key(mut self, public_key: PublicKey) -> Self {
self.public_key = Some(public_key);
pub fn public_key(mut self, public_key: &PublicKey) -> Self {
self.public_key = Some(public_key.to_owned());
self
}

View File

@@ -22,7 +22,6 @@ use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use states::state::UnwrappingStatus;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
@@ -627,7 +626,7 @@ impl Sidebar {
.name(this.display_name(cx))
.avatar(this.display_image(proxy, cx))
.created_at(this.created_at.to_ago())
.public_key(this.members[0])
.public_key(this.members.iter().nth(0).unwrap().0)
.kind(this.kind)
.on_click(handler),
)
@@ -669,7 +668,7 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let registry = Registry::read_global(cx);
let loading = registry.unwrapping_status.read(cx) != &UnwrappingStatus::Complete;
let loading = registry.loading;
// Get rooms from either search results or the chat registry
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {