use std::sync::Arc; use anyhow::Error; use auto_update::AutoUpdater; use chat::{ChatEvent, ChatRegistry}; use common::download_dir; use gpui::{ Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px, }; use nip4e::{DeviceEvent, DeviceRegistry}; use nip29::{GroupId, GroupKey}; use nostr_sdk::prelude::*; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; use state::{NostrRegistry, StateEvent}; use theme::{ActiveTheme, TextStyle, Typeset as _, SIDEBAR_WIDTH, Theme}; use ui::button::Button; use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle}; use ui::notification::{Notification, NotificationKind}; use ui::{Root, WindowExtension, v_flex}; use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::{join_group, new_chat, new_group, profile, relays, settings}; use crate::panels::{backup, browse, contact_list, greeter, inbox}; use crate::sidebar::Sidebar; mod dialogs; mod panels; mod sidebar; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Workspace::new(window, cx)) } struct DeviceNotifcation; struct MsgRelayNotification; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = workspace, no_json)] enum Command { Update, RefreshMessagingRelays, BackupEncryption, ImportEncryption, RefreshEncryption, ResetEncryption, ShowRelayList, ShowProfile, ShowSettings, ShowBackup, ShowContactList, ShowInbox, ShowBrowse, NewChat, NewGroup, JoinGroup, } pub struct Workspace { dock: Entity, sidebar: Entity, /// Async tasks tasks: Vec>>, /// Event subscriptions _subscriptions: SmallVec<[Subscription; 7]>, } impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); let dock = dock::dock_area("coop", window, cx); let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx)); let left_sidebar = sidebar.clone(); let mut subscriptions = smallvec![]; subscriptions.push( // Observe system appearance and update theme cx.observe_window_appearance(window, |_this, window, cx| { Theme::sync_system_appearance(Some(window), cx); }), ); subscriptions.push( // Subscribe to the nostr events cx.subscribe_in(&nostr, window, move |_this, _state, event, window, cx| { if let StateEvent::SignerChanged = event { window.close_all_dialogs(cx); } }), ); subscriptions.push( // Observe all events emitted by the device registry cx.subscribe_in(&device, window, |_this, _device, event, window, cx| { match event { DeviceEvent::Requesting(code) => { const MSG: &str = "Please open another client and approve the request \ for the encryption key. Compare the code with the \ one shown there before approving."; let note = Notification::new() .id::() .autohide(false) .title(format!("Wait for approval ยท {code}")) .message(MSG) .with_kind(NotificationKind::Info); window.push_notification(note, cx); } DeviceEvent::NotSet => { const MSG: &str = "No encryption key announcement was found. It may \ still be propagating, or you can restore one \ from the settings."; let note = Notification::new() .id::() .message(MSG) .with_kind(NotificationKind::Info) .action(|_this, _window, _cx| { Button::new("retry").label("Retry").on_click( move |_this, window, cx| { let device = DeviceRegistry::global(cx); device.update(cx, |this, cx| { this.get_announcement(cx); }); window.clear_notification::(cx); }, ) }); window.push_notification(note, cx); } DeviceEvent::Set => { let note = Notification::new() .id::() .message("Encryption Key has been set") .with_kind(NotificationKind::Success); window.push_notification(note, cx); } DeviceEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); } }; }), ); subscriptions.push( // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |_this, _chat, ev, window, cx| { match ev { ChatEvent::InboxRelayNotFound => { const MSG: &str = "Messaging Relays not found. Cannot receive messages."; window.push_notification( Notification::warning(MSG) .id::() .autohide(false) .action(|_this, _window, _cx| { Button::new("retry").label("Retry").on_click( move |_this, window, cx| { let chat = ChatRegistry::global(cx); chat.update(cx, |this, cx| { this.get_metadata(cx); }); window.clear_notification::(cx); }, ) }), cx, ); } ChatEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); } _ => {} }; }), ); cx.defer_in(window, move |this, window, cx| { let sidebar = PanelHandle::new(left_sidebar); let greeter = PanelHandle::new(greeter::init(window, cx)); this.dock.update(cx, |this, cx| { let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx); let center = DockLayout::v_split() .child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None); this.set_dock(DockPlacement::Left, left, window, cx); this.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx); this.set_center(center, window, cx); }); }); // Listen app-wide, so a group panel can show or hide the sidebar without holding it. let workspace = cx.entity().downgrade(); App::on_action(cx, { let workspace = workspace.clone(); move |action: &nip29_ui::ShowGroupMeta, cx| { let Some(workspace) = workspace.upgrade() else { return; }; workspace.update(cx, |this, cx| this.show_group_meta(action, cx)); } }); App::on_action(cx, move |action: &nip29_ui::HideGroupMeta, cx| { let Some(workspace) = workspace.upgrade() else { return; }; workspace.update(cx, |this, cx| this.hide_group_meta(action, cx)); }); Self { dock, sidebar, tasks: vec![], _subscriptions: subscriptions, } } /// Add a panel to the dock, from anywhere that has the window but not the workspace. pub fn add_panel( panel: Entity

, placement: DockPlacement, window: &mut Window, cx: &mut App, ) { if let Some(root) = window.root::().flatten() && let Ok(workspace) = root.read(cx).view().clone().downcast::() { workspace.update(cx, |this, cx| { this.add_panel_to_dock(panel, placement, window, cx) }); } } /// Add a panel to the dock, or focus it if it is already docked. fn add_panel_to_dock( &mut self, panel: Entity

, placement: DockPlacement, window: &mut Window, cx: &mut Context, ) { self.dock.update(cx, |area, cx| { ui::dock::add_panel(area, PanelHandle::new(panel), placement, window, cx) }); } /// Handle command events fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context) { match command { Command::ShowSettings => { let view = settings::init(window, cx); window.open_dialog(cx, move |this, _window, _cx| { this.width(px(520.)) .show_close(true) .pb_2() .title("Preferences") .child(view.clone()) }); } Command::ShowProfile => { let nostr = NostrRegistry::global(cx); if let Some(public_key) = nostr.read(cx).current_user() { profile::open(public_key, window, cx); } } Command::ShowContactList => { self.add_panel_to_dock( contact_list::init(window, cx), DockPlacement::Left, window, cx, ); } Command::ShowInbox => { self.add_panel_to_dock(inbox::init(window, cx), DockPlacement::Center, window, cx); } Command::ShowBrowse => { let panel = browse::init(self.dock.downgrade(), window, cx); self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); } Command::NewChat => { new_chat::open(self.dock.downgrade(), window, cx); } Command::NewGroup => { new_group::open(window, cx); } Command::JoinGroup => { join_group::open(window, cx); } Command::ShowBackup => { self.add_panel_to_dock(backup::init(window, cx), DockPlacement::Left, window, cx); } Command::RefreshMessagingRelays => { let chat = ChatRegistry::global(cx); // Trigger a refresh of the chat registry chat.update(cx, |this, cx| { this.reload(cx); }); } Command::ShowRelayList => { relays::open(window, cx); } Command::RefreshEncryption => { let device = DeviceRegistry::global(cx); device.update(cx, |this, cx| { this.get_announcement(cx); }); } Command::ResetEncryption => { self.confirm_reset_encryption(window, cx); } Command::BackupEncryption => { let device = DeviceRegistry::global(cx).downgrade(); let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt")); self.tasks.push(cx.spawn_in(window, async move |_this, cx| { // Get the output path from the save dialog let output_path = match save_dialog.await { Ok(Ok(Some(path))) => path, Ok(Ok(None)) | Err(_) => return Ok(()), Ok(Err(error)) => { cx.update(|window, cx| { let message = format!("Failed to pick save location: {error:#}"); let note = Notification::error(message).autohide(false); window.push_notification(note, cx); })?; return Ok(()); } }; // Get the backup task let backup = device.read_with(cx, |this, cx| this.backup(output_path.clone(), cx))?; // Run the backup task backup.await?; // Open the backup file with the system's default application cx.update(|_window, cx| { cx.open_with_system(output_path.as_path()); })?; Ok(()) })); } Command::ImportEncryption => { self.import_encryption(window, cx); } Command::Update => { if let Some(auto_updater) = AutoUpdater::try_global(cx) { auto_updater.update(cx, |this, cx| this.check(cx)); } } } } /// Show the sidebar's information for the group a panel belongs to. fn show_group_meta(&mut self, action: &nip29_ui::ShowGroupMeta, cx: &mut Context) { let Ok(id) = GroupId::new(action.group.clone()) else { return; }; let key = GroupKey::new(action.relay.clone(), id); self.sidebar.update(cx, |sidebar, cx| { sidebar.show_group_meta(&key, cx); }); } /// Hide the sidebar's information for the group a panel belongs to. fn hide_group_meta(&mut self, action: &nip29_ui::HideGroupMeta, cx: &mut Context) { let Ok(id) = GroupId::new(action.group.clone()) else { return; }; let key = GroupKey::new(action.relay.clone(), id); self.sidebar.update(cx, |sidebar, cx| { sidebar.hide_group_meta(&key, cx); }); } fn confirm_reset_encryption(&mut self, window: &mut Window, cx: &mut Context) { const ENC_MSG: &str = "Encryption Key is a special key that used to encrypt and decrypt your messages. \ Your identity is completely decoupled from all encryption processes to protect your privacy."; const ENC_WARN: &str = "By resetting your encryption key, you will lose access to \ all your encrypted messages before. This action cannot be undone."; let device = DeviceRegistry::global(cx); let ent = device.downgrade(); window.open_dialog(cx, move |this, _window, cx| { let ent = ent.clone(); this.confirm() .show_close(true) .title("Reset Encryption Key") .child( v_flex() .gap_1() .text_style(TextStyle::Callout) .child(SharedString::from(ENC_MSG)) .child( div() .italic() .text_color(cx.theme().danger) .child(SharedString::from(ENC_WARN)), ), ) .on_ok(move |_ev, _window, cx| { ent.update(cx, |this, cx| { this.set_announcement(Keys::generate(), cx); }) .ok(); // true to close dialog true }) }); } fn import_encryption(&mut self, window: &mut Window, cx: &mut Context) { let restore = cx.new(|cx| RestoreEncryption::new(window, cx)); window.open_dialog(cx, move |this, _window, _cx| { this.width(px(420.)) .title("Restore Encryption") .child(restore.clone()) }); } } impl Render for Workspace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let dialog_layer = Root::render_dialog_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); div() .id("workspace") .on_action(cx.listener(Self::on_command)) .relative() .size_full() .child(self.dock.clone()) // Notifications .children(notification_layer) // Dialogs .children(dialog_layer) } }