diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs index 3c08e8001..a9534c822 100644 --- a/crates/notedeck/src/lib.rs +++ b/crates/notedeck/src/lib.rs @@ -67,7 +67,7 @@ pub use muted::{MuteFun, Muted}; pub use name::NostrName; pub use note::{ BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef, - RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction, + OpenColumnInfo, RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction, }; pub use notecache::{CachedNote, NoteCache}; pub use options::NotedeckOptions; diff --git a/crates/notedeck/src/note/action.rs b/crates/notedeck/src/note/action.rs index 026ebb47e..93f7192da 100644 --- a/crates/notedeck/src/note/action.rs +++ b/crates/notedeck/src/note/action.rs @@ -9,6 +9,25 @@ pub struct ScrollInfo { pub offset: Vec2, } +#[derive(Debug, PartialEq)] +pub enum OpenColumnInfo { + /// User has clicked the quote reply action + Reply(NoteId), + + /// User has clicked the quote repost action + Quote(NoteId), + + /// User has clicked a hashtag + Hashtag(String), + + /// User has clicked a profile + Profile(Pubkey), + + Note { + note_id: NoteId, + }, +} + #[derive(Debug)] pub enum NoteAction { /// User has clicked the quote reply action @@ -24,7 +43,10 @@ pub enum NoteAction { Profile(Pubkey), /// User has clicked a note link - Note { note_id: NoteId, preview: bool }, + Note { + note_id: NoteId, + preview: bool, + }, /// User has selected some context option Context(ContextSelection), @@ -37,6 +59,8 @@ pub enum NoteAction { /// User scrolled the timeline Scroll(ScrollInfo), + + OpenColumn(OpenColumnInfo), } impl NoteAction { diff --git a/crates/notedeck/src/note/mod.rs b/crates/notedeck/src/note/mod.rs index 882111997..a58684bbe 100644 --- a/crates/notedeck/src/note/mod.rs +++ b/crates/notedeck/src/note/mod.rs @@ -1,7 +1,7 @@ mod action; mod context; -pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount}; +pub use action::{NoteAction, OpenColumnInfo, ScrollInfo, ZapAction, ZapTargetAmount}; pub use context::{BroadcastContext, ContextSelection, NoteContextSelection}; use crate::Accounts; diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs index 29bf6dc3a..6e0d124b7 100644 --- a/crates/notedeck_chrome/src/chrome.rs +++ b/crates/notedeck_chrome/src/chrome.rs @@ -101,6 +101,12 @@ impl ChromePanelAction { chrome.switch_to_columns(); if let Some(c) = chrome.get_columns_app().and_then(|columns| { + columns + .decks_cache + .active_columns_mut(ctx.i18n, ctx.accounts) + .unwrap() + .selected = 0; + columns .decks_cache .selected_column_mut(ctx.i18n, ctx.accounts) @@ -847,7 +853,7 @@ fn chrome_handle_app_action( if let Some(action) = m_action { let col = cols.selected_mut(); - action.process(&mut col.router, &mut col.sheet_router); + action.process(0, &mut col.router, &mut col.sheet_router); } } } @@ -903,7 +909,7 @@ fn columns_route_to_profile( if let Some(action) = m_action { let col = cols.selected_mut(); - action.process(&mut col.router, &mut col.sheet_router); + action.process(0, &mut col.router, &mut col.sheet_router); } } diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs index b156cada0..230876be7 100644 --- a/crates/notedeck_columns/src/actionbar.rs +++ b/crates/notedeck_columns/src/actionbar.rs @@ -30,6 +30,27 @@ pub enum NotesOpenResult { Thread(NewThreadNotes), } +impl NotesOpenResult { + pub fn process( + self, + threads: &mut Threads, + ndb: &Ndb, + note_cache: &mut NoteCache, + txn: &Transaction, + timeline_cache: &mut TimelineCache, + unknown_ids: &mut UnknownIds, + ) { + match self { + NotesOpenResult::Timeline(timeline_open_result) => { + timeline_open_result.process(ndb, note_cache, txn, timeline_cache, unknown_ids); + } + NotesOpenResult::Thread(thread_open_result) => { + thread_open_result.process(threads, ndb, txn, unknown_ids, note_cache); + } + } + } +} + pub enum TimelineOpenResult { NewNotes(NewNotes), } @@ -81,6 +102,9 @@ fn execute_note_action( .open(ndb, note_cache, txn, pool, &kind) .map(NotesOpenResult::Timeline); } + NoteAction::OpenColumn(args) => { + router_action = Some(RouterAction::OpenColumn(args)); + } NoteAction::Note { note_id, preview } => 'ex: { let Ok(thread_selection) = ThreadSelection::from_note_id(ndb, note_cache, txn, note_id) else { @@ -224,14 +248,7 @@ pub fn execute_and_process_note_action( ); if let Some(br) = resp.timeline_res { - match br { - NotesOpenResult::Timeline(timeline_open_result) => { - timeline_open_result.process(ndb, note_cache, txn, timeline_cache, unknown_ids); - } - NotesOpenResult::Thread(thread_open_result) => { - thread_open_result.process(threads, ndb, txn, unknown_ids, note_cache); - } - } + br.process(threads, ndb, note_cache, txn, timeline_cache, unknown_ids); } resp.router_action diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index f7846257e..9f65fa9e7 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -1,4 +1,5 @@ use crate::{ + actionbar::NotesOpenResult, args::{ColumnsArgs, ColumnsFlag}, column::Columns, decks::{Decks, DecksCache}, @@ -9,7 +10,9 @@ use crate::{ storage, subscriptions::{SubKind, Subscriptions}, support::Support, - timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind}, + timeline::{ + self, kind::ListKind, thread::Threads, ThreadSelection, TimelineCache, TimelineKind, + }, ui::{self, DesktopSidePanel, ShowSourceClientOption, SidePanelAction}, view_state::ViewState, Result, @@ -19,7 +22,7 @@ use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPo use nostrdb::Transaction; use notedeck::{ tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, - Images, JobsCache, Localization, NotedeckOptions, SettingsHandler, UnknownIds, + Images, JobsCache, Localization, NotedeckOptions, OpenColumnInfo, SettingsHandler, UnknownIds, }; use notedeck_ui::{ media::{MediaViewer, MediaViewerFlags, MediaViewerState}, @@ -574,6 +577,102 @@ impl Damus { pub fn unrecognized_args(&self) -> &BTreeSet { &self.unrecognized_args } + + fn process_open_column( + &mut self, + info: &OpenColumnInfo, + next_col: usize, + app_ctx: &mut AppContext, + ) { + match info { + OpenColumnInfo::Profile(_) | OpenColumnInfo::Hashtag(_) => { + let kind = { + if let OpenColumnInfo::Hashtag(htag) = info { + Some(TimelineKind::Hashtag(vec![htag.clone()])) + } else if let OpenColumnInfo::Profile(pk) = info { + Some(TimelineKind::Profile(*pk)) + } else { + None + } + }; + + let kind = kind.expect("kind expected"); + + let txn = Transaction::new(app_ctx.ndb).unwrap(); + + if let Some(result) = self + .timeline_cache + .open(app_ctx.ndb, app_ctx.note_cache, &txn, app_ctx.pool, &kind) + .map(NotesOpenResult::Timeline) + { + result.process( + &mut self.threads, + app_ctx.ndb, + app_ctx.note_cache, + &txn, + &mut self.timeline_cache, + app_ctx.unknown_ids, + ); + } + + let route = Route::Timeline(kind.clone()); + + let columns = self.columns_mut(app_ctx.i18n, app_ctx.accounts); + + columns.new_column_at_with_route(next_col, route); + columns.select_column(next_col as i32); + } + OpenColumnInfo::Note { note_id } => 'ex: { + let txn = Transaction::new(app_ctx.ndb).unwrap(); + let Ok(thread_selection) = + ThreadSelection::from_note_id(app_ctx.ndb, app_ctx.note_cache, &txn, *note_id) + else { + tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes())); + break 'ex; + }; + let route = Route::Thread(thread_selection.clone()); + + if let Some(result) = self + .threads + .open( + app_ctx.ndb, + &txn, + app_ctx.pool, + &thread_selection, + true, + next_col, + ) + .map(NotesOpenResult::Thread) + { + result.process( + &mut self.threads, + app_ctx.ndb, + app_ctx.note_cache, + &txn, + &mut self.timeline_cache, + app_ctx.unknown_ids, + ); + } + + let columns = self.columns_mut(app_ctx.i18n, app_ctx.accounts); + + columns.new_column_at_with_route(next_col, route); + columns.select_column(next_col as i32); + } + OpenColumnInfo::Quote(note_id) => { + let columns = self.columns_mut(app_ctx.i18n, app_ctx.accounts); + + columns.new_column_at_with_route(next_col, Route::quote(*note_id)); + columns.select_column(next_col as i32); + } + OpenColumnInfo::Reply(note_id) => { + let columns = self.columns_mut(app_ctx.i18n, app_ctx.accounts); + + columns.new_column_at_with_route(next_col, Route::reply(*note_id)); + columns.select_column(next_col as i32); + } + } + } } fn get_note_options(args: ColumnsArgs, settings_handler: &mut SettingsHandler) -> NoteOptions { @@ -651,6 +750,11 @@ fn render_damus_mobile( ProcessNavResult::PfpClicked => { app_action = Some(AppAction::ToggleChrome); } + + ProcessNavResult::OpenColumn((col, info)) => { + let next_col = *col + 1; + app.process_open_column(info, next_col, app_ctx); + } } } } @@ -767,9 +871,9 @@ fn timelines_view( ui: &mut egui::Ui, sizes: Size, app: &mut Damus, - ctx: &mut AppContext<'_>, + app_ctx: &mut AppContext<'_>, ) -> Option { - let num_cols = get_active_columns(ctx.accounts, &app.decks_cache).num_columns(); + let num_cols = get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns(); let mut side_panel_action: Option = None; let mut responses = Vec::with_capacity(num_cols); @@ -781,9 +885,9 @@ fn timelines_view( strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); let side_panel = DesktopSidePanel::new( - ctx.accounts.get_selected_account(), + app_ctx.accounts.get_selected_account(), &app.decks_cache, - ctx.i18n, + app_ctx.i18n, ) .show(ui); @@ -791,9 +895,9 @@ fn timelines_view( if side_panel.response.clicked() || side_panel.response.secondary_clicked() { if let Some(action) = DesktopSidePanel::perform_action( &mut app.decks_cache, - ctx.accounts, + app_ctx.accounts, side_panel.action, - ctx.i18n, + app_ctx.i18n, ) { side_panel_action = Some(action); } @@ -828,7 +932,7 @@ fn timelines_view( inner.set_right(rect.right() - v_line_stroke.width); inner }; - responses.push(nav::render_nav(col_index, inner_rect, app, ctx, ui)); + responses.push(nav::render_nav(col_index, inner_rect, app, app_ctx, ui)); // vertical line ui.painter() @@ -849,13 +953,18 @@ fn timelines_view( let mut save_cols = false; if let Some(action) = side_panel_action { save_cols = save_cols - || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx, ui.ctx()); + || action.process( + &mut app.timeline_cache, + &mut app.decks_cache, + app_ctx, + ui.ctx(), + ); } let mut app_action: Option = None; for response in responses { - let nav_result = response.process_render_nav_response(app, ctx, ui); + let nav_result = response.process_render_nav_response(app, app_ctx, ui); if let Some(nr) = &nav_result { match nr { @@ -864,6 +973,13 @@ fn timelines_view( ProcessNavResult::PfpClicked => { app_action = Some(AppAction::ToggleChrome); } + + ProcessNavResult::OpenColumn((col, info)) => { + let next_col = *col + 1; + app.process_open_column(info, next_col, app_ctx); + + save_cols = true + } } } } @@ -873,7 +989,7 @@ fn timelines_view( } if save_cols { - storage::save_decks_cache(ctx.path, &app.decks_cache); + storage::save_decks_cache(app_ctx.path, &app.decks_cache); } app_action diff --git a/crates/notedeck_columns/src/column.rs b/crates/notedeck_columns/src/column.rs index f961c5685..db063cc54 100644 --- a/crates/notedeck_columns/src/column.rs +++ b/crates/notedeck_columns/src/column.rs @@ -116,6 +116,14 @@ impl Columns { )])); } + pub fn new_column_at_with_route(&mut self, col: usize, route: Route) { + if col >= self.columns.len() { + self.add_column(Column::new(vec![route])); + } else { + self.add_column_at(Column::new(vec![route]), col as u32); + } + } + pub fn insert_intermediary_routes( &mut self, timeline_cache: &mut TimelineCache, @@ -194,7 +202,10 @@ impl Columns { #[inline] pub fn selected_mut(&mut self) -> &mut Column { self.ensure_column(); - assert!(self.selected < self.columns.len() as i32); + if self.selected >= self.columns.len() as i32 { + self.selected = (self.columns.len() - 1) as i32; + } + &mut self.columns[self.selected as usize] } @@ -239,6 +250,7 @@ impl Columns { if self.columns.is_empty() { self.new_column_picker(); + self.selected = 0; } kinds_to_pop diff --git a/crates/notedeck_columns/src/multi_subscriber.rs b/crates/notedeck_columns/src/multi_subscriber.rs index 4433ed98c..f3c1bb965 100644 --- a/crates/notedeck_columns/src/multi_subscriber.rs +++ b/crates/notedeck_columns/src/multi_subscriber.rs @@ -114,7 +114,7 @@ impl ThreadSubs { if scope.root_id.bytes() != id.root_id.bytes() { tracing::error!( - "Somehow the current scope's root is not equal to the selected note's root. scope's root: {:?}, thread's root: {:?}", + "ReturnType::Drag: Somehow the current scope's root is not equal to the selected note's root. scope's root: {:?}, thread's root: {:?}", scope.root_id.hex(), id.root_id.bytes() ); @@ -137,7 +137,7 @@ impl ThreadSubs { if scope.root_id.bytes() != id.root_id.bytes() { tracing::error!( - "Somehow the current scope's root is not equal to the selected note's root. scope's root: {:?}, thread's root: {:?}", + "ReturnType::Click: Somehow the current scope's root is not equal to the selected note's root. scope's root: {:?}, thread's root: {:?}", scope.root_id.hex(), id.root_id.bytes() ); @@ -192,7 +192,7 @@ fn sub_current_scope( if selection.root_id.bytes() != cur_scope.root_id.bytes() { tracing::error!( - "Somehow the current scope's root is not equal to the selected note's root" + "sub_current_scope: Somehow the current scope's root is not equal to the selected note's root" ); } diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index eed516c20..4ef126154 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -35,14 +35,16 @@ use enostr::ProfileState; use nostrdb::{Filter, Ndb, Transaction}; use notedeck::{ get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext, - RelayAction, + OpenColumnInfo, RelayAction, }; use tracing::error; /// The result of processing a nav response +#[derive(PartialEq)] pub enum ProcessNavResult { SwitchOccurred, PfpClicked, + OpenColumn((usize, OpenColumnInfo)), } impl ProcessNavResult { @@ -338,6 +340,7 @@ pub enum RouterAction { route: Route, make_new: bool, }, + OpenColumn(OpenColumnInfo), } pub enum RouterType { @@ -356,6 +359,7 @@ fn go_back(stack: &mut Router, sheet: &mut SingletonRouter) { impl RouterAction { pub fn process( self, + col: usize, stack_router: &mut Router, sheet_router: &mut SingletonRouter, ) -> Option { @@ -398,6 +402,7 @@ impl RouterAction { } None } + RouterAction::OpenColumn(info) => Some(ProcessNavResult::OpenColumn((col, info))), } } @@ -496,7 +501,7 @@ fn process_render_nav_action( let router = &mut cols.router; let sheet_router = &mut cols.sheet_router; - action.process(router, sheet_router) + action.process(col, router, sheet_router) } else { None } @@ -593,7 +598,7 @@ fn render_nav_body( .map(RenderNavAction::SettingsAction), Route::Reply(id) => { - let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { + let txn = if let Ok(txn) = Transaction::new(note_context.ndb) { txn } else { ui.label(tr!( diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs index d4ceb0f08..423e0bef5 100644 --- a/crates/notedeck_columns/src/timeline/route.rs +++ b/crates/notedeck_columns/src/timeline/route.rs @@ -84,16 +84,9 @@ pub fn render_thread_route( // We need the reply lines in threads note_options.set(NoteOptions::Wide, false); - ui::ThreadView::new( - threads, - selection.selected_or_root(), - note_options, - note_context, - jobs, - col, - ) - .ui(ui) - .map(Into::into) + ui::ThreadView::new(threads, selection, note_options, note_context, jobs, col) + .ui(ui) + .map(Into::into) } #[allow(clippy::too_many_arguments)] diff --git a/crates/notedeck_columns/src/timeline/thread.rs b/crates/notedeck_columns/src/timeline/thread.rs index 5cbbede50..5b78c58c2 100644 --- a/crates/notedeck_columns/src/timeline/thread.rs +++ b/crates/notedeck_columns/src/timeline/thread.rs @@ -229,10 +229,10 @@ impl Threads { txn: &Transaction, unknown_ids: &mut UnknownIds, col: usize, - ) { + ) -> bool { let Some(selected_key) = selected.key() else { tracing::error!("Selected note did not have a key"); - return; + return false; }; let reply = note_cache @@ -246,14 +246,14 @@ impl Threads { .expect("should be guarenteed to exist from `Self::fill_reply_chain_recursive`"); let Some(sub) = self.subs.get_local(col) else { - tracing::error!("Was expecting to find local sub"); - return; + tracing::error!("Was expecting to find local sub {}", col); + return true; }; let keys = ndb.poll_for_notes(sub.sub, 10); if keys.is_empty() { - return; + return false; } tracing::info!("Got {} new notes", keys.len()); @@ -267,6 +267,7 @@ impl Threads { unknown_ids, note_cache, ); + false } fn fill_reply_chain_recursive( diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs index e4af1db39..e79e30853 100644 --- a/crates/notedeck_columns/src/ui/column/header.rs +++ b/crates/notedeck_columns/src/ui/column/header.rs @@ -76,6 +76,17 @@ impl<'a> NavTitle<'a> { } fn title_bar(&mut self, ui: &mut egui::Ui) -> Option { + let col_idx = 1 + self.col_id; + ui.add_enabled( + false, + egui::Button::new( + RichText::new(col_idx.to_string()) + .text_style(NotedeckTextStyle::Small.text_style()), + ) + .fill(ui.visuals().noninteractive().bg_fill) + .corner_radius(8.0), + ); + let item_spacing = 8.0; ui.spacing_mut().item_spacing.x = item_spacing; diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs index e0632b585..5ff30cf45 100644 --- a/crates/notedeck_columns/src/ui/thread.rs +++ b/crates/notedeck_columns/src/ui/thread.rs @@ -8,6 +8,7 @@ use notedeck_ui::note::NoteResponse; use notedeck_ui::{NoteOptions, NoteView}; use crate::timeline::thread::{NoteSeenFlags, ParentState, Threads}; +use crate::timeline::ThreadSelection; pub struct ThreadView<'a, 'd> { threads: &'a mut Threads, @@ -22,7 +23,7 @@ impl<'a, 'd> ThreadView<'a, 'd> { #[allow(clippy::too_many_arguments)] pub fn new( threads: &'a mut Threads, - selected_note_id: &'a [u8; 32], + selection: &'a ThreadSelection, note_options: NoteOptions, note_context: &'a mut NoteContext<'d>, jobs: &'a mut JobsCache, @@ -30,7 +31,7 @@ impl<'a, 'd> ThreadView<'a, 'd> { ) -> Self { ThreadView { threads, - selected_note_id, + selected_note_id: selection.selected_or_root(), note_options, note_context, jobs, @@ -76,14 +77,16 @@ impl<'a, 'd> ThreadView<'a, 'd> { return None; }; - self.threads.update( + if self.threads.update( &cur_note, self.note_context.note_cache, self.note_context.ndb, txn, self.note_context.unknown_ids, self.col, - ); + ) { + panic!("missing thread sub"); + } let cur_node = self.threads.threads.get(&self.selected_note_id).unwrap(); diff --git a/crates/notedeck_ui/src/mention.rs b/crates/notedeck_ui/src/mention.rs index d68339c0b..2165dbea3 100644 --- a/crates/notedeck_ui/src/mention.rs +++ b/crates/notedeck_ui/src/mention.rs @@ -2,9 +2,12 @@ use crate::ProfilePreview; use egui::Sense; use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; -use notedeck::{name::get_display_name, Images, NoteAction, NotedeckTextStyle}; +use notedeck::{ + name::get_display_name, tr, Images, Localization, NoteAction, NotedeckTextStyle, OpenColumnInfo, +}; pub struct Mention<'a> { + i18n: &'a mut Localization, ndb: &'a Ndb, img_cache: &'a mut Images, txn: &'a Transaction, @@ -15,6 +18,7 @@ pub struct Mention<'a> { impl<'a> Mention<'a> { pub fn new( + i18n: &'a mut Localization, ndb: &'a Ndb, img_cache: &'a mut Images, txn: &'a Transaction, @@ -23,6 +27,7 @@ impl<'a> Mention<'a> { let size = None; let selectable = true; Mention { + i18n, ndb, img_cache, txn, @@ -44,6 +49,7 @@ impl<'a> Mention<'a> { pub fn show(self, ui: &mut egui::Ui) -> Option { mention_ui( + self.i18n, self.ndb, self.img_cache, self.txn, @@ -58,6 +64,7 @@ impl<'a> Mention<'a> { #[allow(clippy::too_many_arguments)] #[profiling::function] fn mention_ui( + i18n: &mut Localization, ndb: &Ndb, img_cache: &mut Images, txn: &Transaction, @@ -82,7 +89,7 @@ fn mention_ui( text = text.size(size); } - let resp = ui + let mut resp = ui .add( egui::Label::new(text) .sense(Sense::click()) @@ -91,8 +98,25 @@ fn mention_ui( .on_hover_cursor(egui::CursorIcon::PointingHand); let note_action = if resp.clicked() { - Some(NoteAction::Profile(Pubkey::new(*pk))) + if ui.input(|i| (i.modifiers.ctrl || i.modifiers.command)) { + Some(NoteAction::OpenColumn(OpenColumnInfo::Profile( + Pubkey::new(*pk), + ))) + } else { + Some(NoteAction::Profile(Pubkey::new(*pk))) + } } else { + if ui.input(|i| (i.modifiers.ctrl || i.modifiers.command)) { + resp = resp.on_hover_text_at_pointer(format!( + "{} + Click {}", + if ui.input(|i| (i.modifiers.command)) { + "Command" + } else { + "Ctrl" + }, + tr!(i18n, "to open profile in a new column", "") + )); + } None }; diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs index 18c42a1c3..20b3d3d42 100644 --- a/crates/notedeck_ui/src/note/contents.rs +++ b/crates/notedeck_ui/src/note/contents.rs @@ -7,7 +7,8 @@ use egui::{Color32, Hyperlink, Label, RichText}; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use notedeck::Localization; use notedeck::{ - time_format, update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle, + time_format, tr, update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, + NotedeckTextStyle, OpenColumnInfo, }; use notedeck::{JobsCache, RenderableMedia}; use tracing::warn; @@ -210,6 +211,7 @@ fn render_undecorated_note_contents<'a>( BlockType::MentionBech32 => match block.as_mention().unwrap() { Mention::Profile(profile) => { let act = crate::Mention::new( + note_context.i18n, note_context.ndb, note_context.img_cache, txn, @@ -224,6 +226,7 @@ fn render_undecorated_note_contents<'a>( Mention::Pubkey(npub) => { let act = crate::Mention::new( + note_context.i18n, note_context.ndb, note_context.img_cache, txn, @@ -266,7 +269,23 @@ fn render_undecorated_note_contents<'a>( .on_hover_cursor(egui::CursorIcon::PointingHand); if resp.clicked() { - note_action = Some(NoteAction::Hashtag(block.as_str().to_string())); + if ui.input(|i| i.modifiers.ctrl || i.modifiers.command) { + note_action = Some(NoteAction::OpenColumn(OpenColumnInfo::Hashtag( + block.as_str().to_string(), + ))); + } else { + note_action = Some(NoteAction::Hashtag(block.as_str().to_string())); + } + } else if ui.input(|i| i.modifiers.ctrl || i.modifiers.command) { + resp.on_hover_text_at_pointer(format!( + "{} + Click {}", + if ui.input(|i| (i.modifiers.command)) { + "Command" + } else { + "Ctrl" + }, + tr!(note_context.i18n, "to open hashtag in a new column", "") + )); } } diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs index 69176b685..d83fc6379 100644 --- a/crates/notedeck_ui/src/note/mod.rs +++ b/crates/notedeck_ui/src/note/mod.rs @@ -21,6 +21,7 @@ use notedeck::Images; use notedeck::JobsCache; use notedeck::Localization; use notedeck::MediaAction; +use notedeck::OpenColumnInfo; pub use options::NoteOptions; pub use reply_description::reply_desc; @@ -412,7 +413,7 @@ impl<'a, 'd> NoteView<'a, 'd> { let pfp_resp = self.pfp(note_key, profile, ui); let pfp_rect = pfp_resp.bounding_rect; note_action = pfp_resp - .into_action(self.note.pubkey()) + .into_action(self.note_context.i18n, self.note.pubkey()) .or(note_action.take()); let size = ui.available_size(); @@ -512,7 +513,8 @@ impl<'a, 'd> NoteView<'a, 'd> { ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { let pfp_resp = self.pfp(note_key, profile, ui); let pfp_rect = pfp_resp.bounding_rect; - let mut note_action: Option = pfp_resp.into_action(self.note.pubkey()); + let mut note_action: Option = + pfp_resp.into_action(self.note_context.i18n, self.note.pubkey()); ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { NoteView::note_header( @@ -622,9 +624,23 @@ impl<'a, 'd> NoteView<'a, 'd> { } } - note_action = note_hitbox_clicked(ui, hitbox_id, &response.response.rect, maybe_hitbox) - .then_some(NoteAction::note(NoteId::new(*self.note.id()))) - .or(note_action); + note_action = note_hitbox_clicked( + ui, + self.note_context.i18n, + hitbox_id, + &response.response.rect, + maybe_hitbox, + ) + .then(|| { + if ui.input(|i| (i.modifiers.ctrl || i.modifiers.command)) { + NoteAction::OpenColumn(OpenColumnInfo::Note { + note_id: NoteId::new(*self.note.id()), + }) + } else { + NoteAction::note(NoteId::new(*self.note.id())) + } + }) + .or(note_action); NoteResponse::new(response.response) .with_action(note_action) @@ -684,9 +700,34 @@ struct PfpResponse { } impl PfpResponse { - fn into_action(self, note_pk: &[u8; 32]) -> Option { + fn into_action(self, i18n: &mut Localization, note_pk: &[u8; 32]) -> Option { if self.response.clicked() { - return Some(NoteAction::Profile(Pubkey::new(*note_pk))); + if self + .response + .ctx + .input(|i| (i.modifiers.ctrl || i.modifiers.command)) + { + return Some(NoteAction::OpenColumn(OpenColumnInfo::Profile( + Pubkey::new(*note_pk), + ))); + } else { + return Some(NoteAction::Profile(Pubkey::new(*note_pk))); + } + } else if self + .response + .ctx + .input(|i| (i.modifiers.ctrl || i.modifiers.command)) + { + let pre = if self.response.ctx.input(|i| (i.modifiers.command)) { + "Command" + } else { + "Ctrl" + }; + self.response.on_hover_text_at_pointer(format!( + "{} + Click {}", + pre, + tr!(i18n, "to open profile in a new column", "") + )); } self.action.map(NoteAction::Media) @@ -779,6 +820,7 @@ fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option fn note_hitbox_clicked( ui: &mut egui::Ui, + i18n: &mut Localization, hitbox_id: egui::Id, note_rect: &Rect, maybe_hitbox: Option, @@ -791,7 +833,25 @@ fn note_hitbox_clicked( // If there was an hitbox and it was clicked open the thread match maybe_hitbox { - Some(hitbox) => hitbox.clicked(), + Some(hitbox) => { + let clicked = hitbox.clicked(); + if !clicked + && hitbox + .ctx + .input(|i| (i.modifiers.command || i.modifiers.ctrl)) + { + hitbox.on_hover_text_at_pointer(format!( + "{} + Click {}", + if ui.input(|i| (i.modifiers.command)) { + "Command" + } else { + "Ctrl" + }, + tr!(i18n, "to open note in a new column", "") + )); + } + clicked + } _ => false, } } @@ -821,11 +881,23 @@ fn render_note_actionbar( let to_noteid = |id: &[u8; 32]| NoteId::new(*id); if reply_resp.clicked() { - return Some(NoteAction::Reply(to_noteid(note_id))); + if ui.input(|i| (i.modifiers.ctrl || i.modifiers.command)) { + return Some(NoteAction::OpenColumn(OpenColumnInfo::Reply(to_noteid( + note_id, + )))); + } else { + return Some(NoteAction::Reply(to_noteid(note_id))); + } } if quote_resp.clicked() { - return Some(NoteAction::Quote(to_noteid(note_id))); + if ui.input(|i| (i.modifiers.ctrl || i.modifiers.command)) { + return Some(NoteAction::OpenColumn(OpenColumnInfo::Quote(to_noteid( + note_id, + )))); + } else { + return Some(NoteAction::Quote(to_noteid(note_id))); + } } let Zapper { zaps, cur_acc } = zapper?; diff --git a/crates/notedeck_ui/src/note/reply_description.rs b/crates/notedeck_ui/src/note/reply_description.rs index 2dedbcddf..90ff542f8 100644 --- a/crates/notedeck_ui/src/note/reply_description.rs +++ b/crates/notedeck_ui/src/note/reply_description.rs @@ -124,6 +124,7 @@ fn render_text_segments( } TextSegment::UserMention(pubkey) | TextSegment::ThreadUserMention(pubkey) => { let action = Mention::new( + note_context.i18n, note_context.ndb, note_context.img_cache, txn,