,
+ ) -> 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(panel: P, window: &mut Window, cx: &mut App)
+ where
+ P: PanelView,
+ {
if let Some(Some(root)) = window.root::() {
if let Ok(chatspace) = root.read(cx).view().clone().downcast::() {
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()),
)
diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs
index 9204ef1..2c75285 100644
--- a/crates/coop/src/main.rs
+++ b/crates/coop/src/main.rs
@@ -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()
};
diff --git a/crates/coop/src/views/backup_keys.rs b/crates/coop/src/views/backup_keys.rs
new file mode 100644
index 0000000..a32d3ba
--- /dev/null
+++ b/crates/coop/src/views/backup_keys.rs
@@ -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,
+ pubkey_input: Entity,
+ secret_input: Entity,
+ error: Option,
+ 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) {
+ 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.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(&mut self, error: E, window: &mut Window, cx: &mut Context)
+ where
+ E: Into,
+ {
+ 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) {
+ 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) -> 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()),
+ )
+ }),
+ )
+ }
+}
diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs
index 64585a5..4a7b727 100644
--- a/crates/coop/src/views/chat/mod.rs
+++ b/crates/coop/src/views/chat/mod.rs
@@ -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