chore: Refine the UI (#102)
* update deps * update window options * linux title bar * fix build * . * fix build * rounded corners on linux * . * . * fix i18n key * fix change subject modal * . * update new account * . * update relay modal * . * fix i18n keys --------- Co-authored-by: reya <reya@macbook.local>
This commit is contained in:
@@ -11,6 +11,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
ui = { path = "../ui" }
|
||||
title_bar = { path = "../title_bar" }
|
||||
identity = { path = "../identity" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Error;
|
||||
use client_keys::ClientKeys;
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
||||
use global::nostr_client;
|
||||
use common::display::DisplayProfile;
|
||||
use global::constants::DEFAULT_SIDEBAR_WIDTH;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, Action, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window,
|
||||
actions, div, px, rems, Action, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::{Registry, RoomEmitter};
|
||||
use serde::Deserialize;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||
use title_bar::TitleBar;
|
||||
use ui::actions::OpenProfile;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::PanelView;
|
||||
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{ContextModal, IconName, Root, Sizable, StyledExt, TitleBar};
|
||||
use ui::popup_menu::PopupMenuExt;
|
||||
use ui::{h_flex, ContextModal, IconName, Root, Sizable, StyledExt};
|
||||
|
||||
use crate::views::compose::compose_button;
|
||||
use crate::views::screening::Screening;
|
||||
use crate::views::user_profile::UserProfile;
|
||||
use crate::views::{
|
||||
chat, login, new_account, onboarding, preferences, sidebar, startup, user_profile, welcome,
|
||||
backup_keys, chat, login, messaging_relays, new_account, onboarding, preferences, sidebar,
|
||||
startup, user_profile, welcome,
|
||||
};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||
@@ -44,6 +50,8 @@ pub fn new_account(window: &mut Window, cx: &mut App) {
|
||||
ChatSpace::set_center_panel(panel, window, cx);
|
||||
}
|
||||
|
||||
actions!(user, [DarkMode, Settings, Logout]);
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum PanelKind {
|
||||
Room(u64),
|
||||
@@ -70,14 +78,15 @@ pub struct ToggleModal {
|
||||
}
|
||||
|
||||
pub struct ChatSpace {
|
||||
title_bar: Entity<TitleBar>,
|
||||
dock: Entity<DockArea>,
|
||||
toolbar: bool,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 5]>,
|
||||
}
|
||||
|
||||
impl ChatSpace {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let title_bar = cx.new(|_| TitleBar::new());
|
||||
let dock = cx.new(|cx| {
|
||||
let panel = Arc::new(startup::init(window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
@@ -99,14 +108,17 @@ impl ChatSpace {
|
||||
window,
|
||||
|_this: &mut Self, state, window, cx| {
|
||||
if !state.read(cx).has_keys() {
|
||||
window.open_modal(cx, |this, _window, cx| {
|
||||
let title = SharedString::new(t!("startup.client_keys_warning"));
|
||||
let desc = SharedString::new(t!("startup.client_keys_desc"));
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.button_props(
|
||||
ModalButtonProps::default()
|
||||
.cancel_text(t!("chatspace.create_new_keys"))
|
||||
.cancel_text(t!("startup.create_new_keys"))
|
||||
.ok_text(t!("common.allow")),
|
||||
)
|
||||
.child(
|
||||
@@ -124,13 +136,9 @@ impl ChatSpace {
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("chatspace.warning"))),
|
||||
.child(title.clone()),
|
||||
)
|
||||
.child(div().line_height(relative(1.4)).child(
|
||||
SharedString::new(t!(
|
||||
"chatspace.allow_keychain_access"
|
||||
)),
|
||||
)),
|
||||
.child(desc.clone()),
|
||||
)
|
||||
.on_cancel(|_, _window, cx| {
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
@@ -157,21 +165,21 @@ impl ChatSpace {
|
||||
window,
|
||||
|this: &mut Self, state, window, cx| {
|
||||
if !state.read(cx).has_signer() {
|
||||
this.open_onboarding(window, cx);
|
||||
this.set_onboarding_panels(window, cx);
|
||||
} else {
|
||||
this.open_chats(window, cx);
|
||||
this.set_chat_panels(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
// Automatically run on_load function when UserProfile is created
|
||||
// Automatically run load function when UserProfile is created
|
||||
subscriptions.push(cx.observe_new::<UserProfile>(|this, window, cx| {
|
||||
if let Some(window) = window {
|
||||
this.on_load(window, cx);
|
||||
this.load(window, cx);
|
||||
}
|
||||
}));
|
||||
|
||||
// Automatically run on_load function when Screening is created
|
||||
// Automatically run load function when Screening is created
|
||||
subscriptions.push(cx.observe_new::<Screening>(|this, window, cx| {
|
||||
if let Some(window) = window {
|
||||
this.load(window, cx);
|
||||
@@ -196,7 +204,7 @@ impl ChatSpace {
|
||||
this.add_panel(panel, DockPlacement::Center, window, cx);
|
||||
});
|
||||
} else {
|
||||
window.push_notification(t!("chatspace.failed_to_open_room"), cx);
|
||||
window.push_notification(t!("common.room_error"), cx);
|
||||
}
|
||||
}
|
||||
RoomEmitter::Close(..) => {
|
||||
@@ -216,16 +224,13 @@ impl ChatSpace {
|
||||
|
||||
Self {
|
||||
dock,
|
||||
title_bar,
|
||||
subscriptions,
|
||||
toolbar: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// No active user, disable user's toolbar
|
||||
self.toolbar(false, cx);
|
||||
|
||||
pub fn set_onboarding_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let panel = Arc::new(onboarding::init(window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
@@ -235,17 +240,14 @@ impl ChatSpace {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Enable the toolbar for logged in users
|
||||
self.toolbar(true, cx);
|
||||
|
||||
// Load all chat rooms from database
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
|
||||
pub fn set_chat_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = Registry::global(cx);
|
||||
let weak_dock = self.dock.downgrade();
|
||||
|
||||
// The left panel will render sidebar
|
||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||
|
||||
// The center panel will render chat rooms (as tabs)
|
||||
let center = DockItem::split_with_sizes(
|
||||
Axis::Vertical,
|
||||
vec![DockItem::tabs(
|
||||
@@ -261,66 +263,31 @@ impl ChatSpace {
|
||||
cx,
|
||||
);
|
||||
|
||||
// Update dock
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
let verify_messaging_relays = this.verify_messaging_relays(cx);
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if let Ok(status) = verify_messaging_relays.await {
|
||||
if !status {
|
||||
cx.update(|window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::SetupRelay,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
// Load all chat rooms from the database
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let settings = preferences::init(window, cx);
|
||||
let title = SharedString::new(t!("chatspace.preferences_title"));
|
||||
pub fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let view = preferences::init(window, cx);
|
||||
let title = SharedString::new(t!("common.preferences"));
|
||||
|
||||
window.open_modal(cx, move |modal, _, _| {
|
||||
modal
|
||||
.title(title.clone())
|
||||
.width(px(DEFAULT_MODAL_WIDTH))
|
||||
.child(settings.clone())
|
||||
.width(px(480.))
|
||||
.child(view.clone())
|
||||
});
|
||||
}
|
||||
|
||||
fn toolbar(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.toolbar = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
let is_exist = client.database().query(filter).await?.first().is_some();
|
||||
|
||||
Ok(is_exist)
|
||||
})
|
||||
}
|
||||
|
||||
fn toggle_appearance(&self, window: &mut Window, cx: &mut App) {
|
||||
fn on_dark_mode(&mut self, _ev: &DarkMode, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if cx.theme().mode.is_dark() {
|
||||
Theme::change(ThemeMode::Light, Some(window), cx);
|
||||
} else {
|
||||
@@ -328,23 +295,80 @@ impl ChatSpace {
|
||||
}
|
||||
}
|
||||
|
||||
fn logout(&self, window: &mut Window, cx: &mut App) {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
fn on_sign_out(&mut self, _ev: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let identity = Identity::global(cx);
|
||||
// TODO: save current session?
|
||||
identity.update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_open_profile(&mut self, a: &OpenProfile, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let public_key = a.0;
|
||||
fn on_open_profile(&mut self, ev: &OpenProfile, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let public_key = ev.0;
|
||||
let profile = user_profile::init(public_key, window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
// user_profile::init(public_key, window, cx)
|
||||
this.child(profile.clone())
|
||||
this.alert()
|
||||
.show_close(true)
|
||||
.overlay_closable(true)
|
||||
.child(profile.clone())
|
||||
.button_props(ModalButtonProps::default().ok_text(t!("profile.njump")))
|
||||
.on_ok(move |_, _window, cx| {
|
||||
let Ok(bech32) = public_key.to_bech32();
|
||||
cx.open_url(&format!("https://njump.me/{bech32}"));
|
||||
false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_center_panel<P: PanelView>(panel: P, window: &mut Window, cx: &mut App) {
|
||||
fn render_titlebar_left_side(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let compose_button = compose_button().into_any_element();
|
||||
|
||||
h_flex().gap_1().child(compose_button)
|
||||
}
|
||||
|
||||
fn render_titlebar_right_side(
|
||||
&mut self,
|
||||
profile: &Profile,
|
||||
_window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||
let need_backup = Identity::read_global(cx).need_backup();
|
||||
let relay_ready = Identity::read_global(cx).relay_ready();
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when_some(relay_ready, |this, status| {
|
||||
this.when(!status, |this| this.child(messaging_relays::relay_button()))
|
||||
})
|
||||
.when_some(need_backup, |this, keys| {
|
||||
this.child(backup_keys::backup_button(keys.to_owned()))
|
||||
})
|
||||
.child(
|
||||
Button::new("user")
|
||||
.small()
|
||||
.reverse()
|
||||
.transparent()
|
||||
.icon(IconName::CaretDown)
|
||||
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.49)))
|
||||
.popup_menu(|this, _window, _cx| {
|
||||
this.menu(t!("user.dark_mode"), Box::new(DarkMode))
|
||||
.menu(t!("user.settings"), Box::new(Settings))
|
||||
.separator()
|
||||
.menu(t!("user.sign_out"), Box::new(Logout))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn set_center_panel<P>(panel: P, window: &mut Window, cx: &mut App)
|
||||
where
|
||||
P: PanelView,
|
||||
{
|
||||
if let Some(Some(root)) = window.root::<Root>() {
|
||||
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
||||
let panel = Arc::new(panel);
|
||||
@@ -365,7 +389,27 @@ impl Render for ChatSpace {
|
||||
let modal_layer = Root::render_modal_layer(window, cx);
|
||||
let notification_layer = Root::render_notification_layer(window, cx);
|
||||
|
||||
// Only render titlebar element if user is logged in
|
||||
if let Some(identity) = Identity::read_global(cx).public_key() {
|
||||
let profile = Registry::read_global(cx).get_person(&identity, cx);
|
||||
|
||||
let left_side = self
|
||||
.render_titlebar_left_side(window, cx)
|
||||
.into_any_element();
|
||||
|
||||
let right_side = self
|
||||
.render_titlebar_right_side(&profile, window, cx)
|
||||
.into_any_element();
|
||||
|
||||
self.title_bar.update(cx, |this, _cx| {
|
||||
this.set_children(vec![left_side, right_side]);
|
||||
})
|
||||
}
|
||||
|
||||
div()
|
||||
.on_action(cx.listener(Self::on_settings))
|
||||
.on_action(cx.listener(Self::on_dark_mode))
|
||||
.on_action(cx.listener(Self::on_sign_out))
|
||||
.on_action(cx.listener(Self::on_open_profile))
|
||||
.relative()
|
||||
.size_full()
|
||||
@@ -375,58 +419,7 @@ impl Render for ChatSpace {
|
||||
.flex_col()
|
||||
.size_full()
|
||||
// Title Bar
|
||||
.child(
|
||||
TitleBar::new()
|
||||
// Left side
|
||||
.child(div())
|
||||
// Right side
|
||||
.when(self.toolbar, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap_1p5()
|
||||
.px_2()
|
||||
.child(
|
||||
Button::new("appearance")
|
||||
.tooltip(t!("chatspace.appearance_tooltip"))
|
||||
.small()
|
||||
.ghost()
|
||||
.map(|this| {
|
||||
if cx.theme().mode.is_dark() {
|
||||
this.icon(IconName::Sun)
|
||||
} else {
|
||||
this.icon(IconName::Moon)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_appearance(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("preferences")
|
||||
.tooltip(t!("chatspace.preferences_tooltip"))
|
||||
.small()
|
||||
.ghost()
|
||||
.icon(IconName::Settings)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_settings(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("logout")
|
||||
.tooltip(t!("common.logout"))
|
||||
.small()
|
||||
.ghost()
|
||||
.icon(IconName::Logout)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.logout(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(self.title_bar.clone())
|
||||
// Dock
|
||||
.child(self.dock.clone()),
|
||||
)
|
||||
|
||||
@@ -5,21 +5,16 @@ use std::time::Duration;
|
||||
use anyhow::{anyhow, Error};
|
||||
use assets::Assets;
|
||||
use auto_update::AutoUpdater;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use global::constants::APP_NAME;
|
||||
use global::constants::{
|
||||
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
|
||||
ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
|
||||
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
|
||||
};
|
||||
use global::{nostr_client, NostrSignal};
|
||||
use gpui::{
|
||||
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
WindowBounds, WindowKind, WindowOptions,
|
||||
actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
SharedString, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
|
||||
WindowKind, WindowOptions,
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use gpui::{point, SharedString, TitlebarOptions};
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
||||
use identity::Identity;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::Registry;
|
||||
@@ -138,7 +133,7 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let duration = smol::Timer::after(Duration::from_secs(75));
|
||||
let duration = smol::Timer::after(Duration::from_secs(30));
|
||||
|
||||
let recv = || async {
|
||||
// prevent inline format
|
||||
@@ -202,25 +197,22 @@ fn main() {
|
||||
items: vec![MenuItem::action("Quit", Quit)],
|
||||
}]);
|
||||
|
||||
// Set up the window bounds
|
||||
let bounds = Bounds::centered(None, size(px(920.0), px(700.0)), cx);
|
||||
|
||||
// Set up the window options
|
||||
let opts = WindowOptions {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
window_background: WindowBackgroundAppearance::Opaque,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
window_min_size: Some(size(px(800.0), px(600.0))),
|
||||
kind: WindowKind::Normal,
|
||||
app_id: Some(APP_ID.to_owned()),
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some(SharedString::new_static(APP_NAME)),
|
||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||
appears_transparent: true,
|
||||
}),
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(920.0), px(700.0)),
|
||||
cx,
|
||||
))),
|
||||
#[cfg(target_os = "linux")]
|
||||
window_background: WindowBackgroundAppearance::Transparent,
|
||||
#[cfg(target_os = "linux")]
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
kind: WindowKind::Normal,
|
||||
app_id: Some(APP_ID.to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
258
crates/coop/src/views/backup_keys.rs
Normal file
258
crates/coop/src/views/backup_keys.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
use dirs::document_dir;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use identity::Identity;
|
||||
use nostr_sdk::prelude::*;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{divider, h_flex, v_flex, ContextModal, Disableable, IconName, Sizable};
|
||||
|
||||
pub fn backup_button(keys: Keys) -> impl IntoElement {
|
||||
div().child(
|
||||
Button::new("backup")
|
||||
.icon(IconName::Info)
|
||||
.label(t!("new_account.backup_label"))
|
||||
.danger()
|
||||
.xsmall()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.on_click(move |_, window, cx| {
|
||||
let title = SharedString::new(t!("new_account.backup_label"));
|
||||
let keys = keys.clone();
|
||||
let view = cx.new(|cx| BackupKeys::new(&keys, window, cx));
|
||||
let weak_view = view.downgrade();
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
let weak_view = weak_view.clone();
|
||||
|
||||
modal
|
||||
.confirm()
|
||||
.title(title.clone())
|
||||
.child(view.clone())
|
||||
.button_props(
|
||||
ModalButtonProps::default()
|
||||
.cancel_text(t!("new_account.backup_skip"))
|
||||
.ok_text(t!("new_account.backup_download")),
|
||||
)
|
||||
.on_ok(move |_, window, cx| {
|
||||
weak_view
|
||||
.update(cx, |this, cx| {
|
||||
this.download(window, cx);
|
||||
})
|
||||
.ok();
|
||||
// true to close the modal
|
||||
false
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub struct BackupKeys {
|
||||
password: Entity<InputState>,
|
||||
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 password = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
|
||||
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 {
|
||||
password,
|
||||
pubkey_input,
|
||||
secret_input,
|
||||
error: None,
|
||||
copied: false,
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
fn download(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let document_dir = document_dir().expect("Failed to get document directory");
|
||||
let password = self.password.read(cx).value().to_string();
|
||||
|
||||
if password.is_empty() {
|
||||
self.set_error(t!("login.password_is_required"), window, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let path = cx.prompt_for_new_path(&document_dir);
|
||||
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))) => {
|
||||
cx.update(|window, cx| {
|
||||
match fs::write(&path, nsec) {
|
||||
Ok(_) => {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.clear_need_backup(password, cx);
|
||||
});
|
||||
// Close the current modal
|
||||
window.close_modal(cx);
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(e.to_string(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {
|
||||
log::error!("Failed to save backup keys");
|
||||
}
|
||||
};
|
||||
})
|
||||
.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")),
|
||||
),
|
||||
)
|
||||
.child(divider(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(shared_t!("login.set_password"))
|
||||
.child(TextInput::new(&self.password).small())
|
||||
.when_some(self.error.as_ref(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::emoji_picker::EmojiPicker;
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::text::RichText;
|
||||
@@ -813,7 +814,7 @@ impl Panel for Chat {
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, cx: &App) -> Vec<Button> {
|
||||
let id = self.room.read(cx).id;
|
||||
let room = self.room.downgrade();
|
||||
let subject = self
|
||||
.room
|
||||
.read(cx)
|
||||
@@ -825,11 +826,31 @@ impl Panel for Chat {
|
||||
.icon(IconName::EditFill)
|
||||
.tooltip(t!("chat.change_subject_button"))
|
||||
.on_click(move |_, window, cx| {
|
||||
let subject = subject::init(id, subject.clone(), window, cx);
|
||||
let view = subject::init(subject.clone(), window, cx);
|
||||
let room = room.clone();
|
||||
let weak_view = view.downgrade();
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
this.title(SharedString::new(t!("chat.change_subject_modal_title")))
|
||||
.child(subject.clone())
|
||||
let room = room.clone();
|
||||
let weak_view = weak_view.clone();
|
||||
|
||||
this.confirm()
|
||||
.title(SharedString::new(t!("chat.change_subject_modal_title")))
|
||||
.child(view.clone())
|
||||
.button_props(ModalButtonProps::default().ok_text(t!("common.change")))
|
||||
.on_ok(move |_, _window, cx| {
|
||||
if let Ok(subject) =
|
||||
weak_view.read_with(cx, |this, cx| this.new_subject(cx))
|
||||
{
|
||||
room.update(cx, |this, cx| {
|
||||
this.subject = Some(subject);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
// true to close the modal
|
||||
true
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -891,8 +912,9 @@ impl Render for Chat {
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.icon(Icon::new(IconName::Upload))
|
||||
.icon(IconName::Upload)
|
||||
.ghost()
|
||||
.large()
|
||||
.disabled(self.uploading)
|
||||
.loading(self.uploading)
|
||||
.on_click(cx.listener(
|
||||
@@ -903,7 +925,8 @@ impl Render for Chat {
|
||||
)
|
||||
.child(
|
||||
EmojiPicker::new(self.input.downgrade())
|
||||
.icon(IconName::EmojiFill),
|
||||
.icon(IconName::EmojiFill)
|
||||
.large(),
|
||||
),
|
||||
)
|
||||
.child(TextInput::new(&self.input)),
|
||||
|
||||
@@ -3,92 +3,53 @@ use gpui::{
|
||||
Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use registry::Registry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Sizable};
|
||||
use ui::{v_flex, Sizable};
|
||||
|
||||
pub fn init(
|
||||
id: u64,
|
||||
subject: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Subject> {
|
||||
Subject::new(id, subject, window, cx)
|
||||
pub fn init(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
|
||||
Subject::new(subject, window, cx)
|
||||
}
|
||||
|
||||
pub struct Subject {
|
||||
id: u64,
|
||||
input: Entity<InputState>,
|
||||
}
|
||||
|
||||
impl Subject {
|
||||
pub fn new(
|
||||
id: u64,
|
||||
subject: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let input = cx.new(|cx| {
|
||||
let mut this = InputState::new(window, cx).placeholder(t!("subject.placeholder"));
|
||||
if let Some(text) = subject.clone() {
|
||||
if let Some(text) = subject.as_ref() {
|
||||
this.set_value(text, window, cx);
|
||||
}
|
||||
this
|
||||
});
|
||||
|
||||
cx.new(|_| Self { id, input })
|
||||
cx.new(|_| Self { input })
|
||||
}
|
||||
|
||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = Registry::global(cx).read(cx);
|
||||
let subject = self.input.read(cx).value().clone();
|
||||
|
||||
if let Some(room) = registry.room(&self.id, cx) {
|
||||
room.update(cx, |this, cx| {
|
||||
this.subject = Some(subject);
|
||||
cx.notify();
|
||||
});
|
||||
window.close_modal(cx);
|
||||
} else {
|
||||
window.push_notification(SharedString::new(t!("subject.room_not_found")), cx);
|
||||
}
|
||||
pub fn new_subject(&self, cx: &App) -> SharedString {
|
||||
self.input.read(cx).value().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Subject {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("subject.title"))),
|
||||
)
|
||||
.child(TextInput::new(&self.input).small())
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(SharedString::new(t!("subject.help_text"))),
|
||||
),
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("subject.title"))),
|
||||
)
|
||||
.child(TextInput::new(&self.input).small())
|
||||
.child(
|
||||
Button::new("submit")
|
||||
.label(t!("common.change"))
|
||||
.primary()
|
||||
.w_full()
|
||||
.on_click(cx.listener(|this, _, window, cx| this.update(window, cx))),
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(SharedString::new(t!("subject.help_text"))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ use global::constants::BOOTSTRAP_RELAYS;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, px, red, relative, uniform_list, App, AppContext, Context, Entity,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
|
||||
div, px, relative, rems, uniform_list, AppContext, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
|
||||
Subscription, Task, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use itertools::Itertools;
|
||||
@@ -21,13 +21,29 @@ use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::{v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
||||
cx.new(|cx| Compose::new(window, cx))
|
||||
pub fn compose_button() -> impl IntoElement {
|
||||
div().child(
|
||||
Button::new("compose")
|
||||
.icon(IconName::Plus)
|
||||
.ghost_alt()
|
||||
.cta()
|
||||
.small()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.on_click(move |_, window, cx| {
|
||||
let compose = cx.new(|cx| Compose::new(window, cx));
|
||||
let title = SharedString::new(t!("sidebar.direct_messages"));
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
modal.title(title.clone()).child(compose.clone())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -147,13 +163,13 @@ impl Compose {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let public_keys: Vec<PublicKey> = self.selected(cx);
|
||||
|
||||
if public_keys.is_empty() {
|
||||
self.set_error(Some(t!("compose.receiver_required").into()), cx);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading spinner
|
||||
self.set_submitting(true, cx);
|
||||
@@ -169,7 +185,7 @@ impl Compose {
|
||||
));
|
||||
}
|
||||
|
||||
let event: Task<Result<Room, anyhow::Error>> = cx.background_spawn(async move {
|
||||
let event: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
||||
let signer = nostr_client().signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -187,15 +203,17 @@ impl Compose {
|
||||
match event.await {
|
||||
Ok(room) => {
|
||||
cx.update(|window, cx| {
|
||||
let registry = Registry::global(cx);
|
||||
// Reset local state
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
// Create and insert the new room into the registry
|
||||
registry.update(cx, |this, cx| {
|
||||
this.push_room(cx.new(|_| room), cx);
|
||||
});
|
||||
|
||||
// Close the current modal
|
||||
window.close_modal(cx);
|
||||
})
|
||||
.ok();
|
||||
@@ -371,22 +389,20 @@ impl Compose {
|
||||
let selected = entity.read(cx).select;
|
||||
|
||||
items.push(
|
||||
div()
|
||||
h_flex()
|
||||
.id(ix)
|
||||
.px_1()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.h_11()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(cx.theme().radius)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.gap_1p5()
|
||||
.text_sm()
|
||||
.child(img(profile.avatar_url(proxy)).size_7().flex_shrink_0())
|
||||
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.75)))
|
||||
.child(profile.display_name()),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
@@ -414,51 +430,52 @@ impl Render for Compose {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let label = if self.submitting {
|
||||
t!("compose.creating_dm_button")
|
||||
} else if self.contacts.len() > 1 {
|
||||
} else if self.selected(cx).len() > 1 {
|
||||
t!("compose.create_group_dm_button")
|
||||
} else {
|
||||
t!("compose.create_dm_button")
|
||||
};
|
||||
|
||||
let error = self.error_message.read(cx).as_ref();
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.mb_4()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("compose.description"))),
|
||||
)
|
||||
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
||||
this.child(div().text_xs().text_color(red()).child(msg.clone()))
|
||||
.when_some(error, |this, msg| {
|
||||
this.child(
|
||||
div()
|
||||
.italic()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(msg.clone()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div().flex().flex_col().child(
|
||||
div()
|
||||
.h_10()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(SharedString::new(t!("compose.subject_label"))),
|
||||
)
|
||||
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.mt_1()
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.h_10()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(SharedString::new(t!("compose.subject_label"))),
|
||||
)
|
||||
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.my_1()
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
@@ -467,9 +484,7 @@ impl Render for Compose {
|
||||
.child(SharedString::new(t!("compose.to_label"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
TextInput::new(&self.user_input)
|
||||
@@ -479,8 +494,8 @@ impl Render for Compose {
|
||||
.child(
|
||||
Button::new("add")
|
||||
.icon(IconName::PlusCircleFill)
|
||||
.small()
|
||||
.ghost()
|
||||
.loading(self.adding)
|
||||
.disabled(self.adding)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.add_and_select_contact(window, cx);
|
||||
@@ -491,14 +506,12 @@ impl Render for Compose {
|
||||
.map(|this| {
|
||||
if self.contacts.is_empty() {
|
||||
this.child(
|
||||
div()
|
||||
.w_full()
|
||||
v_flex()
|
||||
.h_24()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_align(TextAlign::Center)
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -525,7 +538,7 @@ impl Render for Compose {
|
||||
this.list_items(range, cx)
|
||||
}),
|
||||
)
|
||||
.min_h(px(280.)),
|
||||
.min_h(px(300.)),
|
||||
)
|
||||
}
|
||||
}),
|
||||
@@ -534,11 +547,12 @@ impl Render for Compose {
|
||||
Button::new("create_dm_btn")
|
||||
.label(label)
|
||||
.primary()
|
||||
.small()
|
||||
.w_full()
|
||||
.loading(self.submitting)
|
||||
.disabled(self.submitting || self.adding)
|
||||
.on_click(cx.listener(move |this, _event, window, cx| {
|
||||
this.compose(window, cx);
|
||||
this.submit(window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use smol::fs;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
||||
use ui::{v_flex, Disableable, IconName, Sizable};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<EditProfile> {
|
||||
EditProfile::new(window, cx)
|
||||
@@ -166,10 +166,7 @@ impl EditProfile {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Show loading spinner
|
||||
self.set_submitting(true, cx);
|
||||
|
||||
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Option<Event>, Error>> {
|
||||
let avatar = self.avatar_input.read(cx).value().to_string();
|
||||
let name = self.name_input.read(cx).value().to_string();
|
||||
let bio = self.bio_input.read(cx).value().to_string();
|
||||
@@ -191,52 +188,25 @@ impl EditProfile {
|
||||
new_metadata = new_metadata.website(url);
|
||||
}
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
nostr_client().set_metadata(&new_metadata).await?;
|
||||
Ok(())
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let output = client.set_metadata(&new_metadata).await?;
|
||||
let event = client.database().event_by_id(&output.val).await?;
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(t!("profile.updated_successfully"), cx);
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
Ok(event)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_submitting = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EditProfile {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
@@ -306,17 +276,5 @@ impl Render for EditProfile {
|
||||
.child(SharedString::new(t!("profile.label_bio")))
|
||||
.child(TextInput::new(&self.bio_input).small()),
|
||||
)
|
||||
.child(
|
||||
div().py_3().child(
|
||||
Button::new("submit")
|
||||
.label(SharedString::new(t!("common.update")))
|
||||
.primary()
|
||||
.disabled(self.is_loading || self.is_submitting)
|
||||
.loading(self.is_submitting)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.submit(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use i18n::{shared_t, t};
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -53,8 +53,11 @@ impl Login {
|
||||
let key_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
||||
|
||||
let relay_input =
|
||||
cx.new(|cx| InputState::new(window, cx).default_value(NOSTR_CONNECT_RELAY));
|
||||
let relay_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(NOSTR_CONNECT_RELAY)
|
||||
.placeholder(NOSTR_CONNECT_RELAY)
|
||||
});
|
||||
|
||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
//
|
||||
@@ -556,12 +559,12 @@ impl Render for Login {
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(SharedString::new(t!("login.title"))),
|
||||
.child(shared_t!("login.title")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("login.key_description"))),
|
||||
.child(shared_t!("login.key_description")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -581,13 +584,12 @@ impl Render for Login {
|
||||
})),
|
||||
)
|
||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||
let msg = t!("login.approve_message", i = i);
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(msg)),
|
||||
.child(shared_t!("login.approve_message", i = i)),
|
||||
)
|
||||
})
|
||||
.when_some(self.error.read(cx).clone(), |this, error| {
|
||||
@@ -603,89 +605,90 @@ impl Render for Login {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cx.theme().surface_background)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.text_color(cx.theme().text)
|
||||
.child(SharedString::new(t!("login.nostr_connect"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("login.scan_qr"))),
|
||||
),
|
||||
)
|
||||
.when_some(self.qr_image.read(cx).clone(), |this, qr| {
|
||||
this.child(
|
||||
div().flex_1().p_1().child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cx.theme().surface_background)
|
||||
.rounded(cx.theme().radius)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.id("")
|
||||
.mb_2()
|
||||
.p_2()
|
||||
.size_72()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.text_color(cx.theme().text)
|
||||
.child(shared_t!("login.nostr_connect")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("login.scan_qr")),
|
||||
),
|
||||
)
|
||||
.when_some(self.qr_image.read(cx).clone(), |this, qr| {
|
||||
this.child(
|
||||
div()
|
||||
.id("")
|
||||
.mb_2()
|
||||
.p_2()
|
||||
.size_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.rounded_2xl()
|
||||
.shadow_md()
|
||||
.when(cx.theme().mode.is_dark(), |this| {
|
||||
this.shadow_none()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
})
|
||||
.bg(cx.theme().background)
|
||||
.child(img(qr).h_64())
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
this.connection_string.read(cx).to_string(),
|
||||
));
|
||||
window.push_notification(t!("common.copied"), cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.rounded_2xl()
|
||||
.shadow_md()
|
||||
.when(cx.theme().mode.is_dark(), |this| {
|
||||
this.shadow_none()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
})
|
||||
.bg(cx.theme().background)
|
||||
.child(img(qr).h_64())
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
this.connection_string.read(cx).to_string(),
|
||||
));
|
||||
window.push_notification(
|
||||
t!("common.copied").to_string(),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(TextInput::new(&self.relay_input).xsmall())
|
||||
.child(
|
||||
Button::new("change")
|
||||
.label(t!("common.change"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.change_relay(window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
.gap_1()
|
||||
.child(TextInput::new(&self.relay_input).xsmall())
|
||||
.child(
|
||||
Button::new("change")
|
||||
.label(t!("common.change"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
this.change_relay(window, cx);
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
396
crates/coop/src/views/messaging_relays.rs
Normal file
396
crates/coop/src/views/messaging_relays.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use global::constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID, NIP17_RELAYS};
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, uniform_list, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
|
||||
TextAlign, UniformList, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<MessagingRelays> {
|
||||
cx.new(|cx| MessagingRelays::new(window, cx))
|
||||
}
|
||||
|
||||
pub fn relay_button() -> impl IntoElement {
|
||||
div().child(
|
||||
Button::new("dm-relays")
|
||||
.icon(IconName::Info)
|
||||
.label(t!("relays.button_label"))
|
||||
.warning()
|
||||
.xsmall()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.on_click(move |_, window, cx| {
|
||||
let title = SharedString::new(t!("relays.modal_title"));
|
||||
let view = cx.new(|cx| MessagingRelays::new(window, cx));
|
||||
let weak_view = view.downgrade();
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
let weak_view = weak_view.clone();
|
||||
|
||||
modal
|
||||
.confirm()
|
||||
.title(title.clone())
|
||||
.child(view.clone())
|
||||
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
||||
.on_ok(move |_, window, cx| {
|
||||
weak_view
|
||||
.update(cx, |this, cx| {
|
||||
this.set_relays(window, cx);
|
||||
})
|
||||
.ok();
|
||||
// true to close the modal
|
||||
false
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub struct MessagingRelays {
|
||||
input: Entity<InputState>,
|
||||
relays: Vec<RelayUrl>,
|
||||
error: Option<SharedString>,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl MessagingRelays {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.observe_new::<Self>(move |this, window, cx| {
|
||||
if let Some(window) = window {
|
||||
this.load(window, cx);
|
||||
}
|
||||
}));
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&input,
|
||||
window,
|
||||
move |this: &mut Self, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.add(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
Self {
|
||||
input,
|
||||
subscriptions,
|
||||
relays: vec![],
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first() {
|
||||
let relays = event
|
||||
.tags
|
||||
.filter(TagKind::Relay)
|
||||
.filter_map(|tag| RelayUrl::parse(tag.content()?).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(relays)
|
||||
} else {
|
||||
Err(anyhow!("Not found."))
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(relays) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.relays = relays;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let value = self.input.read(cx).value().to_string();
|
||||
|
||||
if !value.starts_with("ws") {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(url) = RelayUrl::parse(&value) {
|
||||
if !self.relays.contains(&url) {
|
||||
self.relays.push(url);
|
||||
}
|
||||
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_value("", window, cx);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, ix: usize, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.relays.remove(ix);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
pub fn set_relays(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.relays.is_empty() {
|
||||
self.set_error(t!("relays.empty"), window, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let relays = self.relays.clone();
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(
|
||||
relays
|
||||
.iter()
|
||||
.map(|relay| Tag::relay(relay.clone()))
|
||||
.collect_vec(),
|
||||
);
|
||||
|
||||
// Set messaging relays
|
||||
client.send_event_builder(builder).await?;
|
||||
|
||||
// Connect to messaging relays
|
||||
for relay in relays.into_iter() {
|
||||
_ = client.add_relay(&relay).await;
|
||||
_ = client.connect_relay(&relay).await;
|
||||
}
|
||||
|
||||
let all_msg_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||
let new_msg_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||
|
||||
let all_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
let new_messages = Filter::new()
|
||||
.kind(Kind::GiftWrap)
|
||||
.pubkey(public_key)
|
||||
.limit(0);
|
||||
|
||||
// Close old subscriptions
|
||||
client.unsubscribe(&all_msg_id).await;
|
||||
client.unsubscribe(&new_msg_id).await;
|
||||
|
||||
// Subscribe to all messages
|
||||
client
|
||||
.subscribe_with_id(all_msg_id, all_messages, None)
|
||||
.await?;
|
||||
|
||||
// Subscribe to new messages
|
||||
client
|
||||
.subscribe_with_id(new_msg_id, new_messages, None)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
window.close_modal(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(e.to_string(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> UniformList {
|
||||
let relays = self.relays.clone();
|
||||
let total = relays.len();
|
||||
|
||||
uniform_list(
|
||||
"relays",
|
||||
total,
|
||||
cx.processor(move |_, range, _window, cx| {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for ix in range {
|
||||
let item = relays.get(ix).map(|i: &RelayUrl| i.to_string()).unwrap();
|
||||
|
||||
items.push(
|
||||
div().group("").w_full().h_9().py_0p5().child(
|
||||
div()
|
||||
.px_2()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.text_xs()
|
||||
.child(item)
|
||||
.child(
|
||||
Button::new("remove_{ix}")
|
||||
.icon(IconName::Close)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.remove(ix, window, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
items
|
||||
}),
|
||||
)
|
||||
.w_full()
|
||||
.min_h(px(200.))
|
||||
}
|
||||
|
||||
fn render_empty(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.h_20()
|
||||
.mb_2()
|
||||
.justify_center()
|
||||
.text_sm()
|
||||
.text_align(TextAlign::Center)
|
||||
.child(SharedString::new(t!("relays.add_some_relays")))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MessagingRelays {
|
||||
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!("relays.description")),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(TextInput::new(&self.input).small())
|
||||
.child(
|
||||
Button::new("add")
|
||||
.icon(IconName::PlusFill)
|
||||
.label(t!("common.add"))
|
||||
.ghost()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.add(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("relays.recommended")),
|
||||
)
|
||||
.child(h_flex().gap_1().children({
|
||||
NIP17_RELAYS.iter().map(|&relay| {
|
||||
div()
|
||||
.id(relay)
|
||||
.group("")
|
||||
.py_0p5()
|
||||
.px_1p5()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.bg(cx.theme().secondary_background)
|
||||
.hover(|this| this.bg(cx.theme().secondary_hover))
|
||||
.active(|this| this.bg(cx.theme().secondary_active))
|
||||
.rounded_full()
|
||||
.child(relay)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.input.update(cx, |this, cx| {
|
||||
this.set_value(relay, window, cx);
|
||||
});
|
||||
this.add(window, cx);
|
||||
}))
|
||||
})
|
||||
})),
|
||||
)
|
||||
.when_some(self.error.as_ref(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if !self.relays.is_empty() {
|
||||
this.child(self.render_list(window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty(window, cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
pub mod backup_keys;
|
||||
pub mod chat;
|
||||
pub mod compose;
|
||||
pub mod edit_profile;
|
||||
pub mod login;
|
||||
pub mod messaging_relays;
|
||||
pub mod new_account;
|
||||
pub mod onboarding;
|
||||
pub mod preferences;
|
||||
pub mod relays;
|
||||
pub mod screening;
|
||||
pub mod sidebar;
|
||||
pub mod startup;
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
use anyhow::anyhow;
|
||||
use common::nip96::nip96_upload;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
||||
Styled, Window,
|
||||
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
||||
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
||||
Render, SharedString, Styled, WeakEntity, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
use ui::{divider, v_flex, ContextModal, Disableable, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||
NewAccount::new(window, cx)
|
||||
@@ -25,7 +27,6 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||
pub struct NewAccount {
|
||||
name_input: Entity<InputState>,
|
||||
avatar_input: Entity<InputState>,
|
||||
bio_input: Entity<InputState>,
|
||||
is_uploading: bool,
|
||||
is_submitting: bool,
|
||||
// Panel
|
||||
@@ -46,19 +47,12 @@ impl NewAccount {
|
||||
.placeholder(SharedString::new(t!("profile.placeholder_name")))
|
||||
});
|
||||
|
||||
let bio_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.placeholder(SharedString::new(t!("profile.placeholder_bio")))
|
||||
});
|
||||
|
||||
let avatar_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.png"));
|
||||
|
||||
Self {
|
||||
name_input,
|
||||
avatar_input,
|
||||
bio_input,
|
||||
is_uploading: false,
|
||||
is_submitting: false,
|
||||
name: "New Account".into(),
|
||||
@@ -69,140 +63,97 @@ impl NewAccount {
|
||||
}
|
||||
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_submitting(true, cx);
|
||||
self.submitting(true, cx);
|
||||
|
||||
let identity = Identity::global(cx);
|
||||
let avatar = self.avatar_input.read(cx).value().to_string();
|
||||
let name = self.name_input.read(cx).value().to_string();
|
||||
let bio = self.bio_input.read(cx).value().to_string();
|
||||
|
||||
let mut metadata = Metadata::new().display_name(name).about(bio);
|
||||
// Build metadata
|
||||
let mut metadata = Metadata::new().display_name(name.clone()).name(name);
|
||||
|
||||
if let Ok(url) = Url::parse(&avatar) {
|
||||
metadata = metadata.picture(url);
|
||||
};
|
||||
|
||||
let current_view = cx.entity().downgrade();
|
||||
let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
let weak_input = pwd_input.downgrade();
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
let metadata = metadata.clone();
|
||||
let weak_input = weak_input.clone();
|
||||
let view_cancel = current_view.clone();
|
||||
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.on_cancel(move |_, window, cx| {
|
||||
view_cancel
|
||||
.update(cx, |_this, cx| {
|
||||
window.push_notification(t!("new_account.password_invalid"), cx)
|
||||
})
|
||||
.ok();
|
||||
true
|
||||
})
|
||||
.on_ok(move |_, window, cx| {
|
||||
let metadata = metadata.clone();
|
||||
let value = weak_input
|
||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||
.ok();
|
||||
|
||||
if let Some(password) = value {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.new_identity(password.to_string(), metadata, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(SharedString::new(t!("new_account.set_password_prompt")))
|
||||
.child(TextInput::new(&pwd_input).small()),
|
||||
)
|
||||
identity.update(cx, |this, cx| {
|
||||
this.new_identity(metadata, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let nip96 = AppSettings::get_media_server(cx);
|
||||
let avatar_input = self.avatar_input.downgrade();
|
||||
self.uploading(true, cx);
|
||||
|
||||
// Get the user's configured NIP96 server
|
||||
let nip96_server = AppSettings::get_media_server(cx);
|
||||
|
||||
// Open native file dialog
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
self.set_uploading(true, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task = Tokio::spawn(cx, async move {
|
||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||
Ok(Some(mut paths)) => {
|
||||
let Some(path) = paths.pop() else {
|
||||
cx.update(|_, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_uploading(false, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
if let Some(path) = paths.pop() {
|
||||
let file = fs::read(path).await?;
|
||||
let url = nip96_upload(nostr_client(), &nip96_server, file).await?;
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(file_data) = fs::read(path).await {
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
nostr_sdk::async_utility::task::spawn(async move {
|
||||
if let Ok(url) = nip96_upload(nostr_client(), &nip96, file_data).await {
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(url) = rx.await {
|
||||
cx.update(|window, cx| {
|
||||
// Stop loading spinner
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_uploading(false, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Set avatar input
|
||||
avatar_input
|
||||
.update(cx, |this, cx| {
|
||||
this.set_value(url.to_string(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Ok(url)
|
||||
} else {
|
||||
Err(anyhow!("Path not found"))
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
cx.update(|_, cx| {
|
||||
Ok(None) => Err(anyhow!("User cancelled")),
|
||||
Err(e) => Err(anyhow!("File dialog error: {e}")),
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match Flatten::flatten(task.await.map_err(|e| e.into())) {
|
||||
Ok(Ok(url)) => {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_uploading(false, cx);
|
||||
this.uploading(false, cx);
|
||||
this.avatar_input.update(cx, |this, cx| {
|
||||
this.set_value(url.to_string(), window, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(_) => {}
|
||||
Ok(Err(e)) => {
|
||||
Self::notify_error(cx, this, e.to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
Self::notify_error(cx, this, e.to_string());
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
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.is_submitting = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_uploading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
fn uploading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_uploading = status;
|
||||
cx.notify();
|
||||
}
|
||||
@@ -243,93 +194,72 @@ impl Focusable for NewAccount {
|
||||
}
|
||||
|
||||
impl Render for NewAccount {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_10()
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.text_lg()
|
||||
.text_center()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(SharedString::new(t!("new_account.title"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
v_flex()
|
||||
.w_96()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h_32()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if self.avatar_input.read(cx).value().is_empty() {
|
||||
this.child(
|
||||
img("brand/avatar.png")
|
||||
.rounded_full()
|
||||
.size_10()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
img(self.avatar_input.read(cx).value().clone())
|
||||
.rounded_full()
|
||||
.size_10()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.label(t!("profile.set_profile_picture"))
|
||||
.icon(Icon::new(IconName::Plus))
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(self.is_submitting)
|
||||
.loading(self.is_uploading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(SharedString::new(t!("profile.label_name")))
|
||||
.child(SharedString::new(t!("new_account.name")))
|
||||
.child(TextInput::new(&self.name_input).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(SharedString::new(t!("profile.label_bio")))
|
||||
.child(TextInput::new(&self.bio_input).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.my_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(cx.theme().elevated_surface_background),
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.child(SharedString::new(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(ButtonRounded::Full)
|
||||
.disabled(self.is_submitting)
|
||||
.loading(self.is_uploading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(divider(cx))
|
||||
.child(
|
||||
Button::new("submit")
|
||||
.label(SharedString::new(t!("common.continue")))
|
||||
|
||||
@@ -15,7 +15,7 @@ use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::checkbox::Checkbox;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
@@ -244,12 +244,13 @@ impl Render for Onboarding {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().w_24().absolute().bottom_4().right_4().child(
|
||||
Button::new("unload")
|
||||
div().w_24().absolute().bottom_2().right_2().child(
|
||||
Button::new("logout")
|
||||
.icon(IconName::Logout)
|
||||
.label(SharedString::new(t!("common.logout")))
|
||||
.ghost()
|
||||
.small()
|
||||
.label(SharedString::new(t!("user.sign_out")))
|
||||
.danger()
|
||||
.xsmall()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.disabled(self.loading)
|
||||
.on_click(|_, window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use common::display::DisplayProfile;
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER};
|
||||
use gpui::http_client::Url;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -14,10 +13,11 @@ use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::switch::Switch;
|
||||
use ui::{v_flex, ContextModal, IconName, Sizable, Size, StyledExt};
|
||||
|
||||
use crate::views::{edit_profile, relays};
|
||||
use crate::views::{edit_profile, messaging_relays};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||
Preferences::new(window, cx)
|
||||
@@ -33,8 +33,8 @@ impl Preferences {
|
||||
let media_server = AppSettings::get_media_server(cx).to_string();
|
||||
let media_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(media_server)
|
||||
.placeholder(NIP96_SERVER)
|
||||
.default_value(media_server.clone())
|
||||
.placeholder(media_server)
|
||||
});
|
||||
|
||||
Self { media_input }
|
||||
@@ -42,25 +42,73 @@ impl Preferences {
|
||||
}
|
||||
|
||||
fn open_edit_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let edit_profile = edit_profile::init(window, cx);
|
||||
let view = edit_profile::init(window, cx);
|
||||
let weak_view = view.downgrade();
|
||||
let title = SharedString::new(t!("profile.title"));
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
let weak_view = weak_view.clone();
|
||||
|
||||
modal
|
||||
.confirm()
|
||||
.title(title.clone())
|
||||
.width(px(DEFAULT_MODAL_WIDTH))
|
||||
.child(edit_profile.clone())
|
||||
.child(view.clone())
|
||||
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
||||
.on_ok(move |_, window, cx| {
|
||||
weak_view
|
||||
.update(cx, |this, cx| {
|
||||
let set_metadata = this.set_metadata(cx);
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
match set_metadata.await {
|
||||
Ok(event) => {
|
||||
if let Some(event) = event {
|
||||
cx.update(|_, cx| {
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
this.insert_or_update_person(event, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
// true to close the modal
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let relays = relays::init(window, cx);
|
||||
let title = SharedString::new(t!("preferences.modal_relays_title"));
|
||||
let title = SharedString::new(t!("relays.modal_title"));
|
||||
let view = messaging_relays::init(window, cx);
|
||||
let weak_view = view.downgrade();
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
this.width(px(DEFAULT_MODAL_WIDTH))
|
||||
let weak_view = weak_view.clone();
|
||||
|
||||
this.confirm()
|
||||
.title(title.clone())
|
||||
.child(relays.clone())
|
||||
.child(view.clone())
|
||||
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
||||
.on_ok(move |_, window, cx| {
|
||||
weak_view
|
||||
.update(cx, |this, cx| {
|
||||
this.set_relays(window, cx);
|
||||
})
|
||||
.ok();
|
||||
// true to close the modal
|
||||
false
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -80,10 +128,8 @@ impl Render for Preferences {
|
||||
|
||||
v_flex()
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
@@ -135,7 +181,7 @@ impl Render for Preferences {
|
||||
)
|
||||
.child(
|
||||
Button::new("relays")
|
||||
.label("DM Relays")
|
||||
.label("Messaging Relays")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
@@ -146,11 +192,8 @@ impl Render for Preferences {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
@@ -162,6 +205,7 @@ impl Render for Preferences {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.my_1()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_1()
|
||||
@@ -193,10 +237,8 @@ impl Render for Preferences {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
@@ -208,9 +250,7 @@ impl Render for Preferences {
|
||||
.child(SharedString::new(t!("preferences.messages_header"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Switch::new("screening")
|
||||
@@ -242,10 +282,8 @@ impl Render for Preferences {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
@@ -257,9 +295,7 @@ impl Render for Preferences {
|
||||
.child(SharedString::new(t!("preferences.display_header"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Switch::new("hide_user_avatars")
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
use anyhow::Error;
|
||||
use global::constants::NEW_MESSAGE_SUB_ID;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign,
|
||||
UniformList, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
||||
|
||||
const MIN_HEIGHT: f32 = 200.0;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Relays> {
|
||||
Relays::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Relays {
|
||||
relays: Entity<Vec<RelayUrl>>,
|
||||
input: Entity<InputState>,
|
||||
focus_handle: FocusHandle,
|
||||
is_loading: bool,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
|
||||
impl Relays {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||
let relays = cx.new(|cx| {
|
||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
let relays = event
|
||||
.tags
|
||||
.filter(TagKind::Relay)
|
||||
.filter_map(|tag| RelayUrl::parse(tag.content()?).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(relays)
|
||||
} else {
|
||||
let relays = vec![
|
||||
RelayUrl::parse("wss://auth.nostr1.com")?,
|
||||
RelayUrl::parse("wss://relay.0xchat.com")?,
|
||||
];
|
||||
|
||||
Ok(relays)
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Ok(relays) = task.await {
|
||||
cx.update(|cx| {
|
||||
this.update(cx, |this: &mut Vec<RelayUrl>, cx| {
|
||||
*this = relays;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
vec![]
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&input,
|
||||
window,
|
||||
move |this: &mut Relays, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.add(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
Self {
|
||||
relays,
|
||||
input,
|
||||
subscriptions,
|
||||
is_loading: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_loading(true, cx);
|
||||
|
||||
let relays = self.relays.read(cx).clone();
|
||||
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// If user didn't have any NIP-65 relays, add default ones
|
||||
if client.database().relay_list(public_key).await?.is_empty() {
|
||||
let builder = EventBuilder::relay_list(vec![
|
||||
(RelayUrl::parse("wss://relay.damus.io/").unwrap(), None),
|
||||
(RelayUrl::parse("wss://relay.primal.net/").unwrap(), None),
|
||||
]);
|
||||
|
||||
if let Err(e) = client.send_event_builder(builder).await {
|
||||
log::error!("Failed to send relay list event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let tags: Vec<Tag> = relays
|
||||
.iter()
|
||||
.map(|relay| Tag::custom(TagKind::Relay, vec![relay.to_string()]))
|
||||
.collect();
|
||||
|
||||
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
|
||||
let output = client.send_event_builder(builder).await?;
|
||||
|
||||
// Connect to messaging relays
|
||||
for relay in relays.into_iter() {
|
||||
_ = client.add_relay(&relay).await;
|
||||
_ = client.connect_relay(&relay).await;
|
||||
}
|
||||
|
||||
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||
|
||||
// Close old subscription
|
||||
client.unsubscribe(&sub_id).await;
|
||||
|
||||
// Subscribe to new messages
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id(
|
||||
sub_id,
|
||||
Filter::new()
|
||||
.kind(Kind::GiftWrap)
|
||||
.pubkey(public_key)
|
||||
.limit(0),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to subscribe to new messages: {e}");
|
||||
}
|
||||
|
||||
Ok(output.val)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if task.await.is_ok() {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_loading(false, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
window.close_modal(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let value = self.input.read(cx).value().to_string();
|
||||
|
||||
if !value.starts_with("ws") {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(url) = RelayUrl::parse(&value) {
|
||||
self.relays.update(cx, |this, cx| {
|
||||
if !this.contains(&url) {
|
||||
this.push(url);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_value("", window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, ix: usize, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.relays.update(cx, |this, cx| {
|
||||
this.remove(ix);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn render_list(
|
||||
&mut self,
|
||||
relays: Vec<RelayUrl>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> UniformList {
|
||||
let total = relays.len();
|
||||
|
||||
uniform_list(
|
||||
"relays",
|
||||
total,
|
||||
cx.processor(move |_, range, _window, cx| {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for ix in range {
|
||||
let item = relays.get(ix).map(|i: &RelayUrl| i.to_string()).unwrap();
|
||||
|
||||
items.push(
|
||||
div().group("").w_full().h_9().py_0p5().child(
|
||||
div()
|
||||
.px_2()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.text_xs()
|
||||
.child(item)
|
||||
.child(
|
||||
Button::new("remove_{ix}")
|
||||
.icon(IconName::Close)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.remove(ix, window, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
items
|
||||
}),
|
||||
)
|
||||
.w_full()
|
||||
.min_h(px(MIN_HEIGHT))
|
||||
}
|
||||
|
||||
fn render_empty(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.h_20()
|
||||
.mb_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_sm()
|
||||
.text_align(TextAlign::Center)
|
||||
.child(SharedString::new(t!("relays.add_some_relays")))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Relays {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("relays.description"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(TextInput::new(&self.input).small())
|
||||
.child(
|
||||
Button::new("add_relay_btn")
|
||||
.icon(IconName::Plus)
|
||||
.label(t!("common.add"))
|
||||
.small()
|
||||
.ghost()
|
||||
.rounded_md()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.add(window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.map(|this| {
|
||||
let relays = self.relays.read(cx).clone();
|
||||
|
||||
if !relays.is_empty() {
|
||||
this.child(self.render_list(relays, window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty(window, cx))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("submti")
|
||||
.label(t!("common.update"))
|
||||
.primary()
|
||||
.w_full()
|
||||
.loading(self.is_loading)
|
||||
.disabled(self.is_loading)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.update(window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,14 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use common::debounced_delay::DebouncedDelay;
|
||||
use common::display::{DisplayProfile, TextUtils};
|
||||
use global::constants::{BOOTSTRAP_RELAYS, DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
||||
use common::display::TextUtils;
|
||||
use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context,
|
||||
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||
Task, Window,
|
||||
div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
|
||||
Styled, Subscription, Task, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use i18n::t;
|
||||
@@ -25,7 +24,6 @@ use registry::{Registry, RoomEmitter};
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
@@ -34,8 +32,6 @@ use ui::popup_menu::PopupMenu;
|
||||
use ui::skeleton::Skeleton;
|
||||
use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||
|
||||
use crate::views::compose;
|
||||
|
||||
mod list_item;
|
||||
|
||||
const FIND_DELAY: u64 = 600;
|
||||
@@ -551,18 +547,6 @@ impl Sidebar {
|
||||
});
|
||||
}
|
||||
|
||||
fn open_compose(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let compose = compose::init(window, cx);
|
||||
let title = SharedString::new(t!("sidebar.direct_messages"));
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
modal
|
||||
.title(title.clone())
|
||||
.width(px(DEFAULT_MODAL_WIDTH))
|
||||
.child(compose.clone())
|
||||
});
|
||||
}
|
||||
|
||||
fn open_loading_modal(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let title = SharedString::new(t!("sidebar.loading_modal_title"));
|
||||
let text_1 = SharedString::new(t!("sidebar.loading_modal_body_1"));
|
||||
@@ -596,49 +580,6 @@ impl Sidebar {
|
||||
});
|
||||
}
|
||||
|
||||
fn account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
|
||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||
|
||||
div()
|
||||
.px_3()
|
||||
.h_8()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.id("current-user")
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.75)))
|
||||
.child(profile.display_name())
|
||||
.on_click(cx.listener({
|
||||
let Ok(public_key) = profile.public_key().to_bech32();
|
||||
let item = ClipboardItem::new_string(public_key);
|
||||
|
||||
move |_, _, window, cx| {
|
||||
cx.write_to_clipboard(item.clone());
|
||||
window.push_notification(t!("common.copied"), cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("compose")
|
||||
.icon(IconName::PlusFill)
|
||||
.tooltip(t!("sidebar.dm_tooltip"))
|
||||
.small()
|
||||
.primary()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_compose(window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn skeletons(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
|
||||
(0..total).map(|_| {
|
||||
div()
|
||||
@@ -727,9 +668,6 @@ 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 profile = Identity::read_global(cx)
|
||||
.public_key()
|
||||
.map(|pk| registry.get_person(&pk, cx));
|
||||
|
||||
// Get rooms from either search results or the chat registry
|
||||
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {
|
||||
@@ -745,22 +683,17 @@ impl Render for Sidebar {
|
||||
}
|
||||
};
|
||||
|
||||
div()
|
||||
v_flex()
|
||||
.image_cache(self.image_cache.clone())
|
||||
.size_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
// Account
|
||||
.when_some(profile, |this, profile| {
|
||||
this.child(self.account(&profile, cx))
|
||||
})
|
||||
// Search Input
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.px_3()
|
||||
.mt_3()
|
||||
.px_2p5()
|
||||
.w_full()
|
||||
.h_7()
|
||||
.flex_none()
|
||||
@@ -781,14 +714,12 @@ impl Render for Sidebar {
|
||||
)
|
||||
// Chat Rooms
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.overflow_y_hidden()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.px_1p5()
|
||||
.w_full()
|
||||
.overflow_y_hidden()
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
@@ -879,8 +810,11 @@ impl Render for Sidebar {
|
||||
),
|
||||
)
|
||||
.when(registry.loading, |this| {
|
||||
let title = SharedString::new(t!("sidebar.retrieving_messages"));
|
||||
let desc = SharedString::new(t!("sidebar.retrieving_messages_description"));
|
||||
|
||||
this.child(
|
||||
div().absolute().bottom_4().px_4().w_full().child(
|
||||
div().absolute().bottom_3().px_3().w_full().child(
|
||||
div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
@@ -890,35 +824,28 @@ impl Render for Sidebar {
|
||||
.justify_between()
|
||||
.bg(cx.theme().panel_background)
|
||||
.shadow_sm()
|
||||
// Empty div
|
||||
.child(div().size_6().flex_shrink_0())
|
||||
// Loading indicator
|
||||
// Loading
|
||||
.child(div().flex_shrink_0().pl_1().child(Indicator::new().small()))
|
||||
// Title
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.line_height(relative(1.2))
|
||||
.child(Indicator::new().xsmall())
|
||||
.child(SharedString::new(t!(
|
||||
"sidebar.retrieving_messages"
|
||||
))),
|
||||
.child(title.clone()),
|
||||
)
|
||||
.child(div().text_color(cx.theme().text_muted).child(
|
||||
SharedString::new(t!(
|
||||
"sidebar.retrieving_messages_description"
|
||||
)),
|
||||
)),
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(desc.clone()),
|
||||
),
|
||||
)
|
||||
// Info button
|
||||
.child(
|
||||
|
||||
@@ -40,7 +40,7 @@ impl UserProfile {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Skip if user isn't logged in
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
return;
|
||||
@@ -100,11 +100,6 @@ impl UserProfile {
|
||||
self.profile(cx).metadata().nip05
|
||||
}
|
||||
|
||||
fn open_njump(&mut self, _window: &mut Window, cx: &mut App) {
|
||||
let Ok(bech32) = self.public_key.to_bech32();
|
||||
cx.open_url(&format!("https://njump.me/{bech32}"));
|
||||
}
|
||||
|
||||
fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok(bech32) = self.public_key.to_bech32();
|
||||
let item = ClipboardItem::new_string(bech32);
|
||||
@@ -221,7 +216,7 @@ impl Render for UserProfile {
|
||||
.child(shared_bech32),
|
||||
)
|
||||
.child(
|
||||
Button::new("copy-pubkey")
|
||||
Button::new("copy")
|
||||
.icon({
|
||||
if self.copied {
|
||||
IconName::CheckCircleFill
|
||||
@@ -259,14 +254,5 @@ impl Render for UserProfile {
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-njump")
|
||||
.label(t!("profile.njump"))
|
||||
.primary()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.open_njump(window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user