Merge remote-tracking branches 'github/pr/864' and 'github/pr/866'
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
column::Columns,
|
||||
route::{Route, Router},
|
||||
nav::{RouterAction, RouterType},
|
||||
route::Route,
|
||||
timeline::{ThreadSelection, TimelineCache, TimelineKind},
|
||||
};
|
||||
|
||||
@@ -21,12 +22,16 @@ pub enum TimelineOpenResult {
|
||||
NewNotes(NewNotes),
|
||||
}
|
||||
|
||||
struct NoteActionResponse {
|
||||
timeline_res: Option<TimelineOpenResult>,
|
||||
router_action: Option<RouterAction>,
|
||||
}
|
||||
|
||||
/// The note action executor for notedeck_columns
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn execute_note_action(
|
||||
action: NoteAction,
|
||||
ndb: &Ndb,
|
||||
router: &mut Router<Route>,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
note_cache: &mut NoteCache,
|
||||
pool: &mut RelayPool,
|
||||
@@ -35,43 +40,45 @@ fn execute_note_action(
|
||||
global_wallet: &mut GlobalWallet,
|
||||
zaps: &mut Zaps,
|
||||
images: &mut Images,
|
||||
router_type: RouterType,
|
||||
ui: &mut egui::Ui,
|
||||
) -> Option<TimelineOpenResult> {
|
||||
) -> NoteActionResponse {
|
||||
let mut timeline_res = None;
|
||||
let mut router_action = None;
|
||||
|
||||
match action {
|
||||
NoteAction::Reply(note_id) => {
|
||||
router.route_to(Route::reply(note_id));
|
||||
None
|
||||
router_action = Some(RouterAction::route_to(Route::reply(note_id)));
|
||||
}
|
||||
NoteAction::Profile(pubkey) => {
|
||||
let kind = TimelineKind::Profile(pubkey);
|
||||
router.route_to(Route::Timeline(kind.clone()));
|
||||
timeline_cache.open(ndb, note_cache, txn, pool, &kind)
|
||||
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone())));
|
||||
timeline_res = timeline_cache.open(ndb, note_cache, txn, pool, &kind);
|
||||
}
|
||||
NoteAction::Note(note_id) => 'ex: {
|
||||
let Ok(thread_selection) = ThreadSelection::from_note_id(ndb, note_cache, txn, note_id)
|
||||
else {
|
||||
tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes()));
|
||||
break 'ex None;
|
||||
break 'ex;
|
||||
};
|
||||
|
||||
let kind = TimelineKind::Thread(thread_selection);
|
||||
router.route_to(Route::Timeline(kind.clone()));
|
||||
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone())));
|
||||
// NOTE!!: you need the note_id to timeline root id thing
|
||||
|
||||
timeline_cache.open(ndb, note_cache, txn, pool, &kind)
|
||||
timeline_res = timeline_cache.open(ndb, note_cache, txn, pool, &kind);
|
||||
}
|
||||
NoteAction::Hashtag(htag) => {
|
||||
let kind = TimelineKind::Hashtag(htag.clone());
|
||||
router.route_to(Route::Timeline(kind.clone()));
|
||||
timeline_cache.open(ndb, note_cache, txn, pool, &kind)
|
||||
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone())));
|
||||
timeline_res = timeline_cache.open(ndb, note_cache, txn, pool, &kind);
|
||||
}
|
||||
NoteAction::Quote(note_id) => {
|
||||
router.route_to(Route::quote(note_id));
|
||||
None
|
||||
router_action = Some(RouterAction::route_to(Route::quote(note_id)));
|
||||
}
|
||||
NoteAction::Zap(zap_action) => 's: {
|
||||
let Some(cur_acc) = accounts.get_selected_account_mut() else {
|
||||
break 's None;
|
||||
break 's;
|
||||
};
|
||||
|
||||
let sender = cur_acc.key.pubkey;
|
||||
@@ -88,6 +95,10 @@ fn execute_note_action(
|
||||
break 'a;
|
||||
};
|
||||
|
||||
if let RouterType::Sheet = router_type {
|
||||
router_action = Some(RouterAction::GoBack);
|
||||
}
|
||||
|
||||
send_zap(
|
||||
&sender,
|
||||
zaps,
|
||||
@@ -98,26 +109,26 @@ fn execute_note_action(
|
||||
}
|
||||
ZapAction::ClearError(target) => clear_zap_error(&sender, zaps, target),
|
||||
ZapAction::CustomizeAmount(target) => {
|
||||
router.route_to(Route::CustomizeZapAmount(target.to_owned()))
|
||||
let route = Route::CustomizeZapAmount(target.to_owned());
|
||||
router_action = Some(RouterAction::route_to_sheet(route));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
NoteAction::Context(context) => {
|
||||
match ndb.get_note_by_key(txn, context.note_key) {
|
||||
Err(err) => tracing::error!("{err}"),
|
||||
Ok(note) => {
|
||||
context.action.process(ui, ¬e, pool);
|
||||
}
|
||||
NoteAction::Context(context) => match ndb.get_note_by_key(txn, context.note_key) {
|
||||
Err(err) => tracing::error!("{err}"),
|
||||
Ok(note) => {
|
||||
context.action.process(ui, ¬e, pool);
|
||||
}
|
||||
None
|
||||
}
|
||||
},
|
||||
NoteAction::Media(media_action) => {
|
||||
media_action.process(images);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
NoteActionResponse {
|
||||
timeline_res,
|
||||
router_action,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a NoteAction and process the result
|
||||
@@ -137,12 +148,20 @@ pub fn execute_and_process_note_action(
|
||||
zaps: &mut Zaps,
|
||||
images: &mut Images,
|
||||
ui: &mut egui::Ui,
|
||||
) {
|
||||
let router = columns.column_mut(col).router_mut();
|
||||
if let Some(br) = execute_note_action(
|
||||
) -> Option<RouterAction> {
|
||||
let router_type = {
|
||||
let sheet_router = &mut columns.column_mut(col).sheet_router;
|
||||
|
||||
if sheet_router.route().is_some() {
|
||||
RouterType::Sheet
|
||||
} else {
|
||||
RouterType::Stack
|
||||
}
|
||||
};
|
||||
|
||||
let resp = execute_note_action(
|
||||
action,
|
||||
ndb,
|
||||
router,
|
||||
timeline_cache,
|
||||
note_cache,
|
||||
pool,
|
||||
@@ -151,10 +170,15 @@ pub fn execute_and_process_note_action(
|
||||
global_wallet,
|
||||
zaps,
|
||||
images,
|
||||
router_type,
|
||||
ui,
|
||||
) {
|
||||
);
|
||||
|
||||
if let Some(br) = resp.timeline_res {
|
||||
br.process(ndb, note_cache, txn, timeline_cache, unknown_ids);
|
||||
}
|
||||
|
||||
resp.router_action
|
||||
}
|
||||
|
||||
fn send_zap(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
actionbar::TimelineOpenResult,
|
||||
route::{Route, Router},
|
||||
route::{Route, Router, SingletonRouter},
|
||||
timeline::{Timeline, TimelineCache, TimelineKind},
|
||||
};
|
||||
use enostr::RelayPool;
|
||||
@@ -11,13 +11,17 @@ use tracing::warn;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Column {
|
||||
router: Router<Route>,
|
||||
pub router: Router<Route>,
|
||||
pub sheet_router: SingletonRouter<Route>,
|
||||
}
|
||||
|
||||
impl Column {
|
||||
pub fn new(routes: Vec<Route>) -> Self {
|
||||
let router = Router::new(routes);
|
||||
Column { router }
|
||||
Column {
|
||||
router,
|
||||
sheet_router: SingletonRouter::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router(&self) -> &Router<Route> {
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
profile::{ProfileAction, SaveProfileChanges},
|
||||
profile_state::ProfileState,
|
||||
relay_pool_manager::RelayPoolManager,
|
||||
route::Route,
|
||||
route::{Route, Router, SingletonRouter},
|
||||
timeline::{route::render_timeline_route, TimelineCache},
|
||||
ui::{
|
||||
self,
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
Damus,
|
||||
};
|
||||
|
||||
use egui_nav::{Nav, NavAction, NavResponse, NavUiType};
|
||||
use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, PopupSheet};
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::{
|
||||
get_current_default_msats, get_current_wallet, AccountsAction, AppContext, NoteAction,
|
||||
@@ -122,7 +122,10 @@ impl From<NoteAction> for RenderNavAction {
|
||||
}
|
||||
}
|
||||
|
||||
pub type NotedeckNavResponse = NavResponse<Option<RenderNavAction>>;
|
||||
enum NotedeckNavResponse {
|
||||
Popup(PopupResponse<Option<RenderNavAction>>),
|
||||
Nav(NavResponse<Option<RenderNavAction>>),
|
||||
}
|
||||
|
||||
pub struct RenderNavResponse {
|
||||
column: usize,
|
||||
@@ -142,128 +145,209 @@ impl RenderNavResponse {
|
||||
ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
) -> bool {
|
||||
let mut switching_occured: bool = false;
|
||||
let col = self.column;
|
||||
|
||||
if let Some(action) = self.response.response.or(self.response.title_response) {
|
||||
// start returning when we're finished posting
|
||||
match action {
|
||||
RenderNavAction::Back => {
|
||||
app.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.go_back();
|
||||
}
|
||||
|
||||
RenderNavAction::RemoveColumn => {
|
||||
let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col);
|
||||
|
||||
for kind in &kinds_to_pop {
|
||||
if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
|
||||
error!("error popping timeline: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
switching_occured = true;
|
||||
}
|
||||
|
||||
RenderNavAction::PostAction(new_post_action) => {
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
match new_post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts) {
|
||||
Err(err) => tracing::error!("Error executing post action: {err}"),
|
||||
Ok(_) => tracing::debug!("Post action executed"),
|
||||
}
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.go_back();
|
||||
}
|
||||
|
||||
RenderNavAction::NoteAction(note_action) => {
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
|
||||
crate::actionbar::execute_and_process_note_action(
|
||||
note_action,
|
||||
ctx.ndb,
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
|
||||
col,
|
||||
&mut app.timeline_cache,
|
||||
ctx.note_cache,
|
||||
ctx.pool,
|
||||
&txn,
|
||||
ctx.unknown_ids,
|
||||
ctx.accounts,
|
||||
ctx.global_wallet,
|
||||
ctx.zaps,
|
||||
ctx.img_cache,
|
||||
ui,
|
||||
);
|
||||
}
|
||||
|
||||
RenderNavAction::SwitchingAction(switching_action) => {
|
||||
switching_occured = switching_action.process(
|
||||
&mut app.timeline_cache,
|
||||
&mut app.decks_cache,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
RenderNavAction::ProfileAction(profile_action) => {
|
||||
profile_action.process(
|
||||
&mut app.view_state.pubkey_to_profile_state,
|
||||
ctx.ndb,
|
||||
ctx.pool,
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.column_mut(col)
|
||||
.router_mut(),
|
||||
);
|
||||
}
|
||||
RenderNavAction::WalletAction(wallet_action) => {
|
||||
let router = get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.column_mut(col)
|
||||
.router_mut();
|
||||
wallet_action.process(ctx.accounts, ctx.global_wallet, router)
|
||||
}
|
||||
match self.response {
|
||||
NotedeckNavResponse::Popup(nav_action) => {
|
||||
process_popup_resp(nav_action, app, ctx, ui, self.column);
|
||||
false
|
||||
}
|
||||
NotedeckNavResponse::Nav(nav_response) => {
|
||||
process_nav_resp(app, ctx, ui, nav_response, self.column)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(action) = self.response.action {
|
||||
match action {
|
||||
NavAction::Returned => {
|
||||
let r = app
|
||||
.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.pop();
|
||||
|
||||
if let Some(Route::Timeline(kind)) = &r {
|
||||
if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
|
||||
error!("popping timeline had an error: {err} for {:?}", kind);
|
||||
}
|
||||
};
|
||||
|
||||
switching_occured = true;
|
||||
}
|
||||
|
||||
NavAction::Navigated => {
|
||||
let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut();
|
||||
cur_router.navigating = false;
|
||||
if cur_router.is_replacing() {
|
||||
cur_router.remove_previous_routes();
|
||||
}
|
||||
switching_occured = true;
|
||||
}
|
||||
|
||||
NavAction::Dragging => {}
|
||||
NavAction::Returning => {}
|
||||
NavAction::Resetting => {}
|
||||
NavAction::Navigating => {}
|
||||
}
|
||||
}
|
||||
|
||||
switching_occured
|
||||
}
|
||||
}
|
||||
|
||||
fn process_popup_resp(
|
||||
action: PopupResponse<Option<RenderNavAction>>,
|
||||
app: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
col: usize,
|
||||
) -> bool {
|
||||
let mut switching_occured = false;
|
||||
if let Some(nav_action) = action.response {
|
||||
switching_occured = process_render_nav_action(app, ctx, ui, col, nav_action);
|
||||
}
|
||||
|
||||
if let Some(NavAction::Returned) = action.action {
|
||||
let column = app.columns_mut(ctx.accounts).column_mut(col);
|
||||
column.sheet_router.clear();
|
||||
} else if let Some(NavAction::Navigating) = action.action {
|
||||
let column = app.columns_mut(ctx.accounts).column_mut(col);
|
||||
column.sheet_router.navigating = false;
|
||||
}
|
||||
|
||||
switching_occured
|
||||
}
|
||||
|
||||
fn process_nav_resp(
|
||||
app: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
response: NavResponse<Option<RenderNavAction>>,
|
||||
col: usize,
|
||||
) -> bool {
|
||||
let mut switching_occured: bool = false;
|
||||
|
||||
if let Some(action) = response.response.or(response.title_response) {
|
||||
// start returning when we're finished posting
|
||||
|
||||
switching_occured = process_render_nav_action(app, ctx, ui, col, action);
|
||||
}
|
||||
|
||||
if let Some(action) = response.action {
|
||||
match action {
|
||||
NavAction::Returned => {
|
||||
let r = app
|
||||
.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.pop();
|
||||
|
||||
if let Some(Route::Timeline(kind)) = &r {
|
||||
if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
|
||||
error!("popping timeline had an error: {err} for {:?}", kind);
|
||||
}
|
||||
};
|
||||
|
||||
switching_occured = true;
|
||||
}
|
||||
|
||||
NavAction::Navigated => {
|
||||
let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut();
|
||||
cur_router.navigating = false;
|
||||
if cur_router.is_replacing() {
|
||||
cur_router.remove_previous_routes();
|
||||
}
|
||||
switching_occured = true;
|
||||
}
|
||||
|
||||
NavAction::Dragging => {}
|
||||
NavAction::Returning => {}
|
||||
NavAction::Resetting => {}
|
||||
NavAction::Navigating => {}
|
||||
}
|
||||
}
|
||||
|
||||
switching_occured
|
||||
}
|
||||
|
||||
pub enum RouterAction {
|
||||
GoBack,
|
||||
RouteTo(Route, RouterType),
|
||||
}
|
||||
|
||||
pub enum RouterType {
|
||||
Sheet,
|
||||
Stack,
|
||||
}
|
||||
|
||||
impl RouterAction {
|
||||
pub fn process(
|
||||
self,
|
||||
stack_router: &mut Router<Route>,
|
||||
sheet_router: &mut SingletonRouter<Route>,
|
||||
) {
|
||||
match self {
|
||||
RouterAction::GoBack => {
|
||||
if sheet_router.route().is_some() {
|
||||
sheet_router.go_back();
|
||||
} else {
|
||||
stack_router.go_back();
|
||||
}
|
||||
}
|
||||
RouterAction::RouteTo(route, router_type) => match router_type {
|
||||
RouterType::Sheet => sheet_router.route_to(route),
|
||||
RouterType::Stack => stack_router.route_to(route),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route_to(route: Route) -> Self {
|
||||
RouterAction::RouteTo(route, RouterType::Stack)
|
||||
}
|
||||
|
||||
pub fn route_to_sheet(route: Route) -> Self {
|
||||
RouterAction::RouteTo(route, RouterType::Sheet)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_render_nav_action(
|
||||
app: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
col: usize,
|
||||
action: RenderNavAction,
|
||||
) -> bool {
|
||||
let router_action = match action {
|
||||
RenderNavAction::Back => Some(RouterAction::GoBack),
|
||||
|
||||
RenderNavAction::RemoveColumn => {
|
||||
let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col);
|
||||
|
||||
for kind in &kinds_to_pop {
|
||||
if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
|
||||
error!("error popping timeline: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
RenderNavAction::PostAction(new_post_action) => {
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
match new_post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts) {
|
||||
Err(err) => tracing::error!("Error executing post action: {err}"),
|
||||
Ok(_) => tracing::debug!("Post action executed"),
|
||||
}
|
||||
|
||||
Some(RouterAction::GoBack)
|
||||
}
|
||||
|
||||
RenderNavAction::NoteAction(note_action) => {
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
|
||||
crate::actionbar::execute_and_process_note_action(
|
||||
note_action,
|
||||
ctx.ndb,
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
|
||||
col,
|
||||
&mut app.timeline_cache,
|
||||
ctx.note_cache,
|
||||
ctx.pool,
|
||||
&txn,
|
||||
ctx.unknown_ids,
|
||||
ctx.accounts,
|
||||
ctx.global_wallet,
|
||||
ctx.zaps,
|
||||
ctx.img_cache,
|
||||
ui,
|
||||
)
|
||||
}
|
||||
|
||||
RenderNavAction::SwitchingAction(switching_action) => {
|
||||
return switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx);
|
||||
}
|
||||
RenderNavAction::ProfileAction(profile_action) => profile_action.process(
|
||||
&mut app.view_state.pubkey_to_profile_state,
|
||||
ctx.ndb,
|
||||
ctx.pool,
|
||||
),
|
||||
RenderNavAction::WalletAction(wallet_action) => {
|
||||
wallet_action.process(ctx.accounts, ctx.global_wallet)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(action) = router_action {
|
||||
let cols = get_active_columns_mut(ctx.accounts, &mut app.decks_cache).column_mut(col);
|
||||
let router = &mut cols.router;
|
||||
let sheet_router = &mut cols.sheet_router;
|
||||
action.process(router, sheet_router);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn render_nav_body(
|
||||
ui: &mut egui::Ui,
|
||||
app: &mut Damus,
|
||||
@@ -626,6 +710,48 @@ pub fn render_nav(
|
||||
ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
) -> RenderNavResponse {
|
||||
if let Some(sheet_route) = app
|
||||
.columns(ctx.accounts)
|
||||
.column(col)
|
||||
.sheet_router
|
||||
.route()
|
||||
.clone()
|
||||
{
|
||||
let navigating = app
|
||||
.columns(ctx.accounts)
|
||||
.column(col)
|
||||
.sheet_router
|
||||
.navigating;
|
||||
let returning = app.columns(ctx.accounts).column(col).sheet_router.returning;
|
||||
let bg_route = app
|
||||
.columns(ctx.accounts)
|
||||
.column(col)
|
||||
.router()
|
||||
.routes()
|
||||
.last()
|
||||
.cloned();
|
||||
if let Some(bg_route) = bg_route {
|
||||
let resp = PopupSheet::new(&bg_route, &sheet_route)
|
||||
.id_source(egui::Id::new(("nav", col)))
|
||||
.navigating(navigating)
|
||||
.returning(returning)
|
||||
.with_split_percent_from_top(Percent::new(35).expect("35 <= 100"))
|
||||
.show_mut(ui, |ui, typ, route| match typ {
|
||||
NavUiType::Title => NavTitle::new(
|
||||
ctx.ndb,
|
||||
ctx.img_cache,
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
|
||||
&[route.clone()],
|
||||
col,
|
||||
)
|
||||
.show(ui),
|
||||
NavUiType::Body => render_nav_body(ui, app, ctx, route, 1, col, inner_rect),
|
||||
});
|
||||
|
||||
return RenderNavResponse::new(col, NotedeckNavResponse::Popup(resp));
|
||||
}
|
||||
};
|
||||
|
||||
let nav_response = Nav::new(
|
||||
&app.columns(ctx.accounts)
|
||||
.column(col)
|
||||
@@ -664,5 +790,5 @@ pub fn render_nav(
|
||||
}
|
||||
});
|
||||
|
||||
RenderNavResponse::new(col, nav_response)
|
||||
RenderNavResponse::new(col, NotedeckNavResponse::Nav(nav_response))
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder};
|
||||
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
profile_state::ProfileState,
|
||||
route::{Route, Router},
|
||||
};
|
||||
use crate::{nav::RouterAction, profile_state::ProfileState, route::Route};
|
||||
|
||||
pub struct SaveProfileChanges {
|
||||
pub kp: FullKeypair,
|
||||
@@ -48,12 +45,9 @@ impl ProfileAction {
|
||||
state_map: &mut HashMap<Pubkey, ProfileState>,
|
||||
ndb: &Ndb,
|
||||
pool: &mut RelayPool,
|
||||
router: &mut Router<Route>,
|
||||
) {
|
||||
) -> Option<RouterAction> {
|
||||
match self {
|
||||
ProfileAction::Edit(kp) => {
|
||||
router.route_to(Route::EditProfile(kp.pubkey));
|
||||
}
|
||||
ProfileAction::Edit(kp) => Some(RouterAction::route_to(Route::EditProfile(kp.pubkey))),
|
||||
ProfileAction::SaveChanges(changes) => {
|
||||
let raw_msg = format!("[\"EVENT\",{}]", changes.to_note().json().unwrap());
|
||||
|
||||
@@ -66,7 +60,7 @@ impl ProfileAction {
|
||||
info!("sending {}", raw_msg);
|
||||
pool.send(&enostr::ClientMessage::raw(raw_msg));
|
||||
|
||||
router.go_back();
|
||||
Some(RouterAction::GoBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,3 +361,40 @@ impl fmt::Display for Route {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SingletonRouter<R: Clone> {
|
||||
route: Option<R>,
|
||||
pub returning: bool,
|
||||
pub navigating: bool,
|
||||
}
|
||||
|
||||
impl<R: Clone> SingletonRouter<R> {
|
||||
pub fn route_to(&mut self, route: R) {
|
||||
self.navigating = true;
|
||||
self.route = Some(route);
|
||||
}
|
||||
|
||||
pub fn go_back(&mut self) {
|
||||
self.returning = true;
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.route = None;
|
||||
self.returning = false;
|
||||
}
|
||||
|
||||
pub fn route(&self) -> &Option<R> {
|
||||
&self.route
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Clone> Default for SingletonRouter<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
route: None,
|
||||
returning: false,
|
||||
navigating: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,67 +65,75 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<ProfileViewAction> {
|
||||
let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey));
|
||||
let offset_id = scroll_id.with("scroll_offset");
|
||||
|
||||
ScrollArea::vertical()
|
||||
.id_salt(scroll_id)
|
||||
.show(ui, |ui| {
|
||||
let mut action = None;
|
||||
let txn = Transaction::new(self.note_context.ndb).expect("txn");
|
||||
if let Ok(profile) = self
|
||||
.note_context
|
||||
.ndb
|
||||
.get_profile_by_pubkey(&txn, self.pubkey.bytes())
|
||||
{
|
||||
if self.profile_body(ui, profile) {
|
||||
action = Some(ProfileViewAction::EditProfile);
|
||||
}
|
||||
let mut scroll_area = ScrollArea::vertical().id_salt(scroll_id);
|
||||
|
||||
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
||||
}
|
||||
|
||||
let output = scroll_area.show(ui, |ui| {
|
||||
let mut action = None;
|
||||
let txn = Transaction::new(self.note_context.ndb).expect("txn");
|
||||
if let Ok(profile) = self
|
||||
.note_context
|
||||
.ndb
|
||||
.get_profile_by_pubkey(&txn, self.pubkey.bytes())
|
||||
{
|
||||
if self.profile_body(ui, profile) {
|
||||
action = Some(ProfileViewAction::EditProfile);
|
||||
}
|
||||
let profile_timeline = self
|
||||
.timeline_cache
|
||||
.notes(
|
||||
self.note_context.ndb,
|
||||
self.note_context.note_cache,
|
||||
&txn,
|
||||
&TimelineKind::Profile(*self.pubkey),
|
||||
)
|
||||
.get_ptr();
|
||||
|
||||
profile_timeline.selected_view =
|
||||
tabs_ui(ui, profile_timeline.selected_view, &profile_timeline.views);
|
||||
|
||||
let reversed = false;
|
||||
// poll for new notes and insert them into our existing notes
|
||||
if let Err(e) = profile_timeline.poll_notes_into_view(
|
||||
}
|
||||
let profile_timeline = self
|
||||
.timeline_cache
|
||||
.notes(
|
||||
self.note_context.ndb,
|
||||
&txn,
|
||||
self.unknown_ids,
|
||||
self.note_context.note_cache,
|
||||
reversed,
|
||||
) {
|
||||
error!("Profile::poll_notes_into_view: {e}");
|
||||
}
|
||||
|
||||
if let Some(note_action) = TimelineTabView::new(
|
||||
profile_timeline.current_view(),
|
||||
reversed,
|
||||
self.note_options,
|
||||
&txn,
|
||||
self.is_muted,
|
||||
self.note_context,
|
||||
&self
|
||||
.accounts
|
||||
.get_selected_account()
|
||||
.map(|a| (&a.key).into()),
|
||||
self.jobs,
|
||||
&TimelineKind::Profile(*self.pubkey),
|
||||
)
|
||||
.show(ui)
|
||||
{
|
||||
action = Some(ProfileViewAction::Note(note_action));
|
||||
}
|
||||
.get_ptr();
|
||||
|
||||
action
|
||||
})
|
||||
.inner
|
||||
profile_timeline.selected_view =
|
||||
tabs_ui(ui, profile_timeline.selected_view, &profile_timeline.views);
|
||||
|
||||
let reversed = false;
|
||||
// poll for new notes and insert them into our existing notes
|
||||
if let Err(e) = profile_timeline.poll_notes_into_view(
|
||||
self.note_context.ndb,
|
||||
&txn,
|
||||
self.unknown_ids,
|
||||
self.note_context.note_cache,
|
||||
reversed,
|
||||
) {
|
||||
error!("Profile::poll_notes_into_view: {e}");
|
||||
}
|
||||
|
||||
if let Some(note_action) = TimelineTabView::new(
|
||||
profile_timeline.current_view(),
|
||||
reversed,
|
||||
self.note_options,
|
||||
&txn,
|
||||
self.is_muted,
|
||||
self.note_context,
|
||||
&self
|
||||
.accounts
|
||||
.get_selected_account()
|
||||
.map(|a| (&a.key).into()),
|
||||
self.jobs,
|
||||
)
|
||||
.show(ui)
|
||||
{
|
||||
action = Some(ProfileViewAction::Note(note_action));
|
||||
}
|
||||
|
||||
action
|
||||
});
|
||||
|
||||
ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y));
|
||||
|
||||
output.inner
|
||||
}
|
||||
|
||||
fn profile_body(&mut self, ui: &mut egui::Ui, profile: ProfileRecord<'_>) -> bool {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use egui::{vec2, Align, Color32, CornerRadius, RichText, Stroke, TextEdit};
|
||||
use enostr::KeypairUnowned;
|
||||
use enostr::{KeypairUnowned, NoteId, Pubkey};
|
||||
use state::TypingType;
|
||||
|
||||
use crate::ui::timeline::TimelineTabView;
|
||||
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
|
||||
use egui_winit::clipboard::Clipboard;
|
||||
use nostrdb::{Filter, Transaction};
|
||||
use nostrdb::{Filter, Ndb, Transaction};
|
||||
use notedeck::{MuteFun, NoteAction, NoteContext, NoteRef};
|
||||
use notedeck_ui::{icons::search_icon, jobs::JobsCache, padding, NoteOptions};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -13,6 +14,8 @@ mod state;
|
||||
|
||||
pub use state::{FocusState, SearchQueryState, SearchState};
|
||||
|
||||
use super::search_results::{SearchResultsResponse, SearchResultsView};
|
||||
|
||||
pub struct SearchView<'a, 'd> {
|
||||
query: &'a mut SearchQueryState,
|
||||
note_options: NoteOptions,
|
||||
@@ -55,95 +58,201 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
||||
) -> Option<NoteAction> {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(0.0, 12.0);
|
||||
|
||||
if search_box(self.query, ui, clipboard) {
|
||||
self.execute_search(ui.ctx());
|
||||
}
|
||||
let search_resp = search_box(
|
||||
&mut self.query.string,
|
||||
self.query.focus_state.clone(),
|
||||
ui,
|
||||
clipboard,
|
||||
);
|
||||
|
||||
match self.query.state {
|
||||
SearchState::New | SearchState::Navigating => None,
|
||||
search_resp.process(self.query);
|
||||
|
||||
SearchState::Searched | SearchState::Typing => {
|
||||
if self.query.state == SearchState::Typing {
|
||||
ui.label(format!("Searching for '{}'", &self.query.string));
|
||||
} else {
|
||||
ui.label(format!(
|
||||
"Got {} results for '{}'",
|
||||
self.query.notes.notes.len(),
|
||||
&self.query.string
|
||||
));
|
||||
}
|
||||
let mut search_action = None;
|
||||
let mut note_action = None;
|
||||
match &self.query.state {
|
||||
SearchState::New | SearchState::Navigating => {}
|
||||
SearchState::Typing(TypingType::Mention(mention_name)) => 's: {
|
||||
let Ok(results) = self
|
||||
.note_context
|
||||
.ndb
|
||||
.search_profile(self.txn, mention_name, 10)
|
||||
else {
|
||||
break 's;
|
||||
};
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.show(ui, |ui| {
|
||||
let reversed = false;
|
||||
TimelineTabView::new(
|
||||
&self.query.notes,
|
||||
reversed,
|
||||
self.note_options,
|
||||
self.txn,
|
||||
self.is_muted,
|
||||
self.note_context,
|
||||
self.cur_acc,
|
||||
self.jobs,
|
||||
)
|
||||
.show(ui)
|
||||
})
|
||||
.inner
|
||||
let search_res = SearchResultsView::new(
|
||||
self.note_context.img_cache,
|
||||
self.note_context.ndb,
|
||||
self.txn,
|
||||
&results,
|
||||
)
|
||||
.show_in_rect(ui.available_rect_before_wrap(), ui);
|
||||
|
||||
search_action = match search_res {
|
||||
SearchResultsResponse::SelectResult(Some(index)) => {
|
||||
let Some(pk_bytes) = results.get(index) else {
|
||||
break 's;
|
||||
};
|
||||
|
||||
let username = self
|
||||
.note_context
|
||||
.ndb
|
||||
.get_profile_by_pubkey(self.txn, pk_bytes)
|
||||
.ok()
|
||||
.and_then(|p| p.record().profile().and_then(|p| p.name()))
|
||||
.unwrap_or(&self.query.string);
|
||||
|
||||
Some(SearchAction::NewSearch {
|
||||
search_type: SearchType::Profile(Pubkey::new(**pk_bytes)),
|
||||
new_search_text: format!("@{username}"),
|
||||
})
|
||||
}
|
||||
SearchResultsResponse::DeleteMention => Some(SearchAction::CloseMention),
|
||||
SearchResultsResponse::SelectResult(None) => break 's,
|
||||
};
|
||||
}
|
||||
SearchState::PerformSearch(search_type) => {
|
||||
execute_search(
|
||||
ui.ctx(),
|
||||
search_type,
|
||||
&self.query.string,
|
||||
self.note_context.ndb,
|
||||
self.txn,
|
||||
&mut self.query.notes,
|
||||
);
|
||||
search_action = Some(SearchAction::Searched);
|
||||
note_action = self.show_search_results(ui);
|
||||
}
|
||||
SearchState::Searched => {
|
||||
ui.label(format!(
|
||||
"Got {} results for '{}'",
|
||||
self.query.notes.notes.len(),
|
||||
&self.query.string
|
||||
));
|
||||
note_action = self.show_search_results(ui);
|
||||
}
|
||||
SearchState::Typing(TypingType::AutoSearch) => {
|
||||
ui.label(format!("Searching for '{}'", &self.query.string));
|
||||
|
||||
note_action = self.show_search_results(ui);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(resp) = search_action {
|
||||
resp.process(self.query);
|
||||
}
|
||||
|
||||
note_action
|
||||
}
|
||||
|
||||
fn execute_search(&mut self, ctx: &egui::Context) {
|
||||
if self.query.string.is_empty() {
|
||||
fn show_search_results(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
|
||||
egui::ScrollArea::vertical()
|
||||
.show(ui, |ui| {
|
||||
let reversed = false;
|
||||
TimelineTabView::new(
|
||||
&self.query.notes,
|
||||
reversed,
|
||||
self.note_options,
|
||||
self.txn,
|
||||
self.is_muted,
|
||||
self.note_context,
|
||||
self.cur_acc,
|
||||
self.jobs,
|
||||
)
|
||||
.show(ui)
|
||||
})
|
||||
.inner
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_search(
|
||||
ctx: &egui::Context,
|
||||
search_type: &SearchType,
|
||||
raw_input: &String,
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
tab: &mut TimelineTab,
|
||||
) {
|
||||
if raw_input.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let max_results = 500;
|
||||
|
||||
let Some(note_refs) = search_type.search(raw_input, ndb, txn, max_results) else {
|
||||
return;
|
||||
};
|
||||
|
||||
tab.notes = note_refs;
|
||||
tab.list.borrow_mut().reset();
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
enum SearchAction {
|
||||
NewSearch {
|
||||
search_type: SearchType,
|
||||
new_search_text: String,
|
||||
},
|
||||
Searched,
|
||||
CloseMention,
|
||||
}
|
||||
|
||||
impl SearchAction {
|
||||
fn process(self, state: &mut SearchQueryState) {
|
||||
match self {
|
||||
SearchAction::NewSearch {
|
||||
search_type,
|
||||
new_search_text,
|
||||
} => {
|
||||
state.state = SearchState::PerformSearch(search_type);
|
||||
state.string = new_search_text;
|
||||
}
|
||||
SearchAction::CloseMention => state.state = SearchState::New,
|
||||
SearchAction::Searched => state.state = SearchState::Searched,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchResponse {
|
||||
requested_focus: bool,
|
||||
input_changed: bool,
|
||||
}
|
||||
|
||||
impl SearchResponse {
|
||||
fn process(self, state: &mut SearchQueryState) {
|
||||
if self.requested_focus {
|
||||
state.focus_state = FocusState::RequestedFocus;
|
||||
}
|
||||
|
||||
if state.string.chars().nth(0) != Some('@') {
|
||||
if self.input_changed {
|
||||
state.state = SearchState::Typing(TypingType::AutoSearch);
|
||||
state.debouncer.bounce();
|
||||
}
|
||||
|
||||
if state.state == SearchState::Typing(TypingType::AutoSearch)
|
||||
&& state.debouncer.should_act()
|
||||
{
|
||||
state.state = SearchState::PerformSearch(SearchType::get_type(&state.string));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let max_results = 500;
|
||||
let filter = Filter::new()
|
||||
.search(&self.query.string)
|
||||
.kinds([1])
|
||||
.limit(max_results)
|
||||
.build();
|
||||
|
||||
// TODO: execute in thread
|
||||
|
||||
let before = Instant::now();
|
||||
let qrs = self
|
||||
.note_context
|
||||
.ndb
|
||||
.query(self.txn, &[filter], max_results as i32);
|
||||
let after = Instant::now();
|
||||
let duration = after - before;
|
||||
|
||||
if duration > Duration::from_millis(20) {
|
||||
warn!(
|
||||
"query took {:?}... let's update this to use a thread!",
|
||||
after - before
|
||||
);
|
||||
}
|
||||
|
||||
match qrs {
|
||||
Ok(qrs) => {
|
||||
info!(
|
||||
"queried '{}' and got {} results",
|
||||
self.query.string,
|
||||
qrs.len()
|
||||
);
|
||||
|
||||
let note_refs = qrs.into_iter().map(NoteRef::from_query_result).collect();
|
||||
self.query.notes.notes = note_refs;
|
||||
self.query.notes.list.borrow_mut().reset();
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
error!("fulltext query failed: {err}")
|
||||
if self.input_changed {
|
||||
if let Some(mention_text) = state.string.get(1..) {
|
||||
state.state = SearchState::Typing(TypingType::Mention(mention_text.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn search_box(query: &mut SearchQueryState, ui: &mut egui::Ui, clipboard: &mut Clipboard) -> bool {
|
||||
fn search_box(
|
||||
input: &mut String,
|
||||
focus_state: FocusState,
|
||||
ui: &mut egui::Ui,
|
||||
clipboard: &mut Clipboard,
|
||||
) -> SearchResponse {
|
||||
ui.horizontal(|ui| {
|
||||
// Container for search input and icon
|
||||
let search_container = egui::Frame {
|
||||
@@ -165,13 +274,13 @@ fn search_box(query: &mut SearchQueryState, ui: &mut egui::Ui, clipboard: &mut C
|
||||
// Magnifying glass icon
|
||||
ui.add(search_icon(16.0, search_height));
|
||||
|
||||
let before_len = query.string.len();
|
||||
let before_len = input.len();
|
||||
|
||||
// Search input field
|
||||
//let font_size = notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
|
||||
let response = ui.add_sized(
|
||||
[ui.available_width(), search_height],
|
||||
TextEdit::singleline(&mut query.string)
|
||||
TextEdit::singleline(input)
|
||||
.hint_text(RichText::new("Search notes...").weak())
|
||||
//.desired_width(available_width - 32.0)
|
||||
//.font(egui::FontId::new(font_size, egui::FontFamily::Proportional))
|
||||
@@ -182,37 +291,32 @@ fn search_box(query: &mut SearchQueryState, ui: &mut egui::Ui, clipboard: &mut C
|
||||
response.context_menu(|ui| {
|
||||
if ui.button("paste").clicked() {
|
||||
if let Some(text) = clipboard.get() {
|
||||
query.string.clear();
|
||||
query.string.push_str(&text);
|
||||
input.clear();
|
||||
input.push_str(&text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if response.middle_clicked() {
|
||||
if let Some(text) = clipboard.get() {
|
||||
query.string.clear();
|
||||
query.string.push_str(&text);
|
||||
input.clear();
|
||||
input.push_str(&text);
|
||||
}
|
||||
}
|
||||
|
||||
if query.focus_state == FocusState::ShouldRequestFocus {
|
||||
let mut requested_focus = false;
|
||||
if focus_state == FocusState::ShouldRequestFocus {
|
||||
response.request_focus();
|
||||
query.focus_state = FocusState::RequestedFocus;
|
||||
requested_focus = true;
|
||||
}
|
||||
|
||||
let after_len = query.string.len();
|
||||
let after_len = input.len();
|
||||
|
||||
let changed = before_len != after_len;
|
||||
if changed {
|
||||
query.mark_updated();
|
||||
}
|
||||
let input_changed = before_len != after_len;
|
||||
|
||||
// Execute search after debouncing
|
||||
if query.should_search() {
|
||||
query.mark_searched(SearchState::Searched);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
SearchResponse {
|
||||
requested_focus,
|
||||
input_changed,
|
||||
}
|
||||
})
|
||||
.inner
|
||||
@@ -221,3 +325,120 @@ fn search_box(query: &mut SearchQueryState, ui: &mut egui::Ui, clipboard: &mut C
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum SearchType {
|
||||
String,
|
||||
NoteId(NoteId),
|
||||
Profile(Pubkey),
|
||||
Hashtag(String),
|
||||
}
|
||||
|
||||
impl SearchType {
|
||||
fn get_type(query: &str) -> Self {
|
||||
if query.len() == 63 && query.starts_with("note1") {
|
||||
if let Some(noteid) = NoteId::from_bech(query) {
|
||||
return SearchType::NoteId(noteid);
|
||||
}
|
||||
} else if query.len() == 63 && query.starts_with("npub1") {
|
||||
if let Ok(pk) = Pubkey::try_from_bech32_string(query, false) {
|
||||
return SearchType::Profile(pk);
|
||||
}
|
||||
} else if query.chars().nth(0).is_some_and(|c| c == '#') {
|
||||
if let Some(hashtag) = query.get(1..) {
|
||||
return SearchType::Hashtag(hashtag.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
SearchType::String
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
raw_query: &String,
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
max_results: u64,
|
||||
) -> Option<Vec<NoteRef>> {
|
||||
match self {
|
||||
SearchType::String => search_string(raw_query, ndb, txn, max_results),
|
||||
SearchType::NoteId(noteid) => search_note(noteid, ndb, txn).map(|n| vec![n]),
|
||||
SearchType::Profile(pk) => search_pk(pk, ndb, txn, max_results),
|
||||
SearchType::Hashtag(hashtag) => search_hashtag(hashtag, ndb, txn, max_results),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn search_string(
|
||||
query: &String,
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
max_results: u64,
|
||||
) -> Option<Vec<NoteRef>> {
|
||||
let filter = Filter::new()
|
||||
.search(query)
|
||||
.kinds([1])
|
||||
.limit(max_results)
|
||||
.build();
|
||||
|
||||
// TODO: execute in thread
|
||||
|
||||
let before = Instant::now();
|
||||
let qrs = ndb.query(txn, &[filter], max_results as i32);
|
||||
let after = Instant::now();
|
||||
let duration = after - before;
|
||||
|
||||
if duration > Duration::from_millis(20) {
|
||||
warn!(
|
||||
"query took {:?}... let's update this to use a thread!",
|
||||
after - before
|
||||
);
|
||||
}
|
||||
|
||||
match qrs {
|
||||
Ok(qrs) => {
|
||||
info!("queried '{}' and got {} results", query, qrs.len());
|
||||
|
||||
return Some(qrs.into_iter().map(NoteRef::from_query_result).collect());
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
error!("fulltext query failed: {err}")
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn search_note(noteid: &NoteId, ndb: &Ndb, txn: &Transaction) -> Option<NoteRef> {
|
||||
ndb.get_note_by_id(txn, noteid.bytes())
|
||||
.ok()
|
||||
.map(|n| NoteRef::from_note(&n))
|
||||
}
|
||||
|
||||
fn search_pk(pk: &Pubkey, ndb: &Ndb, txn: &Transaction, max_results: u64) -> Option<Vec<NoteRef>> {
|
||||
let filter = Filter::new()
|
||||
.authors([pk.bytes()])
|
||||
.kinds([1])
|
||||
.limit(max_results)
|
||||
.build();
|
||||
|
||||
let qrs = ndb.query(txn, &[filter], max_results as i32).ok()?;
|
||||
Some(qrs.into_iter().map(NoteRef::from_query_result).collect())
|
||||
}
|
||||
|
||||
fn search_hashtag(
|
||||
hashtag_name: &str,
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
max_results: u64,
|
||||
) -> Option<Vec<NoteRef>> {
|
||||
let filter = Filter::new()
|
||||
.kinds([1])
|
||||
.limit(max_results)
|
||||
.tags([hashtag_name], 't')
|
||||
.build();
|
||||
|
||||
let qrs = ndb.query(txn, &[filter], max_results as i32).ok()?;
|
||||
Some(qrs.into_iter().map(NoteRef::from_query_result).collect())
|
||||
}
|
||||
|
||||
@@ -2,15 +2,24 @@ use crate::timeline::TimelineTab;
|
||||
use notedeck::debouncer::Debouncer;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::SearchType;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum SearchState {
|
||||
Typing,
|
||||
Typing(TypingType),
|
||||
PerformSearch(SearchType),
|
||||
Searched,
|
||||
Navigating,
|
||||
New,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum TypingType {
|
||||
Mention(String),
|
||||
AutoSearch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum FocusState {
|
||||
/// Get ready to focus
|
||||
Navigating,
|
||||
@@ -60,22 +69,4 @@ impl SearchQueryState {
|
||||
debouncer: Debouncer::new(Duration::from_millis(200)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_search(&self) -> bool {
|
||||
self.state == SearchState::Typing && self.debouncer.should_act()
|
||||
}
|
||||
|
||||
/// Mark the search as updated. This will update our debouncer and clear
|
||||
/// the searched flag, enabling us to search again. This should be
|
||||
/// called when the search box changes
|
||||
pub fn mark_updated(&mut self) {
|
||||
self.state = SearchState::Typing;
|
||||
self.debouncer.bounce();
|
||||
}
|
||||
|
||||
/// Call this when you are about to do a search so that we don't try
|
||||
/// to search again next frame
|
||||
pub fn mark_searched(&mut self, state: SearchState) {
|
||||
self.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,62 +54,72 @@ impl<'a, 'd> ThreadView<'a, 'd> {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
|
||||
let txn = Transaction::new(self.note_context.ndb).expect("txn");
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
let mut scroll_area = egui::ScrollArea::vertical()
|
||||
.id_salt(self.id_source)
|
||||
.animated(false)
|
||||
.auto_shrink([false, false])
|
||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
|
||||
.show(ui, |ui| {
|
||||
let root_id = match RootNoteId::new(
|
||||
self.note_context.ndb,
|
||||
self.note_context.note_cache,
|
||||
&txn,
|
||||
self.selected_note_id,
|
||||
) {
|
||||
Ok(root_id) => root_id,
|
||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible);
|
||||
|
||||
Err(err) => {
|
||||
ui.label(format!("Error loading thread: {:?}", err));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let offset_id = self.id_source.with("scroll_offset");
|
||||
|
||||
let thread_timeline = self
|
||||
.timeline_cache
|
||||
.notes(
|
||||
self.note_context.ndb,
|
||||
self.note_context.note_cache,
|
||||
&txn,
|
||||
&TimelineKind::Thread(ThreadSelection::from_root_id(root_id.to_owned())),
|
||||
)
|
||||
.get_ptr();
|
||||
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
||||
}
|
||||
|
||||
// TODO(jb55): skip poll if ThreadResult is fresh?
|
||||
let output = scroll_area.show(ui, |ui| {
|
||||
let root_id = match RootNoteId::new(
|
||||
self.note_context.ndb,
|
||||
self.note_context.note_cache,
|
||||
&txn,
|
||||
self.selected_note_id,
|
||||
) {
|
||||
Ok(root_id) => root_id,
|
||||
|
||||
let reversed = true;
|
||||
// poll for new notes and insert them into our existing notes
|
||||
if let Err(err) = thread_timeline.poll_notes_into_view(
|
||||
self.note_context.ndb,
|
||||
&txn,
|
||||
self.unknown_ids,
|
||||
self.note_context.note_cache,
|
||||
reversed,
|
||||
) {
|
||||
error!("error polling notes into thread timeline: {err}");
|
||||
Err(err) => {
|
||||
ui.label(format!("Error loading thread: {:?}", err));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
TimelineTabView::new(
|
||||
thread_timeline.current_view(),
|
||||
true,
|
||||
self.note_options,
|
||||
let thread_timeline = self
|
||||
.timeline_cache
|
||||
.notes(
|
||||
self.note_context.ndb,
|
||||
self.note_context.note_cache,
|
||||
&txn,
|
||||
self.is_muted,
|
||||
self.note_context,
|
||||
self.cur_acc,
|
||||
self.jobs,
|
||||
&TimelineKind::Thread(ThreadSelection::from_root_id(root_id.to_owned())),
|
||||
)
|
||||
.show(ui)
|
||||
})
|
||||
.inner
|
||||
.get_ptr();
|
||||
|
||||
// TODO(jb55): skip poll if ThreadResult is fresh?
|
||||
|
||||
let reversed = true;
|
||||
// poll for new notes and insert them into our existing notes
|
||||
if let Err(err) = thread_timeline.poll_notes_into_view(
|
||||
self.note_context.ndb,
|
||||
&txn,
|
||||
self.unknown_ids,
|
||||
self.note_context.note_cache,
|
||||
reversed,
|
||||
) {
|
||||
error!("error polling notes into thread timeline: {err}");
|
||||
}
|
||||
|
||||
TimelineTabView::new(
|
||||
thread_timeline.current_view(),
|
||||
true,
|
||||
self.note_options,
|
||||
&txn,
|
||||
self.is_muted,
|
||||
self.note_context,
|
||||
self.cur_acc,
|
||||
self.jobs,
|
||||
)
|
||||
.show(ui)
|
||||
});
|
||||
|
||||
ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y));
|
||||
|
||||
output.inner
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,12 @@ fn timeline_ui(
|
||||
.auto_shrink([false, false])
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible);
|
||||
|
||||
let offset_id = scroll_id.with("timeline_scroll_offset");
|
||||
|
||||
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
||||
}
|
||||
|
||||
if let Some(goto_top_resp) = goto_top_resp {
|
||||
if goto_top_resp.clicked() {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(0.0);
|
||||
@@ -163,6 +169,8 @@ fn timeline_ui(
|
||||
.show(ui)
|
||||
});
|
||||
|
||||
ui.data_mut(|d| d.insert_temp(offset_id, scroll_output.state.offset.y));
|
||||
|
||||
let at_top_after_scroll = scroll_output.state.offset.y == 0.0;
|
||||
let cur_show_top_button = ui.ctx().data(|d| d.get_temp::<bool>(show_top_button_id));
|
||||
|
||||
@@ -362,9 +370,9 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
||||
let len = self.tab.notes.len();
|
||||
|
||||
let is_muted = self.is_muted;
|
||||
|
||||
self.tab
|
||||
.list
|
||||
.clone()
|
||||
.borrow_mut()
|
||||
.ui_custom_layout(ui, len, |ui, start_index| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
|
||||
@@ -4,7 +4,7 @@ use notedeck::{
|
||||
PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet,
|
||||
};
|
||||
|
||||
use crate::route::{Route, Router};
|
||||
use crate::{nav::RouterAction, route::Route};
|
||||
|
||||
use super::widgets::styled_button;
|
||||
|
||||
@@ -55,43 +55,40 @@ impl WalletAction {
|
||||
&self,
|
||||
accounts: &mut Accounts,
|
||||
global_wallet: &mut GlobalWallet,
|
||||
router: &mut Router<Route>,
|
||||
) {
|
||||
) -> Option<RouterAction> {
|
||||
let mut action = None;
|
||||
|
||||
match &self {
|
||||
WalletAction::SaveURI => {
|
||||
let ui_state = &mut global_wallet.ui_state;
|
||||
if ui_state.for_local_only {
|
||||
ui_state.for_local_only = false;
|
||||
let Some(cur_acc) = accounts.get_selected_account_mut() else {
|
||||
return;
|
||||
};
|
||||
let cur_acc = accounts.get_selected_account_mut()?;
|
||||
|
||||
if cur_acc.wallet.is_some() {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(wallet) = try_create_wallet(ui_state) else {
|
||||
return;
|
||||
};
|
||||
let wallet = try_create_wallet(ui_state)?;
|
||||
|
||||
accounts.update_current_account(move |acc| {
|
||||
acc.wallet = Some(wallet.into());
|
||||
});
|
||||
} else {
|
||||
if global_wallet.wallet.is_some() {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(wallet) = try_create_wallet(ui_state) else {
|
||||
return;
|
||||
};
|
||||
let wallet = try_create_wallet(ui_state)?;
|
||||
|
||||
global_wallet.wallet = Some(wallet.into());
|
||||
global_wallet.save_wallet();
|
||||
}
|
||||
}
|
||||
WalletAction::AddLocalOnly => {
|
||||
router.route_to(Route::Wallet(notedeck::WalletType::Local));
|
||||
action = Some(RouterAction::route_to(Route::Wallet(
|
||||
notedeck::WalletType::Local,
|
||||
)));
|
||||
global_wallet.ui_state.for_local_only = true;
|
||||
}
|
||||
WalletAction::Delete => {
|
||||
@@ -100,7 +97,7 @@ impl WalletAction {
|
||||
accounts.update_current_account(|acc| {
|
||||
acc.wallet = None;
|
||||
});
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +150,7 @@ impl WalletAction {
|
||||
(wallet.default_zap.get_default_zap_msats() / 1000).to_string();
|
||||
}
|
||||
}
|
||||
action
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user