notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit e8a1233174d0f355e64d575a8593a72024cc3dff
parent 4cedea9fdb2e02173bc7a21738c1c721ce540885
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 22 Apr 2025 18:42:12 -0700

dave: bubble note actions to chrome

This allows chrome to pass note actions to other apps

Diffstat:
Mcrates/notedeck/src/app.rs | 9+++++++--
Mcrates/notedeck/src/lib.rs | 2+-
Mcrates/notedeck_chrome/src/app.rs | 4++--
Mcrates/notedeck_chrome/src/chrome.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/notedeck_columns/src/app.rs | 6++++--
Mcrates/notedeck_columns/src/lib.rs | 2+-
Mcrates/notedeck_columns/src/ui/account_login_view.rs | 11++++++++---
Mcrates/notedeck_columns/src/ui/configure_deck.rs | 10++++++++--
Mcrates/notedeck_columns/src/ui/edit_deck.rs | 9+++++++--
Mcrates/notedeck_columns/src/ui/note/post.rs | 6++++--
Mcrates/notedeck_columns/src/ui/preview.rs | 9++++++++-
Mcrates/notedeck_columns/src/ui/profile/edit.rs | 9+++++++--
Mcrates/notedeck_columns/src/ui/relay.rs | 5+++--
Mcrates/notedeck_dave/src/lib.rs | 11+++++++++--
Mcrates/notedeck_dave/src/ui/dave.rs | 123++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
15 files changed, 229 insertions(+), 61 deletions(-)

diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs @@ -3,7 +3,8 @@ use crate::wallet::GlobalWallet; use crate::zaps::Zaps; use crate::{ frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath, - DataPathType, Directory, Images, NoteCache, RelayDebugView, ThemeHandler, UnknownIds, + DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler, + UnknownIds, }; use egui::ThemePreference; use egui_winit::clipboard::Clipboard; @@ -15,8 +16,12 @@ use std::path::Path; use std::rc::Rc; use tracing::{error, info}; +pub enum AppAction { + Note(NoteAction), +} + pub trait App { - fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui); + fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction>; } /// Main notedeck app framework diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -33,7 +33,7 @@ mod wallet; mod zaps; pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction, SwitchAccountAction}; -pub use app::{App, Notedeck}; +pub use app::{App, AppAction, Notedeck}; pub use args::Args; pub use context::AppContext; pub use error::{Error, FilterError, ZapError}; diff --git a/crates/notedeck_chrome/src/app.rs b/crates/notedeck_chrome/src/app.rs @@ -1,4 +1,4 @@ -use notedeck::AppContext; +use notedeck::{AppAction, AppContext}; use notedeck_columns::Damus; use notedeck_dave::Dave; @@ -9,7 +9,7 @@ pub enum NotedeckApp { } impl notedeck::App for NotedeckApp { - fn update(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) { + fn update(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<AppAction> { match self { NotedeckApp::Dave(dave) => dave.update(ctx, ui), NotedeckApp::Columns(columns) => columns.update(ctx, ui), diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -6,9 +6,13 @@ use egui::{vec2, Button, Label, Layout, RichText, ThemePreference, Widget}; use egui_extras::{Size, StripBuilder}; use nostrdb::{ProfileRecord, Transaction}; use notedeck::{ - profile::get_profile_url, App, AppContext, NotedeckTextStyle, UserAccount, WalletType, + profile::get_profile_url, App, AppAction, AppContext, NoteAction, NotedeckTextStyle, + UserAccount, WalletType, +}; +use notedeck_columns::{ + timeline::{ThreadSelection, TimelineKind}, + Damus, Route, }; -use notedeck_columns::Damus; use notedeck_dave::{Dave, DaveAvatar}; use notedeck_ui::{AnimationHelper, ProfilePic}; @@ -179,7 +183,9 @@ impl Chrome { ); */ - self.apps[self.active as usize].update(ctx, ui); + if let Some(action) = self.apps[self.active as usize].update(ctx, ui) { + chrome_handle_app_action(self, ctx, action, ui); + } }); }); @@ -297,11 +303,12 @@ impl Chrome { } impl notedeck::App for Chrome { - fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) { + fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> { if let Some(action) = self.show(ctx, ui) { action.process(ctx, self, ui); } // TODO: unify this constant with the columns side panel width. ui crate? + None } } @@ -453,3 +460,62 @@ fn wallet_button() -> impl Widget { helper.take_animation_response() } } + +fn chrome_handle_app_action( + chrome: &mut Chrome, + ctx: &mut AppContext, + action: AppAction, + ui: &mut egui::Ui, +) { + match action { + AppAction::Note(note_action) => match note_action { + NoteAction::Hashtag(hashtag) => { + ChromePanelAction::columns_navigate( + ctx, + chrome, + Route::Timeline(TimelineKind::Hashtag(hashtag)), + ); + } + + NoteAction::Reply(note_id) => { + ChromePanelAction::columns_navigate(ctx, chrome, Route::Reply(note_id)); + } + + NoteAction::Zap(_) => { + todo!("implement note zaps in chrome"); + } + + NoteAction::Context(context) => 'brk: { + let txn = Transaction::new(ctx.ndb).unwrap(); + let Some(note) = ctx.ndb.get_note_by_key(&txn, context.note_key).ok() else { + break 'brk; + }; + + context.action.process(ui, &note, ctx.pool); + } + + NoteAction::Quote(note_id) => { + ChromePanelAction::columns_navigate(ctx, chrome, Route::Quote(note_id)); + } + + NoteAction::Profile(pubkey) => { + ChromePanelAction::columns_navigate( + ctx, + chrome, + Route::Timeline(TimelineKind::Profile(pubkey)), + ); + } + + NoteAction::Note(note_id) => { + let txn = Transaction::new(ctx.ndb).unwrap(); + let thread = ThreadSelection::from_note_id(ctx.ndb, ctx.note_cache, &txn, note_id); + + match thread { + Ok(t) => ChromePanelAction::columns_navigate(ctx, chrome, Route::thread(t)), + + Err(err) => tracing::error!("{:?}", err), + } + } + }, + } +} diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -12,7 +12,7 @@ use crate::{ Result, }; -use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, UnknownIds}; +use notedeck::{Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, UnknownIds}; use notedeck_ui::NoteOptions; use enostr::{ClientMessage, Keypair, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; @@ -639,7 +639,7 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut App } impl notedeck::App for Damus { - fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { + fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { /* self.app .frame_history @@ -648,6 +648,8 @@ impl notedeck::App for Damus { update_damus(self, ctx, ui.ctx()); render_damus(self, ctx, ui); + + None } } diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs @@ -26,7 +26,7 @@ mod search; mod subscriptions; mod support; mod test_data; -mod timeline; +pub mod timeline; pub mod ui; mod unknowns; mod view_state; diff --git a/crates/notedeck_columns/src/ui/account_login_view.rs b/crates/notedeck_columns/src/ui/account_login_view.rs @@ -5,8 +5,7 @@ use egui::{ }; use egui::{Layout, TextEdit}; use enostr::Keypair; -use notedeck::fonts::get_font_size; -use notedeck::NotedeckTextStyle; +use notedeck::{fonts::get_font_size, AppAction, NotedeckTextStyle}; pub struct AccountLoginView<'a> { manager: &'a mut AcquireKeyState, @@ -155,8 +154,14 @@ mod preview { } impl App for AccountLoginPreview { - fn update(&mut self, _app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { + fn update( + &mut self, + _app_ctx: &mut AppContext<'_>, + ui: &mut egui::Ui, + ) -> Option<AppAction> { AccountLoginView::new(&mut self.manager).ui(ui); + + None } } diff --git a/crates/notedeck_columns/src/ui/configure_deck.rs b/crates/notedeck_columns/src/ui/configure_deck.rs @@ -301,7 +301,7 @@ mod preview { }; use super::ConfigureDeckView; - use notedeck::{App, AppContext}; + use notedeck::{App, AppAction, AppContext}; pub struct ConfigureDeckPreview { state: DeckState, @@ -316,8 +316,14 @@ mod preview { } impl App for ConfigureDeckPreview { - fn update(&mut self, _app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { + fn update( + &mut self, + _app_ctx: &mut AppContext<'_>, + ui: &mut egui::Ui, + ) -> Option<AppAction> { ConfigureDeckView::new(&mut self.state).ui(ui); + + None } } diff --git a/crates/notedeck_columns/src/ui/edit_deck.rs b/crates/notedeck_columns/src/ui/edit_deck.rs @@ -60,7 +60,7 @@ mod preview { }; use super::EditDeckView; - use notedeck::{App, AppContext}; + use notedeck::{App, AppAction, AppContext}; pub struct EditDeckPreview { state: DeckState, @@ -75,8 +75,13 @@ mod preview { } impl App for EditDeckPreview { - fn update(&mut self, _app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { + fn update( + &mut self, + _app_ctx: &mut AppContext<'_>, + ui: &mut egui::Ui, + ) -> Option<AppAction> { EditDeckView::new(&mut self.state).ui(ui); + None } } diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs @@ -691,7 +691,7 @@ mod preview { use crate::media_upload::Nip94Event; use super::*; - use notedeck::{App, AppContext}; + use notedeck::{App, AppAction, AppContext}; pub struct PostPreview { draft: Draft, @@ -730,7 +730,7 @@ mod preview { } impl App for PostPreview { - fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) { + fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { let txn = Transaction::new(app.ndb).expect("txn"); let mut note_context = NoteContext { ndb: app.ndb, @@ -749,6 +749,8 @@ mod preview { NoteOptions::default(), ) .ui(&txn, ui); + + None } } diff --git a/crates/notedeck_columns/src/ui/preview.rs b/crates/notedeck_columns/src/ui/preview.rs @@ -1,3 +1,5 @@ +use notedeck::AppAction; + pub struct PreviewConfig { pub is_mobile: bool, } @@ -20,7 +22,12 @@ impl PreviewApp { } impl notedeck::App for PreviewApp { - fn update(&mut self, app_ctx: &mut notedeck::AppContext<'_>, ui: &mut egui::Ui) { + fn update( + &mut self, + app_ctx: &mut notedeck::AppContext<'_>, + ui: &mut egui::Ui, + ) -> Option<AppAction> { self.view.update(app_ctx, ui); + None } } diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs @@ -167,7 +167,7 @@ fn button(text: &str, width: f32) -> egui::Button<'static> { } mod preview { - use notedeck::App; + use notedeck::{App, AppAction}; use crate::{ profile_state::ProfileState, @@ -190,8 +190,13 @@ mod preview { } impl App for EditProfilePreivew { - fn update(&mut self, ctx: &mut notedeck::AppContext<'_>, ui: &mut egui::Ui) { + fn update( + &mut self, + ctx: &mut notedeck::AppContext<'_>, + ui: &mut egui::Ui, + ) -> Option<AppAction> { EditProfileView::new(&mut self.state, ctx.img_cache).ui(ui); + None } } diff --git a/crates/notedeck_columns/src/ui/relay.rs b/crates/notedeck_columns/src/ui/relay.rs @@ -274,7 +274,7 @@ fn get_connection_icon(status: RelayStatus) -> egui::Image<'static> { mod preview { use super::*; use crate::test_data::sample_pool; - use notedeck::{App, AppContext}; + use notedeck::{App, AppAction, AppContext}; pub struct RelayViewPreview { pool: RelayPool, @@ -289,7 +289,7 @@ mod preview { } impl App for RelayViewPreview { - fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) { + fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { self.pool.try_recv(); let mut id_string_map = HashMap::new(); RelayView::new( @@ -298,6 +298,7 @@ mod preview { &mut id_string_map, ) .ui(ui); + None } } diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -7,7 +7,7 @@ use chrono::{Duration, Local}; use egui_wgpu::RenderState; use futures::StreamExt; use nostrdb::Transaction; -use notedeck::AppContext; +use notedeck::{AppAction, AppContext}; use std::collections::HashMap; use std::string::ToString; use std::sync::mpsc::{self, Receiver}; @@ -313,17 +313,21 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } impl notedeck::App for Dave { - fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { + fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { /* self.app .frame_history .on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); */ + let mut app_action: Option<AppAction> = None; //update_dave(self, ctx, ui.ctx()); let should_send = self.process_events(ctx); if let Some(action) = self.ui(ctx, ui).action { match action { + DaveAction::Note(n) => { + app_action = Some(AppAction::Note(n)); + } DaveAction::NewChat => { self.handle_new_chat(); } @@ -332,8 +336,11 @@ impl notedeck::App for Dave { } } } + if should_send { self.send_user_message(ctx, ui.ctx()); } + + app_action } } diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -4,7 +4,7 @@ use crate::{ }; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; use nostrdb::{Ndb, Transaction}; -use notedeck::{AppContext, NoteContext}; +use notedeck::{AppContext, NoteAction, NoteContext}; use notedeck_ui::{icons::search_icon, NoteOptions, ProfilePic}; /// DaveUi holds all of the data it needs to render itself @@ -27,6 +27,10 @@ impl DaveResponse { } } + fn note(action: NoteAction) -> DaveResponse { + Self::new(DaveAction::Note(action)) + } + fn or(self, r: DaveResponse) -> DaveResponse { DaveResponse { action: self.action.or(r.action), @@ -51,6 +55,7 @@ pub enum DaveAction { /// The action generated when the user sends a message to dave Send, NewChat, + Note(NoteAction), } impl<'a> DaveUi<'a> { @@ -112,18 +117,23 @@ impl<'a> DaveUi<'a> { .show(ui, |ui| self.inputbox(ui)) .inner; - egui::ScrollArea::vertical() + let note_action = egui::ScrollArea::vertical() .stick_to_bottom(true) .auto_shrink([false; 2]) .show(ui, |ui| { - Self::chat_frame(ui.ctx()).show(ui, |ui| { - ui.vertical(|ui| { - self.render_chat(app_ctx, ui); - }); - }); - }); - - r + Self::chat_frame(ui.ctx()) + .show(ui, |ui| { + ui.vertical(|ui| self.render_chat(app_ctx, ui)).inner + }) + .inner + }) + .inner; + + if let Some(action) = note_action { + DaveResponse::note(action) + } else { + r + } }) .inner }) @@ -132,21 +142,36 @@ impl<'a> DaveUi<'a> { } /// Render a chat message (user, assistant, tool call/response, etc) - fn render_chat(&self, ctx: &mut AppContext, ui: &mut egui::Ui) { + fn render_chat(&self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<NoteAction> { + let mut action: Option<NoteAction> = None; for message in self.chat { - match message { - Message::User(msg) => self.user_chat(msg, ui), - Message::Assistant(msg) => self.assistant_chat(msg, ui), - Message::ToolResponse(msg) => Self::tool_response_ui(msg, ui), + let r = match message { + Message::User(msg) => { + self.user_chat(msg, ui); + None + } + Message::Assistant(msg) => { + self.assistant_chat(msg, ui); + None + } + Message::ToolResponse(msg) => { + Self::tool_response_ui(msg, ui); + None + } Message::System(_msg) => { // system prompt is not rendered. Maybe we could // have a debug option to show this + None } - Message::ToolCalls(toolcalls) => { - Self::tool_calls_ui(ctx, toolcalls, ui); - } + Message::ToolCalls(toolcalls) => Self::tool_calls_ui(ctx, toolcalls, ui), + }; + + if r.is_some() { + action = r; } } + + action } fn tool_response_ui(_tool_response: &ToolResponse, _ui: &mut egui::Ui) { @@ -161,7 +186,11 @@ impl<'a> DaveUi<'a> { } /// The ai has asked us to render some notes, so we do that here - fn present_notes_ui(ctx: &mut AppContext, call: &PresentNotesCall, ui: &mut egui::Ui) { + fn present_notes_ui( + ctx: &mut AppContext, + call: &PresentNotesCall, + ui: &mut egui::Ui, + ) -> Option<NoteAction> { let mut note_context = NoteContext { ndb: ctx.ndb, img_cache: ctx.img_cache, @@ -177,6 +206,7 @@ impl<'a> DaveUi<'a> { .show(ui, |ui| { ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.spacing_mut().item_spacing.x = 10.0; + let mut action: Option<NoteAction> = None; for note_id in &call.note_ids { let Ok(note) = note_context.ndb.get_note_by_id(&txn, note_id.bytes()) @@ -184,26 +214,51 @@ impl<'a> DaveUi<'a> { continue; }; - let mut note_view = notedeck_ui::NoteView::new( - &mut note_context, - &None, - &note, - NoteOptions::default(), - ) - .preview_style(); - - // TODO: remove current account thing, just add to note context - ui.add_sized([400.0, 400.0], &mut note_view); + let r = ui + .allocate_ui_with_layout( + [400.0, 400.0].into(), + Layout::centered_and_justified(ui.layout().main_dir()), + |ui| { + notedeck_ui::NoteView::new( + &mut note_context, + &None, + &note, + NoteOptions::default(), + ) + .preview_style() + .show(ui) + }, + ) + .inner; + + if r.action.is_some() { + action = r.action; + } } - }); - }); + + action + }) + .inner + }) + .inner } - fn tool_calls_ui(ctx: &mut AppContext, toolcalls: &[ToolCall], ui: &mut egui::Ui) { + fn tool_calls_ui( + ctx: &mut AppContext, + toolcalls: &[ToolCall], + ui: &mut egui::Ui, + ) -> Option<NoteAction> { + let mut note_action: Option<NoteAction> = None; + ui.vertical(|ui| { for call in toolcalls { match call.calls() { - ToolCalls::PresentNotes(call) => Self::present_notes_ui(ctx, call, ui), + ToolCalls::PresentNotes(call) => { + let r = Self::present_notes_ui(ctx, call, ui); + if r.is_some() { + note_action = r; + } + } ToolCalls::Invalid(err) => { ui.label(format!("invalid tool call: {:?}", err)); } @@ -219,6 +274,8 @@ impl<'a> DaveUi<'a> { } } }); + + note_action } fn inputbox(&mut self, ui: &mut egui::Ui) -> DaveResponse {