diff --git a/src/column.rs b/src/column.rs index 0b986323..bd0acdbb 100644 --- a/src/column.rs +++ b/src/column.rs @@ -61,7 +61,9 @@ impl Columns { } pub fn new_column_picker(&mut self) { - self.add_column(Column::new(vec![Route::AddColumn])); + self.add_column(Column::new(vec![Route::AddColumn( + crate::ui::add_column::AddColumnRoute::Base, + )])); } fn get_new_id() -> u32 { diff --git a/src/nav.rs b/src/nav.rs index efe1283a..266970e3 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -13,7 +13,7 @@ use crate::{ }, ui::{ self, - add_column::{AddColumnResponse, AddColumnView}, + add_column::render_add_column_routes, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, note::PostAction, support::SupportView, @@ -101,19 +101,9 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { None } - Route::AddColumn => { - let resp = - AddColumnView::new(&app.ndb, app.accounts.get_selected_account()).ui(ui); + Route::AddColumn(route) => { + render_add_column_routes(ui, app, col, route); - if let Some(resp) = resp { - match resp { - AddColumnResponse::Timeline(timeline) => { - let id = timeline.id; - app.columns_mut().add_timeline_to_column(col, timeline); - app.subscribe_new_timeline(id); - } - }; - } None } @@ -205,7 +195,7 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { let cur_router = app.columns_mut().column_mut(col).router_mut(); cur_router.navigating = false; if cur_router.is_replacing() { - cur_router.remove_previous_route(); + cur_router.remove_previous_routes(); } } diff --git a/src/route.rs b/src/route.rs index 27923fb3..4d96188f 100644 --- a/src/route.rs +++ b/src/route.rs @@ -6,7 +6,10 @@ use crate::{ account_manager::AccountsRoute, column::Columns, timeline::{TimelineId, TimelineRoute}, - ui::profile::preview::{get_note_users_displayname_string, get_profile_displayname_string}, + ui::{ + add_column::AddColumnRoute, + profile::preview::{get_note_users_displayname_string, get_profile_displayname_string}, + }, }; /// App routing. These describe different places you can go inside Notedeck. @@ -16,7 +19,7 @@ pub enum Route { Accounts(AccountsRoute), Relays, ComposeNote, - AddColumn, + AddColumn(AddColumnRoute), Profile(Pubkey), Support, } @@ -97,7 +100,13 @@ impl Route { AccountsRoute::AddAccount => "Add Account".to_owned(), }, Route::ComposeNote => "Compose Note".to_owned(), - Route::AddColumn => "Add Column".to_owned(), + Route::AddColumn(c) => match c { + AddColumnRoute::Base => "Add Column".to_owned(), + AddColumnRoute::UndecidedNotification => "Add Notifications Column".to_owned(), + AddColumnRoute::ExternalNotification => { + "Add External Notifications Column".to_owned() + } + }, Route::Profile(pubkey) => { format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey)) } @@ -142,7 +151,7 @@ impl Router { self.routes.push(route); } - // Route to R. Then when it is successfully placed, should call `remove_previous_route` + // Route to R. Then when it is successfully placed, should call `remove_previous_routes` to remove all previous routes pub fn route_to_replaced(&mut self, route: R) { self.navigating = true; self.replacing = true; @@ -167,14 +176,15 @@ impl Router { self.routes.pop() } - pub fn remove_previous_route(&mut self) -> Option { + pub fn remove_previous_routes(&mut self) { let num_routes = self.routes.len(); if num_routes <= 1 { - return None; + return; } + self.returning = false; self.replacing = false; - Some(self.routes.remove(num_routes - 2)) + self.routes.drain(..num_routes - 1); } pub fn is_replacing(&self) -> bool { @@ -208,7 +218,7 @@ impl fmt::Display for Route { }, Route::ComposeNote => write!(f, "Compose Note"), - Route::AddColumn => write!(f, "Add Column"), + Route::AddColumn(_) => write!(f, "Add Column"), Route::Profile(_) => write!(f, "Profile"), Route::Support => write!(f, "Support"), } diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs index 298addcc..477528f8 100644 --- a/src/ui/add_column.rs +++ b/src/ui/add_column.rs @@ -1,26 +1,45 @@ -use egui::{pos2, vec2, Color32, FontId, ImageSource, Pos2, Rect, Separator, Ui}; +use egui::{pos2, vec2, Color32, FontId, ImageSource, Pos2, Rect, RichText, Separator, Ui}; use nostrdb::Ndb; +use tracing::{error, info}; use crate::{ app_style::{get_font_size, NotedeckTextStyle}, + key_parsing::perform_key_retrieval, timeline::{PubkeySource, Timeline, TimelineKind}, ui::anim::ICON_EXPANSION_MULTIPLE, user_account::UserAccount, + Damus, }; use super::anim::AnimationHelper; pub enum AddColumnResponse { Timeline(Timeline), + UndecidedNotification, + ExternalNotification, +} + +pub enum NotificationColumnType { + Home, + External, } #[derive(Clone, Debug)] enum AddColumnOption { Universe, + UndecidedNotification, + ExternalNotification, Notification(PubkeySource), Home(PubkeySource), } +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum AddColumnRoute { + Base, + UndecidedNotification, + ExternalNotification, +} + impl AddColumnOption { pub fn take_as_response( self, @@ -34,11 +53,15 @@ impl AddColumnOption { AddColumnOption::Notification(pubkey) => TimelineKind::Notifications(pubkey) .into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes())) .map(AddColumnResponse::Timeline), + AddColumnOption::UndecidedNotification => { + Some(AddColumnResponse::UndecidedNotification) + } AddColumnOption::Home(pubkey) => { let tlk = TimelineKind::contact_list(pubkey); tlk.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes())) .map(AddColumnResponse::Timeline) } + AddColumnOption::ExternalNotification => Some(AddColumnResponse::ExternalNotification), } } } @@ -55,7 +78,7 @@ impl<'a> AddColumnView<'a> { pub fn ui(&mut self, ui: &mut Ui) -> Option { let mut selected_option: Option = None; - for column_option_data in self.get_column_options() { + for column_option_data in self.get_base_options() { let option = column_option_data.option.clone(); if self.column_option_ui(ui, column_option_data).clicked() { selected_option = option.take_as_response(self.ndb, self.cur_account); @@ -67,6 +90,65 @@ impl<'a> AddColumnView<'a> { selected_option } + fn notifications_ui(&mut self, ui: &mut Ui) -> Option { + let mut selected_option: Option = None; + for column_option_data in self.get_notifications_options() { + let option = column_option_data.option.clone(); + if self.column_option_ui(ui, column_option_data).clicked() { + selected_option = option.take_as_response(self.ndb, self.cur_account); + } + + ui.add(Separator::default().spacing(0.0)); + } + + selected_option + } + + fn external_notification_ui(&mut self, ui: &mut Ui) -> Option { + ui.label(RichText::new("External Notification").heading()); + ui.label("Paste the user's npub that you would like to have a notifications column for:"); + + let id = ui.id().with("external_notif"); + let mut text = ui.ctx().data_mut(|data| { + let text = data.get_temp_mut_or_insert_with(id, String::new); + text.clone() + }); + ui.text_edit_singleline(&mut text); + ui.ctx().data_mut(|d| d.insert_temp(id, text.clone())); + + if ui.button("Validate").clicked() { + if let Some(payload) = perform_key_retrieval(&text).ready() { + match payload { + Ok(keypair) => { + info!( + "Successfully retrieved external notification keypair {}", + keypair.pubkey + ); + if let Some(resp) = + AddColumnOption::Notification(PubkeySource::Explicit(keypair.pubkey)) + .take_as_response(self.ndb, self.cur_account) + { + Some(resp) + } else { + error!("Failed to get timeline column"); + None + } + } + Err(_) => { + info!("User did not enter a valid npub or nip05"); + ui.colored_label(Color32::RED, "Please enter a valid npub or nip05"); + None + } + } + } else { + ui.spinner(); + None + } + } else { + None + } + } + fn column_option_ui(&mut self, ui: &mut Ui, data: ColumnOptionData) -> egui::Response { let icon_padding = 8.0; let min_icon_width = 32.0; @@ -168,7 +250,7 @@ impl<'a> AddColumnView<'a> { helper.take_animation_response() } - fn get_column_options(&self) -> Vec { + fn get_base_options(&self) -> Vec { let mut vec = Vec::new(); vec.push(ColumnOptionData { title: "Universe", @@ -190,14 +272,42 @@ impl<'a> AddColumnView<'a> { icon: egui::include_image!("../../assets/icons/home_icon_dark_4x.png"), option: AddColumnOption::Home(source.clone()), }); + } + vec.push(ColumnOptionData { + title: "Notifications", + description: "Stay up to date with notifications and mentions", + icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), + option: AddColumnOption::UndecidedNotification, + }); + + vec + } + + fn get_notifications_options(&self) -> Vec { + let mut vec = Vec::new(); + + if let Some(acc) = self.cur_account { + let source = if acc.secret_key.is_some() { + PubkeySource::DeckAuthor + } else { + PubkeySource::Explicit(acc.pubkey) + }; + vec.push(ColumnOptionData { - title: "Notifications", - description: "Stay up to date with notifications and mentions", + title: "Your Notifications", + description: "Stay up to date with your notifications and mentions", icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), option: AddColumnOption::Notification(source), }); } + vec.push(ColumnOptionData { + title: "Someone else's Notifications", + description: "Stay up to date with someone else's notifications and mentions", + icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), + option: AddColumnOption::ExternalNotification, + }); + vec } } @@ -209,6 +319,46 @@ struct ColumnOptionData { option: AddColumnOption, } +pub fn render_add_column_routes( + ui: &mut egui::Ui, + app: &mut Damus, + col: usize, + route: &AddColumnRoute, +) { + let resp = match route { + AddColumnRoute::Base => { + AddColumnView::new(&app.ndb, app.accounts.get_selected_account()).ui(ui) + } + AddColumnRoute::UndecidedNotification => { + AddColumnView::new(&app.ndb, app.accounts.get_selected_account()).notifications_ui(ui) + } + AddColumnRoute::ExternalNotification => { + AddColumnView::new(&app.ndb, app.accounts.get_selected_account()) + .external_notification_ui(ui) + } + }; + + if let Some(resp) = resp { + match resp { + AddColumnResponse::Timeline(timeline) => { + let id = timeline.id; + app.columns_mut().add_timeline_to_column(col, timeline); + app.subscribe_new_timeline(id); + } + AddColumnResponse::UndecidedNotification => { + app.columns_mut().column_mut(col).router_mut().route_to( + crate::route::Route::AddColumn(AddColumnRoute::UndecidedNotification), + ); + } + AddColumnResponse::ExternalNotification => { + app.columns_mut().column_mut(col).router_mut().route_to( + crate::route::Route::AddColumn(AddColumnRoute::ExternalNotification), + ); + } + }; + } +} + mod preview { use crate::{ test_data, diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index 7cefedbe..9f311421 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -196,7 +196,11 @@ impl<'a> DesktopSidePanel<'a> { } } SidePanelAction::Columns => { - if router.routes().iter().any(|&r| r == Route::AddColumn) { + if router + .routes() + .iter() + .any(|&r| matches!(r, Route::AddColumn(_))) + { router.go_back(); } else { columns.new_column_picker();