feat: revamp the onboarding process (#205)
* redesign * restructure * . * . * . * . * .
This commit is contained in:
@@ -30,13 +30,12 @@ use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
|||||||
use ui::modal::ModalButtonProps;
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::popover::{Popover, PopoverContent};
|
use ui::popover::{Popover, PopoverContent};
|
||||||
use ui::popup_menu::PopupMenuExt;
|
use ui::popup_menu::PopupMenuExt;
|
||||||
use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable};
|
use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::actions::{reset, DarkMode, KeyringPopup, Logout, Settings};
|
use crate::actions::{reset, DarkMode, KeyringPopup, Logout, Settings};
|
||||||
use crate::views::compose::compose_button;
|
use crate::views::compose::compose_button;
|
||||||
use crate::views::{
|
use crate::views::{onboarding, preferences, sidebar, startup, user_profile, welcome};
|
||||||
login, new_account, onboarding, preferences, sidebar, startup, user_profile, welcome,
|
use crate::{login, new_identity};
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||||
cx.new(|cx| ChatSpace::new(window, cx))
|
cx.new(|cx| ChatSpace::new(window, cx))
|
||||||
@@ -48,7 +47,7 @@ pub fn login(window: &mut Window, cx: &mut App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_account(window: &mut Window, cx: &mut App) {
|
pub fn new_account(window: &mut Window, cx: &mut App) {
|
||||||
let panel = new_account::init(window, cx);
|
let panel = new_identity::init(window, cx);
|
||||||
ChatSpace::set_center_panel(panel, window, cx);
|
ChatSpace::set_center_panel(panel, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +495,43 @@ impl ChatSpace {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn titlebar_center(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let entity = cx.entity().downgrade();
|
||||||
|
let panel = self.dock.read(cx).items.view();
|
||||||
|
let title = panel.title(cx);
|
||||||
|
let id = panel.panel_id(cx);
|
||||||
|
|
||||||
|
if id == "Onboarding" {
|
||||||
|
return div();
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.w_full()
|
||||||
|
.justify_center()
|
||||||
|
.text_center()
|
||||||
|
.font_semibold()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
div().flex_1().child(
|
||||||
|
Button::new("back")
|
||||||
|
.icon(IconName::ArrowLeft)
|
||||||
|
.small()
|
||||||
|
.ghost_alt()
|
||||||
|
.rounded()
|
||||||
|
.on_click(move |_ev, window, cx| {
|
||||||
|
entity
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.set_onboarding_layout(window, cx);
|
||||||
|
})
|
||||||
|
.expect("Entity has been released");
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(div().flex_1().child(title))
|
||||||
|
.child(div().flex_1())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ChatSpace {
|
impl Render for ChatSpace {
|
||||||
@@ -505,10 +541,16 @@ impl Render for ChatSpace {
|
|||||||
|
|
||||||
let left = self.titlebar_left(window, cx).into_any_element();
|
let left = self.titlebar_left(window, cx).into_any_element();
|
||||||
let right = self.titlebar_right(window, cx).into_any_element();
|
let right = self.titlebar_right(window, cx).into_any_element();
|
||||||
|
let center = self.titlebar_center(cx).into_any_element();
|
||||||
|
let single_panel = self.dock.read(cx).items.panel_ids(cx).is_empty();
|
||||||
|
|
||||||
// Update title bar children
|
// Update title bar children
|
||||||
self.title_bar.update(cx, |this, _cx| {
|
self.title_bar.update(cx, |this, _cx| {
|
||||||
|
if single_panel {
|
||||||
|
this.set_children(vec![center]);
|
||||||
|
} else {
|
||||||
this.set_children(vec![left, right]);
|
this.set_children(vec![left, right]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ use ui::{v_flex, ContextModal, Disableable, StyledExt};
|
|||||||
use crate::actions::CoopAuthUrlHandler;
|
use crate::actions::CoopAuthUrlHandler;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||||
Login::new(window, cx)
|
cx.new(|cx| Login::new(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Login {
|
pub struct Login {
|
||||||
key_input: Entity<InputState>,
|
key_input: Entity<InputState>,
|
||||||
pass_input: Entity<InputState>,
|
pass_input: Entity<InputState>,
|
||||||
@@ -42,11 +43,7 @@ pub struct Login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Login {
|
impl Login {
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
cx.new(|cx| Self::view(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let key_input = cx.new(|cx| InputState::new(window, cx));
|
let key_input = cx.new(|cx| InputState::new(window, cx));
|
||||||
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
|
||||||
@@ -78,7 +75,7 @@ impl Login {
|
|||||||
pass_input,
|
pass_input,
|
||||||
error,
|
error,
|
||||||
countdown,
|
countdown,
|
||||||
name: "Login".into(),
|
name: "Welcome Back".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
logging_in: false,
|
logging_in: false,
|
||||||
require_password: false,
|
require_password: false,
|
||||||
@@ -370,18 +367,10 @@ impl Render for Login {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_center()
|
.text_center()
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xl()
|
.text_xl()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.line_height(relative(1.3))
|
.line_height(relative(1.3))
|
||||||
.child(shared_t!("login.title")),
|
.child(SharedString::from("Continue with Private Key or Bunker")),
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(shared_t!("login.key_description")),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -11,9 +11,11 @@ use ui::Root;
|
|||||||
|
|
||||||
use crate::actions::{load_embedded_fonts, quit, Quit};
|
use crate::actions::{load_embedded_fonts, quit, Quit};
|
||||||
|
|
||||||
pub(crate) mod actions;
|
mod actions;
|
||||||
pub(crate) mod chatspace;
|
mod chatspace;
|
||||||
pub(crate) mod views;
|
mod login;
|
||||||
|
mod new_identity;
|
||||||
|
mod views;
|
||||||
|
|
||||||
i18n::init!();
|
i18n::init!();
|
||||||
|
|
||||||
|
|||||||
217
crates/coop/src/new_identity/backup.rs
Normal file
217
crates/coop/src/new_identity/backup.rs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
|
use common::home_dir;
|
||||||
|
use gpui::{
|
||||||
|
div, App, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||||
|
Render, SharedString, Styled, Task, Window,
|
||||||
|
};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::input::{InputState, TextInput};
|
||||||
|
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
|
pub fn init(keys: &Keys, window: &mut Window, cx: &mut App) -> Entity<Backup> {
|
||||||
|
cx.new(|cx| Backup::new(keys, window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Backup {
|
||||||
|
pubkey_input: Entity<InputState>,
|
||||||
|
secret_input: Entity<InputState>,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
copied: bool,
|
||||||
|
|
||||||
|
// Async operations
|
||||||
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backup {
|
||||||
|
pub fn new(keys: &Keys, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let Ok(npub) = keys.public_key.to_bech32();
|
||||||
|
let Ok(nsec) = keys.secret_key().to_bech32();
|
||||||
|
|
||||||
|
let pubkey_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.disabled(true)
|
||||||
|
.default_value(npub)
|
||||||
|
});
|
||||||
|
|
||||||
|
let secret_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.disabled(true)
|
||||||
|
.default_value(nsec)
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pubkey_input,
|
||||||
|
secret_input,
|
||||||
|
error: None,
|
||||||
|
copied: false,
|
||||||
|
_tasks: smallvec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backup(&self, window: &Window, cx: &Context<Self>) -> Task<Result<(), Error>> {
|
||||||
|
let dir = home_dir();
|
||||||
|
let path = cx.prompt_for_new_path(dir, Some("My Nostr Account"));
|
||||||
|
let nsec = self.secret_input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match Flatten::flatten(path.await.map_err(|e| e.into())) {
|
||||||
|
Ok(Ok(Some(path))) => {
|
||||||
|
if let Err(e) = smol::fs::write(&path, nsec).await {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_error(e.to_string(), window, cx);
|
||||||
|
})
|
||||||
|
.expect("Entity has been released");
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!("Failed to save backup keys");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Err(anyhow!("Failed to backup keys"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy(&mut self, value: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let item = ClipboardItem::new_string(value.into());
|
||||||
|
cx.write_to_clipboard(item);
|
||||||
|
|
||||||
|
self.set_copied(true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_copied(&mut self, status: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.copied = status;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
// Reset the copied state after a delay
|
||||||
|
if status {
|
||||||
|
self._tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_copied(false, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
E: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
// Clear the error message after a delay
|
||||||
|
self._tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Backup {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
const DESCRIPTION: &str = "In Nostr, your account is defined by a KEY PAIR. These keys are used to sign your messages and identify you.";
|
||||||
|
const WARN: &str = "You must keep the Secret Key in a safe place. If you lose it, you will lose access to your account.";
|
||||||
|
const PK: &str = "Public Key is the address that others will use to find you.";
|
||||||
|
const SK: &str = "Secret Key provides access to your account.";
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(SharedString::from(DESCRIPTION))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.font_semibold()
|
||||||
|
.child(SharedString::from("Public Key:")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(TextInput::new(&self.pubkey_input).small())
|
||||||
|
.child(
|
||||||
|
Button::new("copy-pubkey")
|
||||||
|
.icon({
|
||||||
|
if self.copied {
|
||||||
|
IconName::CheckCircleFill
|
||||||
|
} else {
|
||||||
|
IconName::Copy
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ghost_alt()
|
||||||
|
.disabled(self.copied)
|
||||||
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
|
this.copy(this.pubkey_input.read(cx).value(), window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(PK)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(divider(cx))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.font_semibold()
|
||||||
|
.child(SharedString::from("Secret Key:")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(TextInput::new(&self.secret_input).small())
|
||||||
|
.child(
|
||||||
|
Button::new("copy-secret")
|
||||||
|
.icon({
|
||||||
|
if self.copied {
|
||||||
|
IconName::CheckCircleFill
|
||||||
|
} else {
|
||||||
|
IconName::Copy
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ghost_alt()
|
||||||
|
.disabled(self.copied)
|
||||||
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
|
this.copy(this.secret_input.read(cx).value(), window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(SK)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(divider(cx))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().danger_foreground)
|
||||||
|
.child(SharedString::from(WARN)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use common::{default_nip17_relays, default_nip65_relays, nip96_upload, BOOTSTRAP_RELAYS};
|
use common::{default_nip17_relays, default_nip65_relays, nip96_upload, BOOTSTRAP_RELAYS};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten, FocusHandle,
|
||||||
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
||||||
Render, SharedString, Styled, Task, WeakEntity, Window,
|
Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
@@ -12,20 +12,20 @@ use nostr_sdk::prelude::*;
|
|||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
use ui::modal::ModalButtonProps;
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::{divider, v_flex, ContextModal, Disableable, IconName, Sizable, StyledExt};
|
use ui::{divider, v_flex, ContextModal, Disableable, IconName, Sizable};
|
||||||
|
|
||||||
use crate::views::backup_keys::BackupKeys;
|
mod backup;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||||
NewAccount::new(window, cx)
|
cx.new(|cx| NewAccount::new(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct NewAccount {
|
pub struct NewAccount {
|
||||||
name_input: Entity<InputState>,
|
name_input: Entity<InputState>,
|
||||||
avatar_input: Entity<InputState>,
|
avatar_input: Entity<InputState>,
|
||||||
@@ -38,11 +38,7 @@ pub struct NewAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NewAccount {
|
impl NewAccount {
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
cx.new(|cx| Self::view(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let temp_keys = cx.new(|_| Keys::generate());
|
let temp_keys = cx.new(|_| Keys::generate());
|
||||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
||||||
let avatar_input = cx.new(|cx| InputState::new(window, cx));
|
let avatar_input = cx.new(|cx| InputState::new(window, cx));
|
||||||
@@ -53,7 +49,7 @@ impl NewAccount {
|
|||||||
temp_keys,
|
temp_keys,
|
||||||
uploading: false,
|
uploading: false,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
name: "New Account".into(),
|
name: "Create a new identity".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +58,7 @@ impl NewAccount {
|
|||||||
self.submitting(true, cx);
|
self.submitting(true, cx);
|
||||||
|
|
||||||
let keys = self.temp_keys.read(cx).clone();
|
let keys = self.temp_keys.read(cx).clone();
|
||||||
let view = cx.new(|cx| BackupKeys::new(&keys, window, cx));
|
let view = backup::init(&keys, window, cx);
|
||||||
let weak_view = view.downgrade();
|
let weak_view = view.downgrade();
|
||||||
let current_view = cx.entity().downgrade();
|
let current_view = cx.entity().downgrade();
|
||||||
|
|
||||||
@@ -80,20 +76,25 @@ impl NewAccount {
|
|||||||
.on_ok(move |_, window, cx| {
|
.on_ok(move |_, window, cx| {
|
||||||
weak_view
|
weak_view
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
let current_view = current_view.clone();
|
let view = current_view.clone();
|
||||||
|
let task = this.backup(window, cx);
|
||||||
|
|
||||||
if let Some(task) = this.backup(window, cx) {
|
cx.spawn_in(window, async move |_this, cx| {
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
let result = task.await;
|
||||||
task.await;
|
|
||||||
|
|
||||||
current_view
|
match result {
|
||||||
.update(cx, |this, cx| {
|
Ok(_) => {
|
||||||
this.set_signer(cx);
|
view.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_signer(window, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.expect("Entity has been released");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to backup: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
// true to close the modal
|
// true to close the modal
|
||||||
@@ -102,7 +103,7 @@ impl NewAccount {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
|
pub fn set_signer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let keystore = KeyStore::global(cx).read(cx).backend();
|
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
@@ -120,24 +121,14 @@ impl NewAccount {
|
|||||||
metadata = metadata.picture(url);
|
metadata = metadata.picture(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn(async move |_, cx| {
|
// Close all modals if available
|
||||||
let url = KeyItem::User.to_string();
|
window.close_all_modals(cx);
|
||||||
|
|
||||||
// Write the app keys for further connection
|
|
||||||
keystore
|
|
||||||
.write_credentials(&url, &username, &secret, cx)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Update the signer
|
|
||||||
// Set the client's signer with the current keys
|
// Set the client's signer with the current keys
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
// Set the client's signer with the current keys
|
let signer = keys.clone();
|
||||||
client.set_signer(keys).await;
|
|
||||||
|
|
||||||
// Verify the signer
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let nip65_relays = default_nip65_relays();
|
let nip65_relays = default_nip65_relays();
|
||||||
|
let nip17_relays = default_nip17_relays();
|
||||||
|
|
||||||
// Construct a NIP-65 event
|
// Construct a NIP-65 event
|
||||||
let event = EventBuilder::new(Kind::RelayList, "")
|
let event = EventBuilder::new(Kind::RelayList, "")
|
||||||
@@ -153,12 +144,6 @@ impl NewAccount {
|
|||||||
// Set NIP-65 relays
|
// Set NIP-65 relays
|
||||||
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
||||||
|
|
||||||
// Ensure relays are connected
|
|
||||||
for (url, _metadata) in nip65_relays.iter() {
|
|
||||||
client.add_relay(url).await?;
|
|
||||||
client.connect_relay(url).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract only write relays
|
// Extract only write relays
|
||||||
let write_relays: Vec<RelayUrl> = nip65_relays
|
let write_relays: Vec<RelayUrl> = nip65_relays
|
||||||
.iter()
|
.iter()
|
||||||
@@ -169,12 +154,17 @@ impl NewAccount {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.take(3)
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Ensure relays are connected
|
||||||
|
for url in write_relays.iter() {
|
||||||
|
client.add_relay(url).await?;
|
||||||
|
client.connect_relay(url).await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Construct a NIP-17 event
|
// Construct a NIP-17 event
|
||||||
let event = EventBuilder::new(Kind::InboxRelays, "")
|
let event = EventBuilder::new(Kind::InboxRelays, "")
|
||||||
.tags(default_nip17_relays().iter().cloned().map(Tag::relay))
|
.tags(nip17_relays.iter().cloned().map(Tag::relay))
|
||||||
.sign(&signer)
|
.sign(&signer)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -184,13 +174,32 @@ impl NewAccount {
|
|||||||
// Construct a metadata event
|
// Construct a metadata event
|
||||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||||
|
|
||||||
// Set metadata
|
// Send metadata event to both write relays and bootstrap relays
|
||||||
client.send_event_to(&write_relays, &event).await?;
|
client.send_event_to(&write_relays, &event).await?;
|
||||||
|
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
||||||
|
|
||||||
|
// Update the client's signer with the current keys
|
||||||
|
client.set_signer(keys).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
task.detach();
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let url = KeyItem::User.to_string();
|
||||||
|
|
||||||
|
// Write the app keys for further connection
|
||||||
|
keystore
|
||||||
|
.write_credentials(&url, &username, &secret, cx)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if let Err(e) = task.await {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.submitting(false, cx);
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
})
|
||||||
|
.expect("Entity has been released");
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -230,39 +239,31 @@ impl NewAccount {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
match Flatten::flatten(task.await.map_err(|e| e.into())) {
|
let result = Flatten::flatten(task.await.map_err(|e| e.into()));
|
||||||
Ok(Ok(url)) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(Ok(url)) => {
|
||||||
this.uploading(false, cx);
|
this.uploading(false, cx);
|
||||||
this.avatar_input.update(cx, |this, cx| {
|
this.avatar_input.update(cx, |this, cx| {
|
||||||
this.set_value(url.to_string(), window, cx);
|
this.set_value(url.to_string(), window, cx);
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
Self::notify_error(cx, this, e.to_string());
|
window.push_notification(e.to_string(), cx);
|
||||||
|
this.uploading(false, cx);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
Self::notify_error(cx, this, e.to_string());
|
log::warn!("Failed to upload avatar: {e}");
|
||||||
}
|
this.uploading(false, cx);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.expect("Entity has been released");
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_error(cx: &mut AsyncWindowContext, entity: WeakEntity<NewAccount>, e: String) {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
entity
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
window.push_notification(e, cx);
|
|
||||||
this.uploading(false, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
self.submitting = status;
|
self.submitting = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -294,64 +295,48 @@ impl Focusable for NewAccount {
|
|||||||
|
|
||||||
impl Render for NewAccount {
|
impl Render for NewAccount {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let avatar = self.avatar_input.read(cx).value();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.relative()
|
.relative()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.gap_10()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_lg()
|
|
||||||
.text_center()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.3))
|
|
||||||
.child(shared_t!("new_account.title")),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_96()
|
.w_96()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.h_40()
|
||||||
|
.w_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
|
.child(Avatar::new(avatar).size(rems(4.25)))
|
||||||
|
.child(
|
||||||
|
Button::new("upload")
|
||||||
|
.icon(IconName::PlusCircleFill)
|
||||||
|
.label("Add an avatar")
|
||||||
|
.xsmall()
|
||||||
|
.ghost()
|
||||||
|
.rounded()
|
||||||
|
.disabled(self.uploading)
|
||||||
|
//.loading(self.uploading)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.upload(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(shared_t!("new_account.name"))
|
.child(shared_t!("new_account.name"))
|
||||||
.child(TextInput::new(&self.name_input).small()),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
TextInput::new(&self.name_input)
|
||||||
.gap_1()
|
.disabled(self.submitting)
|
||||||
.child(div().text_sm().child(shared_t!("new_account.avatar")))
|
.small(),
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.p_1()
|
|
||||||
.h_32()
|
|
||||||
.w_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap_2()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.border_1()
|
|
||||||
.border_dashed()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.child(
|
|
||||||
Avatar::new(self.avatar_input.read(cx).value().to_string())
|
|
||||||
.size(rems(2.25)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("upload")
|
|
||||||
.icon(IconName::Plus)
|
|
||||||
.label(t!("common.upload"))
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.rounded()
|
|
||||||
.disabled(self.submitting || self.uploading)
|
|
||||||
.loading(self.uploading)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.upload(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(divider(cx))
|
.child(divider(cx))
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use common::home_dir;
|
|
||||||
use gpui::{
|
|
||||||
div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render,
|
|
||||||
SharedString, Styled, Task, Window,
|
|
||||||
};
|
|
||||||
use i18n::{shared_t, t};
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::input::{InputState, TextInput};
|
|
||||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable};
|
|
||||||
|
|
||||||
pub struct BackupKeys {
|
|
||||||
pubkey_input: Entity<InputState>,
|
|
||||||
secret_input: Entity<InputState>,
|
|
||||||
error: Option<SharedString>,
|
|
||||||
copied: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackupKeys {
|
|
||||||
pub fn new(keys: &Keys, window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
|
|
||||||
let Ok(npub) = keys.public_key.to_bech32();
|
|
||||||
let Ok(nsec) = keys.secret_key().to_bech32();
|
|
||||||
|
|
||||||
let pubkey_input = cx.new(|cx| {
|
|
||||||
InputState::new(window, cx)
|
|
||||||
.disabled(true)
|
|
||||||
.default_value(npub)
|
|
||||||
});
|
|
||||||
|
|
||||||
let secret_input = cx.new(|cx| {
|
|
||||||
InputState::new(window, cx)
|
|
||||||
.disabled(true)
|
|
||||||
.default_value(nsec)
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
pubkey_input,
|
|
||||||
secret_input,
|
|
||||||
error: None,
|
|
||||||
copied: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn backup(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Task<()>> {
|
|
||||||
let dir = home_dir();
|
|
||||||
let path = cx.prompt_for_new_path(dir, Some("My Nostr Account"));
|
|
||||||
let nsec = self.secret_input.read(cx).value().to_string();
|
|
||||||
|
|
||||||
Some(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match Flatten::flatten(path.await.map_err(|e| e.into())) {
|
|
||||||
Ok(Ok(Some(path))) => {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
if let Err(e) = fs::write(&path, nsec) {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_error(e.to_string(), window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::error!("Failed to save backup keys");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn copy_secret(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let item = ClipboardItem::new_string(self.secret_input.read(cx).value().to_string());
|
|
||||||
cx.write_to_clipboard(item);
|
|
||||||
|
|
||||||
self.set_copied(true, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_copied(&mut self, status: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.copied = status;
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
// Reset the copied state after a delay
|
|
||||||
if status {
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_copied(false, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
E: Into<SharedString>,
|
|
||||||
{
|
|
||||||
self.error = Some(error.into());
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
// Clear the error message after a delay
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.error = None;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for BackupKeys {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.gap_3()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(shared_t!("new_account.backup_description")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(shared_t!("common.pubkey"))
|
|
||||||
.child(TextInput::new(&self.pubkey_input).small())
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(shared_t!("new_account.backup_pubkey_note")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(divider(cx))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(shared_t!("common.secret"))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(TextInput::new(&self.secret_input).small())
|
|
||||||
.child(
|
|
||||||
Button::new("copy")
|
|
||||||
.icon({
|
|
||||||
if self.copied {
|
|
||||||
IconName::CheckCircleFill
|
|
||||||
} else {
|
|
||||||
IconName::Copy
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ghost()
|
|
||||||
.disabled(self.copied)
|
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
|
||||||
this.copy_secret(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(shared_t!("new_account.backup_secret_note")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
pub mod backup_keys;
|
|
||||||
pub mod compose;
|
pub mod compose;
|
||||||
pub mod edit_profile;
|
pub mod edit_profile;
|
||||||
pub mod login;
|
|
||||||
pub mod new_account;
|
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use list_item::RoomListItem;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::NostrRegistry;
|
use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
@@ -535,7 +535,9 @@ impl Sidebar {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let task: Task<Result<Vec<Relay>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Vec<Relay>, Error>> = cx.background_spawn(async move {
|
||||||
let subscription = client.subscription(&SubscriptionId::new("inbox")).await;
|
let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
|
||||||
|
let subscription = client.subscription(&id).await;
|
||||||
|
|
||||||
let mut relays: Vec<Relay> = vec![];
|
let mut relays: Vec<Relay> = vec![];
|
||||||
|
|
||||||
for (url, _filter) in subscription.into_iter() {
|
for (url, _filter) in subscription.into_iter() {
|
||||||
@@ -812,6 +814,7 @@ impl Render for Sidebar {
|
|||||||
this.child(deferred(
|
this.child(deferred(
|
||||||
v_flex()
|
v_flex()
|
||||||
.py_2()
|
.py_2()
|
||||||
|
.px_1p5()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
@@ -835,6 +838,7 @@ impl Render for Sidebar {
|
|||||||
this.child(deferred(
|
this.child(deferred(
|
||||||
v_flex()
|
v_flex()
|
||||||
.py_2()
|
.py_2()
|
||||||
|
.px_1p5()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ impl Startup {
|
|||||||
Self {
|
Self {
|
||||||
credential,
|
credential,
|
||||||
loading: false,
|
loading: false,
|
||||||
name: "Account".into(),
|
name: "Continue".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
image_cache: RetainAllImageCache::new(cx),
|
image_cache: RetainAllImageCache::new(cx),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ impl Gossip {
|
|||||||
relays
|
relays
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(url, metadata)| {
|
.filter_map(|(url, metadata)| {
|
||||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
|
||||||
Some(url.to_owned())
|
Some(url.to_owned())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -97,7 +97,7 @@ impl Gossip {
|
|||||||
relays
|
relays
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(url, metadata)| {
|
.filter_map(|(url, metadata)| {
|
||||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
|
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
||||||
Some(url.to_owned())
|
Some(url.to_owned())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ impl Render for TitleBar {
|
|||||||
if window.is_fullscreen() {
|
if window.is_fullscreen() {
|
||||||
this.px_2()
|
this.px_2()
|
||||||
} else if cx.theme().platform_kind.is_mac() {
|
} else if cx.theme().platform_kind.is_mac() {
|
||||||
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING)).pr_2()
|
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
||||||
|
.pr_2()
|
||||||
|
.when(children.len() <= 1, |this| {
|
||||||
|
this.pr(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.px_2()
|
this.px_2()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -522,7 +522,7 @@ impl InputState {
|
|||||||
if new_row >= last_layout.visible_range.start {
|
if new_row >= last_layout.visible_range.start {
|
||||||
let visible_row = new_row.saturating_sub(last_layout.visible_range.start);
|
let visible_row = new_row.saturating_sub(last_layout.visible_range.start);
|
||||||
if let Some(line) = last_layout.lines.get(visible_row) {
|
if let Some(line) = last_layout.lines.get(visible_row) {
|
||||||
if let Ok(x) = line.index_for_position(
|
if let Ok(x) = line.closest_index_for_position(
|
||||||
Point {
|
Point {
|
||||||
x: preferred_x,
|
x: preferred_x,
|
||||||
y: px(0.),
|
y: px(0.),
|
||||||
@@ -1655,10 +1655,11 @@ impl InputState {
|
|||||||
|
|
||||||
// Return offset by use closest_index_for_x if is single line mode.
|
// Return offset by use closest_index_for_x if is single line mode.
|
||||||
if self.mode.is_single_line() {
|
if self.mode.is_single_line() {
|
||||||
return rendered_line.unwrapped_layout.index_for_x(pos.x).unwrap();
|
return rendered_line.unwrapped_layout.closest_index_for_x(pos.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
let index_result = rendered_line.index_for_position(pos, line_height);
|
let index_result = rendered_line.closest_index_for_position(pos, line_height);
|
||||||
|
|
||||||
if let Ok(v) = index_result {
|
if let Ok(v) = index_result {
|
||||||
index += v;
|
index += v;
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user