dave: bubble note actions to chrome

This allows chrome to pass note actions to other apps
This commit is contained in:
William Casarin
2025-04-22 18:42:12 -07:00
parent 4cedea9fdb
commit e8a1233174
15 changed files with 227 additions and 59 deletions

View File

@@ -3,7 +3,8 @@ use crate::wallet::GlobalWallet;
use crate::zaps::Zaps; use crate::zaps::Zaps;
use crate::{ use crate::{
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath, 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::ThemePreference;
use egui_winit::clipboard::Clipboard; use egui_winit::clipboard::Clipboard;
@@ -15,8 +16,12 @@ use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use tracing::{error, info}; use tracing::{error, info};
pub enum AppAction {
Note(NoteAction),
}
pub trait App { 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 /// Main notedeck app framework

View File

@@ -33,7 +33,7 @@ mod wallet;
mod zaps; mod zaps;
pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction, SwitchAccountAction}; 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 args::Args;
pub use context::AppContext; pub use context::AppContext;
pub use error::{Error, FilterError, ZapError}; pub use error::{Error, FilterError, ZapError};

View File

@@ -1,4 +1,4 @@
use notedeck::AppContext; use notedeck::{AppAction, AppContext};
use notedeck_columns::Damus; use notedeck_columns::Damus;
use notedeck_dave::Dave; use notedeck_dave::Dave;
@@ -9,7 +9,7 @@ pub enum NotedeckApp {
} }
impl notedeck::App for 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 { match self {
NotedeckApp::Dave(dave) => dave.update(ctx, ui), NotedeckApp::Dave(dave) => dave.update(ctx, ui),
NotedeckApp::Columns(columns) => columns.update(ctx, ui), NotedeckApp::Columns(columns) => columns.update(ctx, ui),

View File

@@ -6,9 +6,13 @@ use egui::{vec2, Button, Label, Layout, RichText, ThemePreference, Widget};
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction}; use nostrdb::{ProfileRecord, Transaction};
use notedeck::{ 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_dave::{Dave, DaveAvatar};
use notedeck_ui::{AnimationHelper, ProfilePic}; 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 { 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) { if let Some(action) = self.show(ctx, ui) {
action.process(ctx, self, ui); action.process(ctx, self, ui);
} }
// TODO: unify this constant with the columns side panel width. ui crate? // 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() 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),
}
}
},
}
}

View File

@@ -12,7 +12,7 @@ use crate::{
Result, Result,
}; };
use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, UnknownIds}; use notedeck::{Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, UnknownIds};
use notedeck_ui::NoteOptions; use notedeck_ui::NoteOptions;
use enostr::{ClientMessage, Keypair, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; 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 { 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 self.app
.frame_history .frame_history
@@ -648,6 +648,8 @@ impl notedeck::App for Damus {
update_damus(self, ctx, ui.ctx()); update_damus(self, ctx, ui.ctx());
render_damus(self, ctx, ui); render_damus(self, ctx, ui);
None
} }
} }

View File

@@ -26,7 +26,7 @@ mod search;
mod subscriptions; mod subscriptions;
mod support; mod support;
mod test_data; mod test_data;
mod timeline; pub mod timeline;
pub mod ui; pub mod ui;
mod unknowns; mod unknowns;
mod view_state; mod view_state;

View File

@@ -5,8 +5,7 @@ use egui::{
}; };
use egui::{Layout, TextEdit}; use egui::{Layout, TextEdit};
use enostr::Keypair; use enostr::Keypair;
use notedeck::fonts::get_font_size; use notedeck::{fonts::get_font_size, AppAction, NotedeckTextStyle};
use notedeck::NotedeckTextStyle;
pub struct AccountLoginView<'a> { pub struct AccountLoginView<'a> {
manager: &'a mut AcquireKeyState, manager: &'a mut AcquireKeyState,
@@ -155,8 +154,14 @@ mod preview {
} }
impl App for AccountLoginPreview { 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); AccountLoginView::new(&mut self.manager).ui(ui);
None
} }
} }

View File

@@ -301,7 +301,7 @@ mod preview {
}; };
use super::ConfigureDeckView; use super::ConfigureDeckView;
use notedeck::{App, AppContext}; use notedeck::{App, AppAction, AppContext};
pub struct ConfigureDeckPreview { pub struct ConfigureDeckPreview {
state: DeckState, state: DeckState,
@@ -316,8 +316,14 @@ mod preview {
} }
impl App for ConfigureDeckPreview { 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); ConfigureDeckView::new(&mut self.state).ui(ui);
None
} }
} }

View File

@@ -60,7 +60,7 @@ mod preview {
}; };
use super::EditDeckView; use super::EditDeckView;
use notedeck::{App, AppContext}; use notedeck::{App, AppAction, AppContext};
pub struct EditDeckPreview { pub struct EditDeckPreview {
state: DeckState, state: DeckState,
@@ -75,8 +75,13 @@ mod preview {
} }
impl App for EditDeckPreview { 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); EditDeckView::new(&mut self.state).ui(ui);
None
} }
} }

View File

@@ -691,7 +691,7 @@ mod preview {
use crate::media_upload::Nip94Event; use crate::media_upload::Nip94Event;
use super::*; use super::*;
use notedeck::{App, AppContext}; use notedeck::{App, AppAction, AppContext};
pub struct PostPreview { pub struct PostPreview {
draft: Draft, draft: Draft,
@@ -730,7 +730,7 @@ mod preview {
} }
impl App for PostPreview { 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 txn = Transaction::new(app.ndb).expect("txn");
let mut note_context = NoteContext { let mut note_context = NoteContext {
ndb: app.ndb, ndb: app.ndb,
@@ -749,6 +749,8 @@ mod preview {
NoteOptions::default(), NoteOptions::default(),
) )
.ui(&txn, ui); .ui(&txn, ui);
None
} }
} }

View File

@@ -1,3 +1,5 @@
use notedeck::AppAction;
pub struct PreviewConfig { pub struct PreviewConfig {
pub is_mobile: bool, pub is_mobile: bool,
} }
@@ -20,7 +22,12 @@ impl PreviewApp {
} }
impl notedeck::App for 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); self.view.update(app_ctx, ui);
None
} }
} }

View File

@@ -167,7 +167,7 @@ fn button(text: &str, width: f32) -> egui::Button<'static> {
} }
mod preview { mod preview {
use notedeck::App; use notedeck::{App, AppAction};
use crate::{ use crate::{
profile_state::ProfileState, profile_state::ProfileState,
@@ -190,8 +190,13 @@ mod preview {
} }
impl App for EditProfilePreivew { 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); EditProfileView::new(&mut self.state, ctx.img_cache).ui(ui);
None
} }
} }

View File

@@ -274,7 +274,7 @@ fn get_connection_icon(status: RelayStatus) -> egui::Image<'static> {
mod preview { mod preview {
use super::*; use super::*;
use crate::test_data::sample_pool; use crate::test_data::sample_pool;
use notedeck::{App, AppContext}; use notedeck::{App, AppAction, AppContext};
pub struct RelayViewPreview { pub struct RelayViewPreview {
pool: RelayPool, pool: RelayPool,
@@ -289,7 +289,7 @@ mod preview {
} }
impl App for RelayViewPreview { 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(); self.pool.try_recv();
let mut id_string_map = HashMap::new(); let mut id_string_map = HashMap::new();
RelayView::new( RelayView::new(
@@ -298,6 +298,7 @@ mod preview {
&mut id_string_map, &mut id_string_map,
) )
.ui(ui); .ui(ui);
None
} }
} }

View File

@@ -7,7 +7,7 @@ use chrono::{Duration, Local};
use egui_wgpu::RenderState; use egui_wgpu::RenderState;
use futures::StreamExt; use futures::StreamExt;
use nostrdb::Transaction; use nostrdb::Transaction;
use notedeck::AppContext; use notedeck::{AppAction, AppContext};
use std::collections::HashMap; use std::collections::HashMap;
use std::string::ToString; use std::string::ToString;
use std::sync::mpsc::{self, Receiver}; 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 { 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 self.app
.frame_history .frame_history
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); .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()); //update_dave(self, ctx, ui.ctx());
let should_send = self.process_events(ctx); let should_send = self.process_events(ctx);
if let Some(action) = self.ui(ctx, ui).action { if let Some(action) = self.ui(ctx, ui).action {
match action { match action {
DaveAction::Note(n) => {
app_action = Some(AppAction::Note(n));
}
DaveAction::NewChat => { DaveAction::NewChat => {
self.handle_new_chat(); self.handle_new_chat();
} }
@@ -332,8 +336,11 @@ impl notedeck::App for Dave {
} }
} }
} }
if should_send { if should_send {
self.send_user_message(ctx, ui.ctx()); self.send_user_message(ctx, ui.ctx());
} }
app_action
} }
} }

View File

@@ -4,7 +4,7 @@ use crate::{
}; };
use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers};
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::{AppContext, NoteContext}; use notedeck::{AppContext, NoteAction, NoteContext};
use notedeck_ui::{icons::search_icon, NoteOptions, ProfilePic}; use notedeck_ui::{icons::search_icon, NoteOptions, ProfilePic};
/// DaveUi holds all of the data it needs to render itself /// 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 { fn or(self, r: DaveResponse) -> DaveResponse {
DaveResponse { DaveResponse {
action: self.action.or(r.action), action: self.action.or(r.action),
@@ -51,6 +55,7 @@ pub enum DaveAction {
/// The action generated when the user sends a message to dave /// The action generated when the user sends a message to dave
Send, Send,
NewChat, NewChat,
Note(NoteAction),
} }
impl<'a> DaveUi<'a> { impl<'a> DaveUi<'a> {
@@ -112,18 +117,23 @@ impl<'a> DaveUi<'a> {
.show(ui, |ui| self.inputbox(ui)) .show(ui, |ui| self.inputbox(ui))
.inner; .inner;
egui::ScrollArea::vertical() let note_action = egui::ScrollArea::vertical()
.stick_to_bottom(true) .stick_to_bottom(true)
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
Self::chat_frame(ui.ctx()).show(ui, |ui| { Self::chat_frame(ui.ctx())
ui.vertical(|ui| { .show(ui, |ui| {
self.render_chat(app_ctx, ui); ui.vertical(|ui| self.render_chat(app_ctx, ui)).inner
}); })
}); .inner
}); })
.inner;
r if let Some(action) = note_action {
DaveResponse::note(action)
} else {
r
}
}) })
.inner .inner
}) })
@@ -132,21 +142,36 @@ impl<'a> DaveUi<'a> {
} }
/// Render a chat message (user, assistant, tool call/response, etc) /// 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 { for message in self.chat {
match message { let r = match message {
Message::User(msg) => self.user_chat(msg, ui), Message::User(msg) => {
Message::Assistant(msg) => self.assistant_chat(msg, ui), self.user_chat(msg, ui);
Message::ToolResponse(msg) => Self::tool_response_ui(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) => { Message::System(_msg) => {
// system prompt is not rendered. Maybe we could // system prompt is not rendered. Maybe we could
// have a debug option to show this // have a debug option to show this
None
} }
Message::ToolCalls(toolcalls) => { Message::ToolCalls(toolcalls) => Self::tool_calls_ui(ctx, toolcalls, ui),
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) { 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 /// 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 { let mut note_context = NoteContext {
ndb: ctx.ndb, ndb: ctx.ndb,
img_cache: ctx.img_cache, img_cache: ctx.img_cache,
@@ -177,6 +206,7 @@ impl<'a> DaveUi<'a> {
.show(ui, |ui| { .show(ui, |ui| {
ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
ui.spacing_mut().item_spacing.x = 10.0; ui.spacing_mut().item_spacing.x = 10.0;
let mut action: Option<NoteAction> = None;
for note_id in &call.note_ids { for note_id in &call.note_ids {
let Ok(note) = note_context.ndb.get_note_by_id(&txn, note_id.bytes()) let Ok(note) = note_context.ndb.get_note_by_id(&txn, note_id.bytes())
@@ -184,26 +214,51 @@ impl<'a> DaveUi<'a> {
continue; continue;
}; };
let mut note_view = notedeck_ui::NoteView::new( let r = ui
&mut note_context, .allocate_ui_with_layout(
&None, [400.0, 400.0].into(),
&note, Layout::centered_and_justified(ui.layout().main_dir()),
NoteOptions::default(), |ui| {
) notedeck_ui::NoteView::new(
.preview_style(); &mut note_context,
&None,
&note,
NoteOptions::default(),
)
.preview_style()
.show(ui)
},
)
.inner;
// TODO: remove current account thing, just add to note context if r.action.is_some() {
ui.add_sized([400.0, 400.0], &mut note_view); 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| { ui.vertical(|ui| {
for call in toolcalls { for call in toolcalls {
match call.calls() { 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) => { ToolCalls::Invalid(err) => {
ui.label(format!("invalid tool call: {:?}", 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 { fn inputbox(&mut self, ui: &mut egui::Ui) -> DaveResponse {