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:
@@ -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" }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(®istry, 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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user