use cargo_packager_updater::{check_update, semver::Version, url::Url}; use common::{ constants::{UPDATER_PUBKEY, UPDATER_URL}, profile::NostrProfile, }; use gpui::{ actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled, StyledImage, Window, }; use log::info; use nostr_sdk::prelude::*; use serde::Deserialize; use state::get_client; use std::sync::Arc; use tokio::sync::oneshot; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, dock_area::{dock::DockPlacement, DockArea, DockItem}, popup_menu::PopupMenuExt, theme::{scale::ColorScaleStep, ActiveTheme, Appearance, Theme}, ContextModal, Icon, IconName, Root, Sizable, TitleBar, }; use super::{chat, contacts, onboarding, profile, relays::Relays, settings, sidebar, welcome}; #[derive(Clone, PartialEq, Eq, Deserialize)] pub enum PanelKind { Room(u64), Profile, Contacts, Settings, } #[derive(Clone, PartialEq, Eq, Deserialize)] pub struct AddPanel { panel: PanelKind, position: DockPlacement, } impl AddPanel { pub fn new(panel: PanelKind, position: DockPlacement) -> Self { Self { panel, position } } } impl_internal_actions!(dock, [AddPanel]); actions!(account, [Logout]); pub fn init(account: NostrProfile, window: &mut Window, cx: &mut App) -> Entity { AppView::new(account, window, cx) } pub struct AppView { account: NostrProfile, relays: Entity>>, dock: Entity, } impl AppView { pub fn new(account: NostrProfile, window: &mut Window, cx: &mut App) -> Entity { // Initialize dock layout let dock = cx.new(|cx| DockArea::new(window, cx)); let weak_dock = dock.downgrade(); // Initialize left dock let left_panel = DockItem::panel(Arc::new(sidebar::init(window, cx))); // Initial central dock let center_panel = DockItem::split_with_sizes( Axis::Vertical, vec![DockItem::tabs( vec![Arc::new(welcome::init(window, cx))], None, &weak_dock, window, cx, )], vec![None], &weak_dock, window, cx, ); // Set default dock layout with left and central docks _ = weak_dock.update(cx, |view, cx| { view.set_left_dock(left_panel, Some(px(240.)), true, window, cx); view.set_center(center_panel, window, cx); }); // Check and auto update to the latest version cx.background_spawn(async move { // Set auto updater config let config = cargo_packager_updater::Config { endpoints: vec![Url::parse(UPDATER_URL).expect("Failed to parse UPDATER URL")], pubkey: String::from(UPDATER_PUBKEY), ..Default::default() }; // Run auto updater if let Ok(current_version) = Version::parse(env!("CARGO_PKG_VERSION")) { if let Ok(Some(update)) = check_update(current_version, config) { if update.download_and_install().is_ok() { info!("Update installed") } } } }) .detach(); cx.new(|cx| { let public_key = account.public_key(); let relays = cx.new(|_| None); let async_relays = relays.downgrade(); // Check user's messaging relays and determine user is ready for NIP17 or not. // If not, show the setup modal and instruct user setup inbox relays let client = get_client(); let window_handle = window.window_handle(); let (tx, rx) = oneshot::channel::>>(); let this = Self { account, relays, dock, }; cx.background_spawn(async move { let filter = Filter::new() .kind(Kind::InboxRelays) .author(public_key) .limit(1); let relays = if let Ok(events) = client.database().query(filter).await { if let Some(event) = events.first_owned() { Some( event .tags .filter_standardized(TagKind::Relay) .filter_map(|t| match t { TagStandard::Relay(url) => Some(url.to_string()), _ => None, }) .collect::>(), ) } else { None } } else { None }; _ = tx.send(relays); }) .detach(); cx.spawn(|this, mut cx| async move { if let Ok(result) = rx.await { if let Some(relays) = result { _ = cx.update(|cx| { _ = async_relays.update(cx, |this, cx| { *this = Some(relays); cx.notify(); }); }); } else { _ = cx.update_window(window_handle, |_, window, cx| { this.update(cx, |this: &mut Self, cx| { this.render_setup_relays(window, cx) }) }); } } }) .detach(); this }) } fn render_setup_relays(&self, window: &mut Window, cx: &mut Context) { let relays = cx.new(|cx| Relays::new(None, window, cx)); window.open_modal(cx, move |this, window, cx| { let is_loading = relays.read(cx).loading(); this.keyboard(false) .closable(false) .width(px(420.)) .title("Your Messaging Relays are not configured") .child(relays.clone()) .footer( div() .p_2() .border_t_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .child( Button::new("update_inbox_relays_btn") .label("Update") .primary() .bold() .rounded(ButtonRounded::Large) .w_full() .loading(is_loading) .on_click(window.listener_for(&relays, |this, _, window, cx| { this.update(window, cx); })), ), ) }); } fn render_edit_relay(&self, window: &mut Window, cx: &mut Context) { let relays = self.relays.read(cx).clone(); let view = cx.new(|cx| Relays::new(relays, window, cx)); window.open_modal(cx, move |this, window, cx| { let is_loading = view.read(cx).loading(); this.width(px(420.)) .title("Edit your Messaging Relays") .child(view.clone()) .footer( div() .p_2() .border_t_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .child( Button::new("update_inbox_relays_btn") .label("Update") .primary() .bold() .rounded(ButtonRounded::Large) .w_full() .loading(is_loading) .on_click(window.listener_for(&view, |this, _, window, cx| { this.update(window, cx); })), ), ) }); } fn render_appearance_button( &self, _window: &mut Window, cx: &mut Context, ) -> impl IntoElement { Button::new("appearance") .xsmall() .ghost() .map(|this| { if cx.theme().appearance.is_dark() { this.icon(IconName::Sun) } else { this.icon(IconName::Moon) } }) .on_click(cx.listener(|_, _, window, cx| { if cx.theme().appearance.is_dark() { Theme::change(Appearance::Light, Some(window), cx); } else { Theme::change(Appearance::Dark, Some(window), cx); } })) } fn render_relays_button( &self, _window: &mut Window, cx: &mut Context, ) -> impl IntoElement { Button::new("relays") .xsmall() .ghost() .icon(IconName::Relays) .on_click(cx.listener(|this, _, window, cx| { this.render_edit_relay(window, cx); })) } fn render_account(&self) -> impl IntoElement { Button::new("account") .ghost() .xsmall() .reverse() .icon(Icon::new(IconName::ChevronDownSmall)) .child( img(self.account.avatar()) .size_5() .rounded_full() .object_fit(ObjectFit::Cover), ) .popup_menu(move |this, _, _cx| { this.menu( "Profile", Box::new(AddPanel::new(PanelKind::Profile, DockPlacement::Right)), ) .menu( "Contacts", Box::new(AddPanel::new(PanelKind::Contacts, DockPlacement::Right)), ) .menu( "Settings", Box::new(AddPanel::new(PanelKind::Settings, DockPlacement::Center)), ) .separator() .menu("Change account", Box::new(Logout)) }) } fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context) { match &action.panel { PanelKind::Room(id) => match chat::init(id, window, cx) { Ok(panel) => { self.dock.update(cx, |dock_area, cx| { dock_area.add_panel(panel, action.position, window, cx); }); } Err(e) => window.push_notification(e.to_string(), cx), }, PanelKind::Profile => { let panel = Arc::new(profile::init(self.account.clone(), window, cx)); self.dock.update(cx, |dock_area, cx| { dock_area.add_panel(panel, action.position, window, cx); }); } PanelKind::Contacts => { let panel = Arc::new(contacts::init(window, cx)); self.dock.update(cx, |dock_area, cx| { dock_area.add_panel(panel, action.position, window, cx); }); } PanelKind::Settings => { let panel = Arc::new(settings::init(window, cx)); self.dock.update(cx, |dock_area, cx| { dock_area.add_panel(panel, action.position, window, cx); }); } }; } fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context) { cx.background_spawn(async move { get_client().reset().await }) .detach(); window.replace_root(cx, |window, cx| { Root::new(onboarding::init(window, cx).into(), window, cx) }); } } impl Render for AppView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); div() .relative() .size_full() .flex() .flex_col() // Main .child( TitleBar::new() // Left side .child(div()) // Right side .child( div() .flex() .items_center() .justify_end() .gap_2() .px_2() .child(self.render_appearance_button(window, cx)) .child(self.render_relays_button(window, cx)) .child(self.render_account()), ), ) .child(self.dock.clone()) .child(div().absolute().top_8().children(notification_layer)) .children(modal_layer) .on_action(cx.listener(Self::on_panel_action)) .on_action(cx.listener(Self::on_logout_action)) } }