feat: revamp the onboarding process (#205)

* redesign

* restructure

* .

* .

* .

* .

* .
This commit is contained in:
reya
2025-11-17 15:10:14 +07:00
committed by GitHub
parent 67c92cb319
commit 6023063cf4
12 changed files with 859 additions and 796 deletions

View File

@@ -30,13 +30,12 @@ use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::modal::ModalButtonProps;
use ui::popover::{Popover, PopoverContent};
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::views::compose::compose_button;
use crate::views::{
login, new_account, onboarding, preferences, sidebar, startup, user_profile, welcome,
};
use crate::views::{onboarding, preferences, sidebar, startup, user_profile, welcome};
use crate::{login, new_identity};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
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) {
let panel = new_account::init(window, cx);
let panel = new_identity::init(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 {
@@ -505,10 +541,16 @@ impl Render for ChatSpace {
let left = self.titlebar_left(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
self.title_bar.update(cx, |this, _cx| {
if single_panel {
this.set_children(vec![center]);
} else {
this.set_children(vec![left, right]);
}
});
div()

View File

@@ -22,9 +22,10 @@ use ui::{v_flex, ContextModal, Disableable, StyledExt};
use crate::actions::CoopAuthUrlHandler;
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 {
key_input: Entity<InputState>,
pass_input: Entity<InputState>,
@@ -42,11 +43,7 @@ pub struct Login {
}
impl Login {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::view(window, cx))
}
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let key_input = cx.new(|cx| InputState::new(window, cx));
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
@@ -78,7 +75,7 @@ impl Login {
pass_input,
error,
countdown,
name: "Login".into(),
name: "Welcome Back".into(),
focus_handle: cx.focus_handle(),
logging_in: false,
require_password: false,
@@ -370,18 +367,10 @@ impl Render for Login {
.child(
div()
.text_center()
.child(
div()
.text_xl()
.font_semibold()
.line_height(relative(1.3))
.child(shared_t!("login.title")),
)
.child(
div()
.text_color(cx.theme().text_muted)
.child(shared_t!("login.key_description")),
),
.child(SharedString::from("Continue with Private Key or Bunker")),
)
.child(
v_flex()

View File

@@ -11,9 +11,11 @@ use ui::Root;
use crate::actions::{load_embedded_fonts, quit, Quit};
pub(crate) mod actions;
pub(crate) mod chatspace;
pub(crate) mod views;
mod actions;
mod chatspace;
mod login;
mod new_identity;
mod views;
i18n::init!();

View 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)),
)
}
}

View File

@@ -1,9 +1,9 @@
use anyhow::{anyhow, Error};
use common::{default_nip17_relays, default_nip65_relays, nip96_upload, BOOTSTRAP_RELAYS};
use gpui::{
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
Render, SharedString, Styled, Task, WeakEntity, Window,
rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten, FocusHandle,
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
Window,
};
use gpui_tokio::Tokio;
use i18n::{shared_t, t};
@@ -12,20 +12,20 @@ use nostr_sdk::prelude::*;
use settings::AppSettings;
use smol::fs;
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputState, TextInput};
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> {
NewAccount::new(window, cx)
cx.new(|cx| NewAccount::new(window, cx))
}
#[derive(Debug)]
pub struct NewAccount {
name_input: Entity<InputState>,
avatar_input: Entity<InputState>,
@@ -38,11 +38,7 @@ pub struct NewAccount {
}
impl NewAccount {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::view(window, cx))
}
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let temp_keys = cx.new(|_| Keys::generate());
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
let avatar_input = cx.new(|cx| InputState::new(window, cx));
@@ -53,7 +49,7 @@ impl NewAccount {
temp_keys,
uploading: false,
submitting: false,
name: "New Account".into(),
name: "Create a new identity".into(),
focus_handle: cx.focus_handle(),
}
}
@@ -62,7 +58,7 @@ impl NewAccount {
self.submitting(true, cx);
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 current_view = cx.entity().downgrade();
@@ -80,20 +76,25 @@ impl NewAccount {
.on_ok(move |_, window, cx| {
weak_view
.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 |_, cx| {
task.await;
cx.spawn_in(window, async move |_this, cx| {
let result = task.await;
current_view
.update(cx, |this, cx| {
this.set_signer(cx);
match result {
Ok(_) => {
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();
}
})
.ok();
// 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 nostr = NostrRegistry::global(cx);
@@ -120,24 +121,14 @@ impl NewAccount {
metadata = metadata.picture(url);
};
cx.spawn(async move |_, cx| {
let url = KeyItem::User.to_string();
// Close all modals if available
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
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
// Set the client's signer with the current keys
client.set_signer(keys).await;
// Verify the signer
let signer = client.signer().await?;
let signer = keys.clone();
let nip65_relays = default_nip65_relays();
let nip17_relays = default_nip17_relays();
// Construct a NIP-65 event
let event = EventBuilder::new(Kind::RelayList, "")
@@ -153,12 +144,6 @@ impl NewAccount {
// Set NIP-65 relays
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
let write_relays: Vec<RelayUrl> = nip65_relays
.iter()
@@ -169,12 +154,17 @@ impl NewAccount {
None
}
})
.take(3)
.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
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)
.await?;
@@ -184,13 +174,32 @@ impl NewAccount {
// Construct a metadata event
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(BOOTSTRAP_RELAYS, &event).await?;
// Update the client's signer with the current keys
client.set_signer(keys).await;
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();
}
@@ -230,39 +239,31 @@ impl NewAccount {
});
cx.spawn_in(window, async move |this, cx| {
match Flatten::flatten(task.await.map_err(|e| e.into())) {
Ok(Ok(url)) => {
let result = Flatten::flatten(task.await.map_err(|e| e.into()));
this.update_in(cx, |this, window, cx| {
match result {
Ok(Ok(url)) => {
this.uploading(false, cx);
this.avatar_input.update(cx, |this, cx| {
this.set_value(url.to_string(), window, cx);
});
})
.ok();
}
Ok(Err(e)) => {
Self::notify_error(cx, this, e.to_string());
window.push_notification(e.to_string(), cx);
this.uploading(false, cx);
}
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();
}
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>) {
self.submitting = status;
cx.notify();
@@ -294,64 +295,48 @@ impl Focusable for NewAccount {
impl Render for NewAccount {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let avatar = self.avatar_input.read(cx).value();
v_flex()
.size_full()
.relative()
.items_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(
v_flex()
.w_96()
.gap_2()
.child(
v_flex()
.h_40()
.w_full()
.items_center()
.justify_center()
.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(
v_flex()
.gap_1()
.text_sm()
.child(shared_t!("new_account.name"))
.child(TextInput::new(&self.name_input).small()),
)
.child(
v_flex()
.gap_1()
.child(div().text_sm().child(shared_t!("new_account.avatar")))
.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);
})),
),
TextInput::new(&self.name_input)
.disabled(self.submitting)
.small(),
),
)
.child(divider(cx))

View File

@@ -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")),
),
)
}
}

View File

@@ -1,8 +1,5 @@
pub mod backup_keys;
pub mod compose;
pub mod edit_profile;
pub mod login;
pub mod new_account;
pub mod onboarding;
pub mod preferences;
pub mod screening;

View File

@@ -18,7 +18,7 @@ use list_item::RoomListItem;
use nostr_sdk::prelude::*;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION};
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
@@ -535,7 +535,9 @@ impl Sidebar {
let client = nostr.read(cx).client();
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![];
for (url, _filter) in subscription.into_iter() {
@@ -812,6 +814,7 @@ impl Render for Sidebar {
this.child(deferred(
v_flex()
.py_2()
.px_1p5()
.gap_1p5()
.items_center()
.justify_center()
@@ -835,6 +838,7 @@ impl Render for Sidebar {
this.child(deferred(
v_flex()
.py_2()
.px_1p5()
.gap_1p5()
.items_center()
.justify_center()

View File

@@ -61,7 +61,7 @@ impl Startup {
Self {
credential,
loading: false,
name: "Account".into(),
name: "Continue".into(),
focus_handle: cx.focus_handle(),
image_cache: RetainAllImageCache::new(cx),
_subscriptions: subscriptions,

View File

@@ -78,7 +78,7 @@ impl Gossip {
relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
Some(url.to_owned())
} else {
None
@@ -97,7 +97,7 @@ impl Gossip {
relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
Some(url.to_owned())
} else {
None

View File

@@ -91,7 +91,11 @@ impl Render for TitleBar {
if window.is_fullscreen() {
this.px_2()
} 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 {
this.px_2()
}

View File

@@ -522,7 +522,7 @@ impl InputState {
if new_row >= 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 Ok(x) = line.index_for_position(
if let Ok(x) = line.closest_index_for_position(
Point {
x: preferred_x,
y: px(0.),
@@ -1655,10 +1655,11 @@ impl InputState {
// Return offset by use closest_index_for_x if is single line mode.
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 {
index += v;
break;