diff --git a/Cargo.lock b/Cargo.lock index 5032687..33d74d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3288,9 +3288,11 @@ dependencies = [ name = "notedeck_ui" version = "0.3.1" dependencies = [ + "bitflags 2.9.0", "egui", "egui_extras", "ehttp", + "enostr", "image", "nostrdb", "notedeck", diff --git a/crates/notedeck_columns/src/abbrev.rs b/crates/notedeck/src/abbrev.rs similarity index 100% rename from crates/notedeck_columns/src/abbrev.rs rename to crates/notedeck/src/abbrev.rs diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs index 6f448f5..53f8e1d 100644 --- a/crates/notedeck/src/lib.rs +++ b/crates/notedeck/src/lib.rs @@ -1,3 +1,4 @@ +pub mod abbrev; mod accounts; mod app; mod args; @@ -9,10 +10,12 @@ pub mod fonts; mod frame_history; mod imgcache; mod muted; +pub mod name; pub mod note; mod notecache; mod persist; pub mod platform; +pub mod profile; pub mod relay_debug; pub mod relayspec; mod result; @@ -41,9 +44,14 @@ pub use imgcache::{ MediaCacheValue, TextureFrame, TexturedImage, }; pub use muted::{MuteFun, Muted}; -pub use note::{NoteRef, RootIdError, RootNoteId, RootNoteIdBuf}; +pub use name::NostrName; +pub use note::{ + BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef, + RootIdError, RootNoteId, RootNoteIdBuf, ZapAction, +}; pub use notecache::{CachedNote, NoteCache}; pub use persist::*; +pub use profile::get_profile_url; pub use relay_debug::RelayDebugView; pub use relayspec::RelaySpec; pub use result::Result; diff --git a/crates/notedeck/src/name.rs b/crates/notedeck/src/name.rs new file mode 100644 index 0000000..c9fcf8a --- /dev/null +++ b/crates/notedeck/src/name.rs @@ -0,0 +1,64 @@ +use nostrdb::ProfileRecord; + +pub struct NostrName<'a> { + pub username: Option<&'a str>, + pub display_name: Option<&'a str>, + pub nip05: Option<&'a str>, +} + +impl<'a> NostrName<'a> { + pub fn name(&self) -> &'a str { + if let Some(name) = self.username { + name + } else if let Some(name) = self.display_name { + name + } else { + self.nip05.unwrap_or("??") + } + } + + pub fn unknown() -> Self { + Self { + username: None, + display_name: None, + nip05: None, + } + } +} + +fn is_empty(s: &str) -> bool { + s.chars().all(|c| c.is_whitespace()) +} + +pub fn get_display_name<'a>(record: Option<&ProfileRecord<'a>>) -> NostrName<'a> { + let Some(record) = record else { + return NostrName::unknown(); + }; + + let Some(profile) = record.record().profile() else { + return NostrName::unknown(); + }; + + let display_name = profile.display_name().filter(|n| !is_empty(n)); + let username = profile.name().filter(|n| !is_empty(n)); + + let nip05 = if let Some(raw_nip05) = profile.nip05() { + if let Some(at_pos) = raw_nip05.find('@') { + if raw_nip05.starts_with('_') { + raw_nip05.get(at_pos + 1..) + } else { + Some(raw_nip05) + } + } else { + None + } + } else { + None + }; + + NostrName { + username, + display_name, + nip05, + } +} diff --git a/crates/notedeck/src/note/action.rs b/crates/notedeck/src/note/action.rs new file mode 100644 index 0000000..6dbcf8a --- /dev/null +++ b/crates/notedeck/src/note/action.rs @@ -0,0 +1,33 @@ +use super::context::ContextSelection; +use crate::zaps::NoteZapTargetOwned; +use enostr::{NoteId, Pubkey}; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum NoteAction { + /// 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), + + /// User has clicked a note link + Note(NoteId), + + /// User has selected some context option + Context(ContextSelection), + + /// User has clicked the zap action + Zap(ZapAction), +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum ZapAction { + Send(NoteZapTargetOwned), + ClearError(NoteZapTargetOwned), +} diff --git a/crates/notedeck/src/note/context.rs b/crates/notedeck/src/note/context.rs new file mode 100644 index 0000000..8ba3516 --- /dev/null +++ b/crates/notedeck/src/note/context.rs @@ -0,0 +1,63 @@ +use enostr::{ClientMessage, NoteId, Pubkey, RelayPool}; +use nostrdb::{Note, NoteKey}; +use tracing::error; + +/// When broadcasting notes, this determines whether to broadcast +/// over the local network via multicast, or globally +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum BroadcastContext { + LocalNetwork, + Everywhere, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[allow(clippy::enum_variant_names)] +pub enum NoteContextSelection { + CopyText, + CopyPubkey, + CopyNoteId, + CopyNoteJSON, + Broadcast(BroadcastContext), +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct ContextSelection { + pub note_key: NoteKey, + pub action: NoteContextSelection, +} + +impl NoteContextSelection { + pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>, pool: &mut RelayPool) { + match self { + NoteContextSelection::Broadcast(context) => { + tracing::info!("Broadcasting note {}", hex::encode(note.id())); + match context { + BroadcastContext::LocalNetwork => { + pool.send_to(&ClientMessage::event(note).unwrap(), "multicast"); + } + + BroadcastContext::Everywhere => { + pool.send(&ClientMessage::event(note).unwrap()); + } + } + } + NoteContextSelection::CopyText => { + ui.ctx().copy_text(note.content().to_string()); + } + NoteContextSelection::CopyPubkey => { + if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() { + ui.ctx().copy_text(bech); + } + } + NoteContextSelection::CopyNoteId => { + if let Some(bech) = NoteId::new(*note.id()).to_bech() { + ui.ctx().copy_text(bech); + } + } + NoteContextSelection::CopyNoteJSON => match note.json() { + Ok(json) => ui.ctx().copy_text(json), + Err(err) => error!("error copying note json: {err}"), + }, + } + } +} diff --git a/crates/notedeck/src/note.rs b/crates/notedeck/src/note/mod.rs similarity index 87% rename from crates/notedeck/src/note.rs rename to crates/notedeck/src/note/mod.rs index c63ae12..772be51 100644 --- a/crates/notedeck/src/note.rs +++ b/crates/notedeck/src/note/mod.rs @@ -1,10 +1,26 @@ -use crate::notecache::NoteCache; -use enostr::NoteId; +mod action; +mod context; + +pub use action::{NoteAction, ZapAction}; +pub use context::{BroadcastContext, ContextSelection, NoteContextSelection}; + +use crate::{notecache::NoteCache, zaps::Zaps, Images}; +use enostr::{NoteId, RelayPool}; use nostrdb::{Ndb, Note, NoteKey, QueryResult, Transaction}; use std::borrow::Borrow; use std::cmp::Ordering; use std::fmt; +/// Aggregates dependencies to reduce the number of parameters +/// passed to inner UI elements, minimizing prop drilling. +pub struct NoteContext<'d> { + pub ndb: &'d Ndb, + pub img_cache: &'d mut Images, + pub note_cache: &'d mut NoteCache, + pub zaps: &'d mut Zaps, + pub pool: &'d mut RelayPool, +} + #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] pub struct NoteRef { pub key: NoteKey, diff --git a/crates/notedeck/src/profile.rs b/crates/notedeck/src/profile.rs new file mode 100644 index 0000000..ca91211 --- /dev/null +++ b/crates/notedeck/src/profile.rs @@ -0,0 +1,18 @@ +use nostrdb::ProfileRecord; + +pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { + unwrap_profile_url(profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture()))) +} + +pub fn unwrap_profile_url(maybe_url: Option<&str>) -> &str { + if let Some(url) = maybe_url { + url + } else { + no_pfp_url() + } +} + +#[inline] +pub fn no_pfp_url() -> &'static str { + "https://damus.io/img/no-profile.svg" +} diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs index a57f38b..a081274 100644 --- a/crates/notedeck_chrome/src/chrome.rs +++ b/crates/notedeck_chrome/src/chrome.rs @@ -5,10 +5,12 @@ use crate::app::NotedeckApp; use egui::{vec2, Button, Label, Layout, RichText, ThemePreference, Widget}; use egui_extras::{Size, StripBuilder}; use nostrdb::{ProfileRecord, Transaction}; -use notedeck::{App, AppContext, NotedeckTextStyle, UserAccount, WalletType}; +use notedeck::{ + profile::get_profile_url, App, AppContext, NotedeckTextStyle, UserAccount, WalletType, +}; use notedeck_columns::Damus; use notedeck_dave::{Dave, DaveAvatar}; -use notedeck_ui::{profile::get_profile_url, AnimationHelper, ProfilePic}; +use notedeck_ui::{AnimationHelper, ProfilePic}; static ICON_WIDTH: f32 = 40.0; pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; @@ -405,7 +407,7 @@ pub fn get_profile_url_owned(profile: Option>) -> &str { if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { url } else { - ProfilePic::no_pfp_url() + notedeck::profile::no_pfp_url() } } diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs index cf58439..7a3578e 100644 --- a/crates/notedeck_columns/src/actionbar.rs +++ b/crates/notedeck_columns/src/actionbar.rs @@ -1,39 +1,17 @@ use crate::{ column::Columns, route::{Route, Router}, - timeline::{TimelineCache, TimelineKind}, - ui::note::NoteContextSelection, + timeline::{ThreadSelection, TimelineCache, TimelineKind}, }; -use enostr::{NoteId, Pubkey, RelayPool}; +use enostr::{Pubkey, RelayPool}; use nostrdb::{Ndb, NoteKey, Transaction}; use notedeck::{ - get_wallet_for_mut, Accounts, GlobalWallet, NoteCache, NoteZapTargetOwned, UnknownIds, - ZapTarget, ZappingError, Zaps, + get_wallet_for_mut, Accounts, GlobalWallet, NoteAction, NoteCache, NoteZapTargetOwned, + UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps, }; use tracing::error; -#[derive(Debug, Eq, PartialEq, Clone)] -pub struct ContextSelection { - pub note_key: NoteKey, - pub action: NoteContextSelection, -} - -#[derive(Debug, Eq, PartialEq, Clone)] -pub enum NoteAction { - Reply(NoteId), - Quote(NoteId), - OpenTimeline(TimelineKind), - Context(ContextSelection), - Zap(ZapAction), -} - -#[derive(Debug, Eq, PartialEq, Clone)] -pub enum ZapAction { - Send(NoteZapTargetOwned), - ClearError(NoteZapTargetOwned), -} - pub struct NewNotes { pub id: TimelineKind, pub notes: Vec, @@ -43,106 +21,128 @@ pub enum TimelineOpenResult { NewNotes(NewNotes), } -impl NoteAction { - #[allow(clippy::too_many_arguments)] - pub fn execute( - &self, - ndb: &Ndb, - router: &mut Router, - timeline_cache: &mut TimelineCache, - note_cache: &mut NoteCache, - pool: &mut RelayPool, - txn: &Transaction, - accounts: &mut Accounts, - global_wallet: &mut GlobalWallet, - zaps: &mut Zaps, - ui: &mut egui::Ui, - ) -> Option { - match self { - NoteAction::Reply(note_id) => { - router.route_to(Route::reply(*note_id)); - None - } +/// The note action executor for notedeck_columns +#[allow(clippy::too_many_arguments)] +fn execute_note_action( + action: &NoteAction, + ndb: &Ndb, + router: &mut Router, + timeline_cache: &mut TimelineCache, + note_cache: &mut NoteCache, + pool: &mut RelayPool, + txn: &Transaction, + accounts: &mut Accounts, + global_wallet: &mut GlobalWallet, + zaps: &mut Zaps, + ui: &mut egui::Ui, +) -> Option { + match action { + NoteAction::Reply(note_id) => { + router.route_to(Route::reply(*note_id)); + None + } - NoteAction::OpenTimeline(kind) => { - router.route_to(Route::Timeline(kind.to_owned())); - timeline_cache.open(ndb, note_cache, txn, pool, kind) - } + NoteAction::Profile(pubkey) => { + let kind = TimelineKind::Profile(*pubkey); + router.route_to(Route::Timeline(kind.clone())); + timeline_cache.open(ndb, note_cache, txn, pool, &kind) + } - NoteAction::Quote(note_id) => { - router.route_to(Route::quote(*note_id)); - None - } + 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; + }; - NoteAction::Zap(zap_action) => 's: { - let Some(cur_acc) = accounts.get_selected_account_mut() else { - break 's None; - }; + let kind = TimelineKind::Thread(thread_selection); + router.route_to(Route::Timeline(kind.clone())); + // NOTE!!: you need the note_id to timeline root id thing - let sender = cur_acc.key.pubkey; + timeline_cache.open(ndb, note_cache, txn, pool, &kind) + } - match zap_action { - ZapAction::Send(target) => { - if get_wallet_for_mut(accounts, global_wallet, sender.bytes()).is_some() { - send_zap(&sender, zaps, pool, target) - } else { - zaps.send_error( - sender.bytes(), - ZapTarget::Note(target.into()), - ZappingError::SenderNoWallet, - ); - } - } - ZapAction::ClearError(target) => clear_zap_error(&sender, zaps, target), - } + 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) + } - None - } + NoteAction::Quote(note_id) => { + router.route_to(Route::quote(*note_id)); + 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::Zap(zap_action) => 's: { + let Some(cur_acc) = accounts.get_selected_account_mut() else { + break 's None; + }; + + let sender = cur_acc.key.pubkey; + + match zap_action { + ZapAction::Send(target) => { + if get_wallet_for_mut(accounts, global_wallet, sender.bytes()).is_some() { + send_zap(&sender, zaps, pool, target) + } else { + zaps.send_error( + sender.bytes(), + ZapTarget::Note(target.into()), + ZappingError::SenderNoWallet, + ); } } - None + ZapAction::ClearError(target) => clear_zap_error(&sender, zaps, target), } + + 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); + } + } + None } } +} - /// Execute the NoteAction and process the TimelineOpenResult - #[allow(clippy::too_many_arguments)] - pub fn execute_and_process_result( - &self, - ndb: &Ndb, - columns: &mut Columns, - col: usize, - timeline_cache: &mut TimelineCache, - note_cache: &mut NoteCache, - pool: &mut RelayPool, - txn: &Transaction, - unknown_ids: &mut UnknownIds, - accounts: &mut Accounts, - global_wallet: &mut GlobalWallet, - zaps: &mut Zaps, - ui: &mut egui::Ui, +/// Execute a NoteAction and process the result +#[allow(clippy::too_many_arguments)] +pub fn execute_and_process_note_action( + action: &NoteAction, + ndb: &Ndb, + columns: &mut Columns, + col: usize, + timeline_cache: &mut TimelineCache, + note_cache: &mut NoteCache, + pool: &mut RelayPool, + txn: &Transaction, + unknown_ids: &mut UnknownIds, + accounts: &mut Accounts, + global_wallet: &mut GlobalWallet, + zaps: &mut Zaps, + ui: &mut egui::Ui, +) { + let router = columns.column_mut(col).router_mut(); + if let Some(br) = execute_note_action( + action, + ndb, + router, + timeline_cache, + note_cache, + pool, + txn, + accounts, + global_wallet, + zaps, + ui, ) { - let router = columns.column_mut(col).router_mut(); - if let Some(br) = self.execute( - ndb, - router, - timeline_cache, - note_cache, - pool, - txn, - accounts, - global_wallet, - zaps, - ui, - ) { - br.process(ndb, note_cache, txn, timeline_cache, unknown_ids); - } + br.process(ndb, note_cache, txn, timeline_cache, unknown_ids); } } diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index b53642b..6977e7c 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -7,12 +7,13 @@ use crate::{ subscriptions::{SubKind, Subscriptions}, support::Support, timeline::{self, TimelineCache}, - ui::{self, note::NoteOptions, DesktopSidePanel}, + ui::{self, DesktopSidePanel}, view_state::ViewState, Result, }; use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, UnknownIds}; +use notedeck_ui::NoteOptions; use enostr::{ClientMessage, Keypair, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; use uuid::Uuid; diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs index ffa29ab..fdfb522 100644 --- a/crates/notedeck_columns/src/lib.rs +++ b/crates/notedeck_columns/src/lib.rs @@ -3,7 +3,6 @@ mod app; mod error; //mod note; //mod block; -mod abbrev; pub mod accounts; mod actionbar; pub mod app_creation; @@ -40,7 +39,6 @@ pub mod storage; pub use app::Damus; pub use error::Error; -pub use profile::NostrName; pub use route::Route; pub type Result = std::result::Result; diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index f454911..ca79083 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -1,6 +1,5 @@ use crate::{ accounts::render_accounts_route, - actionbar::NoteAction, app::{get_active_columns_mut, get_decks_mut}, column::ColumnsAction, deck_state::DeckState, @@ -16,19 +15,20 @@ use crate::{ column::NavTitle, configure_deck::ConfigureDeckView, edit_deck::{EditDeckResponse, EditDeckView}, - note::{contents::NoteContext, NewPostAction, PostAction, PostType}, + note::{NewPostAction, PostAction, PostType}, profile::EditProfileView, search::{FocusState, SearchView}, support::SupportView, wallet::{WalletAction, WalletView}, - RelayView, View, + RelayView, }, Damus, }; use egui_nav::{Nav, NavAction, NavResponse, NavUiType}; use nostrdb::Transaction; -use notedeck::{AccountsAction, AppContext, WalletState}; +use notedeck::{AccountsAction, AppContext, NoteAction, NoteContext, WalletState}; +use notedeck_ui::View; use tracing::error; #[allow(clippy::enum_variant_names)] @@ -184,7 +184,8 @@ impl RenderNavResponse { RenderNavAction::NoteAction(note_action) => { let txn = Transaction::new(ctx.ndb).expect("txn"); - note_action.execute_and_process_result( + crate::actionbar::execute_and_process_note_action( + note_action, ctx.ndb, get_active_columns_mut(ctx.accounts, &mut app.decks_cache), col, diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs index d57c8a7..7ada272 100644 --- a/crates/notedeck_columns/src/profile.rs +++ b/crates/notedeck_columns/src/profile.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use enostr::{FullKeypair, Pubkey, RelayPool}; -use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder, ProfileRecord}; +use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder}; use tracing::info; @@ -10,69 +10,6 @@ use crate::{ route::{Route, Router}, }; -pub struct NostrName<'a> { - pub username: Option<&'a str>, - pub display_name: Option<&'a str>, - pub nip05: Option<&'a str>, -} - -impl<'a> NostrName<'a> { - pub fn name(&self) -> &'a str { - if let Some(name) = self.username { - name - } else if let Some(name) = self.display_name { - name - } else { - self.nip05.unwrap_or("??") - } - } - - pub fn unknown() -> Self { - Self { - username: None, - display_name: None, - nip05: None, - } - } -} - -fn is_empty(s: &str) -> bool { - s.chars().all(|c| c.is_whitespace()) -} - -pub fn get_display_name<'a>(record: Option<&ProfileRecord<'a>>) -> NostrName<'a> { - let Some(record) = record else { - return NostrName::unknown(); - }; - - let Some(profile) = record.record().profile() else { - return NostrName::unknown(); - }; - - let display_name = profile.display_name().filter(|n| !is_empty(n)); - let username = profile.name().filter(|n| !is_empty(n)); - - let nip05 = if let Some(raw_nip05) = profile.nip05() { - if let Some(at_pos) = raw_nip05.find('@') { - if raw_nip05.starts_with('_') { - raw_nip05.get(at_pos + 1..) - } else { - Some(raw_nip05) - } - } else { - None - } - } else { - None - }; - - NostrName { - username, - display_name, - nip05, - } -} - pub struct SaveProfileChanges { pub kp: FullKeypair, pub state: ProfileState, diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs index 8f2de05..d20264f 100644 --- a/crates/notedeck_columns/src/timeline/kind.rs +++ b/crates/notedeck_columns/src/timeline/kind.rs @@ -634,7 +634,7 @@ impl<'a> TitleNeedsDb<'a> { let m_name = profile .as_ref() .ok() - .map(|p| crate::profile::get_display_name(Some(p)).name()); + .map(|p| notedeck::name::get_display_name(Some(p)).name()); m_name.unwrap_or("Profile") } else { diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs index 23cf95d..6d084ec 100644 --- a/crates/notedeck_columns/src/timeline/route.rs +++ b/crates/notedeck_columns/src/timeline/route.rs @@ -2,15 +2,12 @@ use crate::{ nav::RenderNavAction, profile::ProfileAction, timeline::{TimelineCache, TimelineKind}, - ui::{ - self, - note::{contents::NoteContext, NoteOptions}, - profile::ProfileView, - }, + ui::{self, ProfileView}, }; use enostr::Pubkey; -use notedeck::{Accounts, MuteFun, UnknownIds}; +use notedeck::{Accounts, MuteFun, NoteContext, UnknownIds}; +use notedeck_ui::NoteOptions; #[allow(clippy::too_many_arguments)] pub fn render_timeline_route( diff --git a/crates/notedeck_columns/src/ui/accounts.rs b/crates/notedeck_columns/src/ui/accounts.rs index a56a631..8a67acd 100644 --- a/crates/notedeck_columns/src/ui/accounts.rs +++ b/crates/notedeck_columns/src/ui/accounts.rs @@ -5,7 +5,7 @@ use nostrdb::{Ndb, Transaction}; use notedeck::{Accounts, Images}; use notedeck_ui::colors::PINK; -use super::profile::preview::SimpleProfilePreview; +use notedeck_ui::profile::preview::SimpleProfilePreview; pub struct AccountsView<'a> { ndb: &'a Ndb, diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs index d3f1c3b..f8ec509 100644 --- a/crates/notedeck_columns/src/ui/add_column.rs +++ b/crates/notedeck_columns/src/ui/add_column.rs @@ -13,14 +13,15 @@ use crate::{ login_manager::AcquireKeyState, route::Route, timeline::{kind::ListKind, PubkeySource, TimelineKind}, - ui::anim::ICON_EXPANSION_MULTIPLE, Damus, }; use notedeck::{AppContext, Images, NotedeckTextStyle, UserAccount}; +use notedeck_ui::anim::ICON_EXPANSION_MULTIPLE; use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; -use super::{anim::AnimationHelper, padding, widgets::styled_button, ProfilePreview}; +use crate::ui::widgets::styled_button; +use notedeck_ui::{anim::AnimationHelper, padding, ProfilePreview}; pub enum AddColumnResponse { Timeline(TimelineKind), diff --git a/crates/notedeck_columns/src/ui/anim.rs b/crates/notedeck_columns/src/ui/anim.rs deleted file mode 100644 index ba47dce..0000000 --- a/crates/notedeck_columns/src/ui/anim.rs +++ /dev/null @@ -1,138 +0,0 @@ -use egui::{Pos2, Rect, Response, Sense}; - -pub fn hover_expand( - ui: &mut egui::Ui, - id: egui::Id, - size: f32, - expand_size: f32, - anim_speed: f32, -) -> (egui::Rect, f32, egui::Response) { - // Allocate space for the profile picture with a fixed size - let default_size = size + expand_size; - let (rect, response) = - ui.allocate_exact_size(egui::vec2(default_size, default_size), egui::Sense::click()); - - let val = ui - .ctx() - .animate_bool_with_time(id, response.hovered(), anim_speed); - - let size = size + val * expand_size; - (rect, size, response) -} - -pub fn hover_expand_small(ui: &mut egui::Ui, id: egui::Id) -> (egui::Rect, f32, egui::Response) { - let size = 10.0; - let expand_size = 5.0; - let anim_speed = 0.05; - - hover_expand(ui, id, size, expand_size, anim_speed) -} - -pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; -pub static ANIM_SPEED: f32 = 0.05; -pub struct AnimationHelper { - rect: Rect, - center: Pos2, - response: Response, - animation_progress: f32, - expansion_multiple: f32, -} - -impl AnimationHelper { - pub fn new( - ui: &mut egui::Ui, - animation_name: impl std::hash::Hash, - max_size: egui::Vec2, - ) -> Self { - let id = ui.id().with(animation_name); - let (rect, response) = ui.allocate_exact_size(max_size, Sense::click()); - - let animation_progress = - ui.ctx() - .animate_bool_with_time(id, response.hovered(), ANIM_SPEED); - - Self { - rect, - center: rect.center(), - response, - animation_progress, - expansion_multiple: ICON_EXPANSION_MULTIPLE, - } - } - - pub fn no_animation(ui: &mut egui::Ui, size: egui::Vec2) -> Self { - let (rect, response) = ui.allocate_exact_size(size, Sense::hover()); - - Self { - rect, - center: rect.center(), - response, - animation_progress: 0.0, - expansion_multiple: ICON_EXPANSION_MULTIPLE, - } - } - - pub fn new_from_rect( - ui: &mut egui::Ui, - animation_name: impl std::hash::Hash, - animation_rect: egui::Rect, - ) -> Self { - let id = ui.id().with(animation_name); - let response = ui.allocate_rect(animation_rect, Sense::click()); - - let animation_progress = - ui.ctx() - .animate_bool_with_time(id, response.hovered(), ANIM_SPEED); - - Self { - rect: animation_rect, - center: animation_rect.center(), - response, - animation_progress, - expansion_multiple: ICON_EXPANSION_MULTIPLE, - } - } - - pub fn scale_1d_pos(&self, min_object_size: f32) -> f32 { - let max_object_size = min_object_size * self.expansion_multiple; - - if self.response.is_pointer_button_down_on() { - min_object_size - } else { - min_object_size + ((max_object_size - min_object_size) * self.animation_progress) - } - } - - pub fn scale_radius(&self, min_diameter: f32) -> f32 { - self.scale_1d_pos((min_diameter - 1.0) / 2.0) - } - - pub fn get_animation_rect(&self) -> egui::Rect { - self.rect - } - - pub fn center(&self) -> Pos2 { - self.rect.center() - } - - pub fn take_animation_response(self) -> egui::Response { - self.response - } - - // Scale a minimum position from center to the current animation position - pub fn scale_from_center(&self, x_min: f32, y_min: f32) -> Pos2 { - Pos2::new( - self.center.x + self.scale_1d_pos(x_min), - self.center.y + self.scale_1d_pos(y_min), - ) - } - - pub fn scale_pos_from_center(&self, min_pos: Pos2) -> Pos2 { - self.scale_from_center(min_pos.x, min_pos.y) - } - - /// New method for min/max scaling when needed - pub fn scale_1d_pos_min_max(&self, min_object_size: f32, max_object_size: f32) -> f32 { - min_object_size + ((max_object_size - min_object_size) * self.animation_progress) - } -} diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs index 2606ab9..66586eb 100644 --- a/crates/notedeck_columns/src/ui/column/header.rs +++ b/crates/notedeck_columns/src/ui/column/header.rs @@ -5,10 +5,7 @@ use crate::{ column::Columns, route::Route, timeline::{ColumnTitle, TimelineKind}, - ui::{ - self, - anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, - }, + ui::{self}, }; use egui::Margin; @@ -16,6 +13,10 @@ use egui::{RichText, Stroke, UiBuilder}; use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; use notedeck::{Images, NotedeckTextStyle}; +use notedeck_ui::{ + anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, + ProfilePic, +}; pub struct NavTitle<'a> { ndb: &'a Ndb, @@ -43,7 +44,7 @@ impl<'a> NavTitle<'a> { } pub fn show(&mut self, ui: &mut egui::Ui) -> Option { - ui::padding(8.0, ui, |ui| { + notedeck_ui::padding(8.0, ui, |ui| { let mut rect = ui.available_rect_before_wrap(); rect.set_height(48.0); @@ -72,7 +73,7 @@ impl<'a> NavTitle<'a> { if let Some(back_resp) = &back_button_resp { if back_resp.hovered() || back_resp.clicked() { - ui::show_pointer(ui); + notedeck_ui::show_pointer(ui); } } else { // add some space where chevron would have been. this makes the ui @@ -220,7 +221,7 @@ impl<'a> NavTitle<'a> { } }); } else if move_resp.hovered() { - ui::show_pointer(ui); + notedeck_ui::show_pointer(ui); } ui.data(|d| d.get_temp(cur_id)).and_then(|val| { @@ -388,14 +389,12 @@ impl<'a> NavTitle<'a> { txn: &'txn Transaction, pubkey: &[u8; 32], pfp_size: f32, - ) -> Option> { + ) -> Option> { self.ndb .get_profile_by_pubkey(txn, pubkey) .as_ref() .ok() - .and_then(move |p| { - Some(ui::ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size)) - }) + .and_then(move |p| Some(ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size))) } fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) { @@ -407,9 +406,7 @@ impl<'a> NavTitle<'a> { { ui.add(pfp); } else { - ui.add( - ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size), - ); + ui.add(ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()).size(pfp_size)); } } @@ -472,9 +469,7 @@ impl<'a> NavTitle<'a> { if let Some(pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) { ui.add(pfp); } else { - ui.add( - ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size), - ); + ui.add(ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()).size(pfp_size)); }; } diff --git a/crates/notedeck_columns/src/ui/configure_deck.rs b/crates/notedeck_columns/src/ui/configure_deck.rs index 884bd7c..6a961f9 100644 --- a/crates/notedeck_columns/src/ui/configure_deck.rs +++ b/crates/notedeck_columns/src/ui/configure_deck.rs @@ -1,10 +1,9 @@ use crate::{app_style::deck_icon_font_sized, deck_state::DeckState}; use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget}; use notedeck::{NamedFontFamily, NotedeckTextStyle}; -use notedeck_ui::colors::PINK; - -use super::{ +use notedeck_ui::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, + colors::PINK, padding, }; diff --git a/crates/notedeck_columns/src/ui/edit_deck.rs b/crates/notedeck_columns/src/ui/edit_deck.rs index e6c79dc..727f3a0 100644 --- a/crates/notedeck_columns/src/ui/edit_deck.rs +++ b/crates/notedeck_columns/src/ui/edit_deck.rs @@ -2,10 +2,8 @@ use egui::Widget; use crate::deck_state::DeckState; -use super::{ - configure_deck::{ConfigureDeckResponse, ConfigureDeckView}, - padding, -}; +use super::configure_deck::{ConfigureDeckResponse, ConfigureDeckView}; +use notedeck_ui::padding; pub struct EditDeckView<'a> { config_view: ConfigureDeckView<'a>, diff --git a/crates/notedeck_columns/src/ui/mod.rs b/crates/notedeck_columns/src/ui/mod.rs index c0e5dd0..dbc50da 100644 --- a/crates/notedeck_columns/src/ui/mod.rs +++ b/crates/notedeck_columns/src/ui/mod.rs @@ -1,12 +1,10 @@ pub mod account_login_view; pub mod accounts; pub mod add_column; -pub mod anim; pub mod column; pub mod configure_deck; pub mod edit_deck; pub mod images; -pub mod mention; pub mod note; pub mod preview; pub mod profile; @@ -17,56 +15,14 @@ pub mod side_panel; pub mod support; pub mod thread; pub mod timeline; -pub mod username; pub mod wallet; pub mod widgets; pub use accounts::AccountsView; -pub use mention::Mention; -pub use note::{NoteResponse, NoteView, PostReplyView, PostView}; -pub use notedeck_ui::ProfilePic; +pub use note::{PostReplyView, PostView}; pub use preview::{Preview, PreviewApp, PreviewConfig}; -pub use profile::ProfilePreview; +pub use profile::ProfileView; pub use relay::RelayView; pub use side_panel::{DesktopSidePanel, SidePanelAction}; pub use thread::ThreadView; pub use timeline::TimelineView; -pub use username::Username; - -use egui::Margin; - -/// This is kind of like the Widget trait but is meant for larger top-level -/// views that are typically stateful. -/// -/// The Widget trait forces us to add mutable -/// implementations at the type level, which screws us when generating Previews -/// for a Widget. I would have just Widget instead of making this Trait otherwise. -/// -/// There is some precendent for this, it looks like there's a similar trait -/// in the egui demo library. -pub trait View { - fn ui(&mut self, ui: &mut egui::Ui); -} - -pub fn padding( - amount: impl Into, - ui: &mut egui::Ui, - add_contents: impl FnOnce(&mut egui::Ui) -> R, -) -> egui::InnerResponse { - egui::Frame::new() - .inner_margin(amount) - .show(ui, add_contents) -} - -pub fn hline(ui: &egui::Ui) { - // pixel perfect horizontal line - let rect = ui.available_rect_before_wrap(); - #[allow(deprecated)] - let resize_y = ui.painter().round_to_pixel(rect.top()) - 0.5; - let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke; - ui.painter().hline(rect.x_range(), resize_y, stroke); -} - -pub fn show_pointer(ui: &egui::Ui) { - ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); -} diff --git a/crates/notedeck_columns/src/ui/note/mod.rs b/crates/notedeck_columns/src/ui/note/mod.rs index 30689f0..adf0439 100644 --- a/crates/notedeck_columns/src/ui/note/mod.rs +++ b/crates/notedeck_columns/src/ui/note/mod.rs @@ -1,765 +1,7 @@ -pub mod contents; -pub mod context; -pub mod options; pub mod post; pub mod quote_repost; pub mod reply; -pub mod reply_description; -pub use contents::NoteContents; -use contents::NoteContext; -pub use context::{NoteContextButton, NoteContextSelection}; -use notedeck_ui::ImagePulseTint; -pub use options::NoteOptions; pub use post::{NewPostAction, PostAction, PostResponse, PostType, PostView}; pub use quote_repost::QuoteRepostView; pub use reply::PostReplyView; -pub use reply_description::reply_desc; - -use crate::{ - actionbar::{ContextSelection, NoteAction, ZapAction}, - profile::get_display_name, - timeline::{ThreadSelection, TimelineKind}, - ui::{self, View}, -}; - -use egui::emath::{pos2, Vec2}; -use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense}; -use enostr::{KeypairUnowned, NoteId, Pubkey}; -use nostrdb::{Ndb, Note, NoteKey, Transaction}; -use notedeck::{ - AnyZapState, CachedNote, NoteCache, NoteZapTarget, NoteZapTargetOwned, NotedeckTextStyle, - ZapTarget, Zaps, -}; - -use super::{profile::preview::one_line_display_name_widget, widgets::x_button}; - -pub struct NoteView<'a, 'd> { - note_context: &'a mut NoteContext<'d>, - cur_acc: &'a Option>, - parent: Option, - note: &'a nostrdb::Note<'a>, - flags: NoteOptions, -} - -pub struct NoteResponse { - pub response: egui::Response, - pub action: Option, -} - -impl NoteResponse { - pub fn new(response: egui::Response) -> Self { - Self { - response, - action: None, - } - } - - pub fn with_action(mut self, action: Option) -> Self { - self.action = action; - self - } -} - -impl View for NoteView<'_, '_> { - fn ui(&mut self, ui: &mut egui::Ui) { - self.show(ui); - } -} - -impl<'a, 'd> NoteView<'a, 'd> { - pub fn new( - note_context: &'a mut NoteContext<'d>, - cur_acc: &'a Option>, - note: &'a nostrdb::Note<'a>, - mut flags: NoteOptions, - ) -> Self { - flags.set_actionbar(true); - flags.set_note_previews(true); - - let parent: Option = None; - Self { - note_context, - cur_acc, - parent, - note, - flags, - } - } - - pub fn textmode(mut self, enable: bool) -> Self { - self.options_mut().set_textmode(enable); - self - } - - pub fn actionbar(mut self, enable: bool) -> Self { - self.options_mut().set_actionbar(enable); - self - } - - pub fn small_pfp(mut self, enable: bool) -> Self { - self.options_mut().set_small_pfp(enable); - self - } - - pub fn medium_pfp(mut self, enable: bool) -> Self { - self.options_mut().set_medium_pfp(enable); - self - } - - pub fn note_previews(mut self, enable: bool) -> Self { - self.options_mut().set_note_previews(enable); - self - } - - pub fn selectable_text(mut self, enable: bool) -> Self { - self.options_mut().set_selectable_text(enable); - self - } - - pub fn wide(mut self, enable: bool) -> Self { - self.options_mut().set_wide(enable); - self - } - - pub fn options_button(mut self, enable: bool) -> Self { - self.options_mut().set_options_button(enable); - self - } - - pub fn options(&self) -> NoteOptions { - self.flags - } - - pub fn options_mut(&mut self) -> &mut NoteOptions { - &mut self.flags - } - - pub fn parent(mut self, parent: NoteKey) -> Self { - self.parent = Some(parent); - self - } - - pub fn is_preview(mut self, is_preview: bool) -> Self { - self.options_mut().set_is_preview(is_preview); - self - } - - fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - let note_key = self.note.key().expect("todo: implement non-db notes"); - let txn = self.note.txn().expect("todo: implement non-db notes"); - - ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { - let profile = self - .note_context - .ndb - .get_profile_by_pubkey(txn, self.note.pubkey()); - - //ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 2.0; - - let cached_note = self - .note_context - .note_cache - .cached_note_or_insert_mut(note_key, self.note); - - let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0)); - ui.allocate_rect(rect, Sense::hover()); - ui.put(rect, |ui: &mut egui::Ui| { - render_reltime(ui, cached_note, false).response - }); - let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0)); - ui.allocate_rect(rect, Sense::hover()); - ui.put(rect, |ui: &mut egui::Ui| { - ui.add( - ui::Username::new(profile.as_ref().ok(), self.note.pubkey()) - .abbreviated(6) - .pk_colored(true), - ) - }); - - ui.add(&mut NoteContents::new( - self.note_context, - self.cur_acc, - txn, - self.note, - self.flags, - )); - //}); - }) - .response - } - - pub fn expand_size() -> i8 { - 5 - } - - fn pfp( - &mut self, - note_key: NoteKey, - profile: &Result, nostrdb::Error>, - ui: &mut egui::Ui, - ) -> egui::Response { - if !self.options().has_wide() { - ui.spacing_mut().item_spacing.x = 16.0; - } else { - ui.spacing_mut().item_spacing.x = 4.0; - } - - let pfp_size = self.options().pfp_size(); - - let sense = Sense::click(); - match profile - .as_ref() - .ok() - .and_then(|p| p.record().profile()?.picture()) - { - // these have different lifetimes and types, - // so the calls must be separate - Some(pic) => { - let anim_speed = 0.05; - let profile_key = profile.as_ref().unwrap().record().note_key(); - let note_key = note_key.as_u64(); - - let (rect, size, resp) = ui::anim::hover_expand( - ui, - egui::Id::new((profile_key, note_key)), - pfp_size as f32, - ui::NoteView::expand_size() as f32, - anim_speed, - ); - - ui.put( - rect, - ui::ProfilePic::new(self.note_context.img_cache, pic).size(size), - ) - .on_hover_ui_at_pointer(|ui| { - ui.set_max_width(300.0); - ui.add(ui::ProfilePreview::new( - profile.as_ref().unwrap(), - self.note_context.img_cache, - )); - }); - - if resp.hovered() || resp.clicked() { - ui::show_pointer(ui); - } - - resp - } - - None => { - // This has to match the expand size from the above case to - // prevent bounciness - let size = (pfp_size + ui::NoteView::expand_size()) as f32; - let (rect, _response) = ui.allocate_exact_size(egui::vec2(size, size), sense); - - ui.put( - rect, - ui::ProfilePic::new(self.note_context.img_cache, ui::ProfilePic::no_pfp_url()) - .size(pfp_size as f32), - ) - .interact(sense) - } - } - } - - pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse { - if self.options().has_textmode() { - NoteResponse::new(self.textmode_ui(ui)) - } else { - let txn = self.note.txn().expect("txn"); - if let Some(note_to_repost) = get_reposted_note(self.note_context.ndb, txn, self.note) { - let profile = self - .note_context - .ndb - .get_profile_by_pubkey(txn, self.note.pubkey()); - - let style = NotedeckTextStyle::Small; - ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.add_space(2.0); - ui.add_sized([20.0, 20.0], repost_icon(ui.visuals().dark_mode)); - }); - ui.add_space(6.0); - let resp = ui.add(one_line_display_name_widget( - ui.visuals(), - get_display_name(profile.as_ref().ok()), - style, - )); - if let Ok(rec) = &profile { - resp.on_hover_ui_at_pointer(|ui| { - ui.set_max_width(300.0); - ui.add(ui::ProfilePreview::new(rec, self.note_context.img_cache)); - }); - } - let color = ui.style().visuals.noninteractive().fg_stroke.color; - ui.add_space(4.0); - ui.label( - RichText::new("Reposted") - .color(color) - .text_style(style.text_style()), - ); - }); - NoteView::new(self.note_context, self.cur_acc, ¬e_to_repost, self.flags).show(ui) - } else { - self.show_standard(ui) - } - } - } - - #[profiling::function] - fn note_header( - ui: &mut egui::Ui, - note_cache: &mut NoteCache, - note: &Note, - profile: &Result, nostrdb::Error>, - ) { - let note_key = note.key().unwrap(); - - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 2.0; - ui.add(ui::Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20)); - - let cached_note = note_cache.cached_note_or_insert_mut(note_key, note); - render_reltime(ui, cached_note, true); - }); - } - - #[profiling::function] - fn show_standard(&mut self, ui: &mut egui::Ui) -> NoteResponse { - let note_key = self.note.key().expect("todo: support non-db notes"); - let txn = self.note.txn().expect("todo: support non-db notes"); - - let mut note_action: Option = None; - - let hitbox_id = note_hitbox_id(note_key, self.options(), self.parent); - let profile = self - .note_context - .ndb - .get_profile_by_pubkey(txn, self.note.pubkey()); - let maybe_hitbox = maybe_note_hitbox(ui, hitbox_id); - - // wide design - let response = if self.options().has_wide() { - ui.vertical(|ui| { - ui.horizontal(|ui| { - if self.pfp(note_key, &profile, ui).clicked() { - note_action = Some(NoteAction::OpenTimeline(TimelineKind::profile( - Pubkey::new(*self.note.pubkey()), - ))); - }; - - let size = ui.available_size(); - ui.vertical(|ui| { - ui.add_sized( - [size.x, self.options().pfp_size() as f32], - |ui: &mut egui::Ui| { - ui.horizontal_centered(|ui| { - NoteView::note_header( - ui, - self.note_context.note_cache, - self.note, - &profile, - ); - }) - .response - }, - ); - - let note_reply = self - .note_context - .note_cache - .cached_note_or_insert_mut(note_key, self.note) - .reply - .borrow(self.note.tags()); - - if note_reply.reply().is_some() { - let action = ui - .horizontal(|ui| { - reply_desc( - ui, - self.cur_acc, - txn, - ¬e_reply, - self.note_context, - self.flags, - ) - }) - .inner; - - if action.is_some() { - note_action = action; - } - } - }); - }); - - let mut contents = - NoteContents::new(self.note_context, self.cur_acc, txn, self.note, self.flags); - - ui.add(&mut contents); - - if let Some(action) = contents.action() { - note_action = Some(action.clone()); - } - - if self.options().has_actionbar() { - if let Some(action) = render_note_actionbar( - ui, - self.note_context.zaps, - self.cur_acc.as_ref(), - self.note.id(), - self.note.pubkey(), - note_key, - ) - .inner - { - note_action = Some(action); - } - } - }) - .response - } else { - // main design - ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { - if self.pfp(note_key, &profile, ui).clicked() { - note_action = Some(NoteAction::OpenTimeline(TimelineKind::Profile( - Pubkey::new(*self.note.pubkey()), - ))); - }; - - ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - NoteView::note_header(ui, self.note_context.note_cache, self.note, &profile); - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 2.0; - - let note_reply = self - .note_context - .note_cache - .cached_note_or_insert_mut(note_key, self.note) - .reply - .borrow(self.note.tags()); - - if note_reply.reply().is_some() { - let action = reply_desc( - ui, - self.cur_acc, - txn, - ¬e_reply, - self.note_context, - self.flags, - ); - - if action.is_some() { - note_action = action; - } - } - }); - - let mut contents = NoteContents::new( - self.note_context, - self.cur_acc, - txn, - self.note, - self.flags, - ); - ui.add(&mut contents); - - if let Some(action) = contents.action() { - note_action = Some(action.clone()); - } - - if self.options().has_actionbar() { - if let Some(action) = render_note_actionbar( - ui, - self.note_context.zaps, - self.cur_acc.as_ref(), - self.note.id(), - self.note.pubkey(), - note_key, - ) - .inner - { - note_action = Some(action); - } - } - }); - }) - .response - }; - - if self.options().has_options_button() { - let context_pos = { - let size = NoteContextButton::max_width(); - let top_right = response.rect.right_top(); - let min = Pos2::new(top_right.x - size, top_right.y); - Rect::from_min_size(min, egui::vec2(size, size)) - }; - - let resp = ui.add(NoteContextButton::new(note_key).place_at(context_pos)); - if let Some(action) = NoteContextButton::menu(ui, resp.clone()) { - note_action = Some(NoteAction::Context(ContextSelection { note_key, action })); - } - } - - let note_action = if note_hitbox_clicked(ui, hitbox_id, &response.rect, maybe_hitbox) { - if let Ok(selection) = ThreadSelection::from_note_id( - self.note_context.ndb, - self.note_context.note_cache, - self.note.txn().unwrap(), - NoteId::new(*self.note.id()), - ) { - Some(NoteAction::OpenTimeline(TimelineKind::Thread(selection))) - } else { - None - } - } else { - note_action - }; - - NoteResponse::new(response).with_action(note_action) - } -} - -fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option> { - let new_note_id: &[u8; 32] = if note.kind() == 6 { - let mut res = None; - for tag in note.tags().iter() { - if tag.count() == 0 { - continue; - } - - if let Some("e") = tag.get(0).and_then(|t| t.variant().str()) { - if let Some(note_id) = tag.get(1).and_then(|f| f.variant().id()) { - res = Some(note_id); - break; - } - } - } - res? - } else { - return None; - }; - - let note = ndb.get_note_by_id(txn, new_note_id).ok(); - note.filter(|note| note.kind() == 1) -} - -fn note_hitbox_id( - note_key: NoteKey, - note_options: NoteOptions, - parent: Option, -) -> egui::Id { - Id::new(("note_size", note_key, note_options, parent)) -} - -fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option { - ui.ctx() - .data_mut(|d| d.get_persisted(hitbox_id)) - .map(|note_size: Vec2| { - // The hitbox should extend the entire width of the - // container. The hitbox height was cached last layout. - let container_rect = ui.max_rect(); - let rect = Rect { - min: pos2(container_rect.min.x, container_rect.min.y), - max: pos2(container_rect.max.x, container_rect.min.y + note_size.y), - }; - - let response = ui.interact(rect, ui.id().with(hitbox_id), egui::Sense::click()); - - response - .widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Other, true, "hitbox")); - - response - }) -} - -fn note_hitbox_clicked( - ui: &mut egui::Ui, - hitbox_id: egui::Id, - note_rect: &Rect, - maybe_hitbox: Option, -) -> bool { - // Stash the dimensions of the note content so we can render the - // hitbox in the next frame - ui.ctx().data_mut(|d| { - d.insert_persisted(hitbox_id, note_rect.size()); - }); - - // If there was an hitbox and it was clicked open the thread - match maybe_hitbox { - Some(hitbox) => hitbox.clicked(), - _ => false, - } -} - -#[profiling::function] -fn render_note_actionbar( - ui: &mut egui::Ui, - zaps: &Zaps, - cur_acc: Option<&KeypairUnowned>, - note_id: &[u8; 32], - note_pubkey: &[u8; 32], - note_key: NoteKey, -) -> egui::InnerResponse> { - ui.horizontal(|ui| 's: { - let reply_resp = reply_button(ui, note_key); - let quote_resp = quote_repost_button(ui, note_key); - - let zap_target = ZapTarget::Note(NoteZapTarget { - note_id, - zap_recipient: note_pubkey, - }); - - let zap_state = cur_acc.map_or_else( - || Ok(AnyZapState::None), - |kp| zaps.any_zap_state_for(kp.pubkey.bytes(), zap_target), - ); - let zap_resp = cur_acc - .filter(|k| k.secret_key.is_some()) - .map(|_| match &zap_state { - Ok(any_zap_state) => ui.add(zap_button(any_zap_state.clone(), note_id)), - Err(zapping_error) => { - let (rect, _) = - ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click()); - ui.add(x_button(rect)) - .on_hover_text(format!("{zapping_error}")) - } - }); - - let to_noteid = |id: &[u8; 32]| NoteId::new(*id); - - if reply_resp.clicked() { - break 's Some(NoteAction::Reply(to_noteid(note_id))); - } - - if quote_resp.clicked() { - break 's Some(NoteAction::Quote(to_noteid(note_id))); - } - - let Some(zap_resp) = zap_resp else { - break 's None; - }; - - if !zap_resp.clicked() { - break 's None; - } - - let target = NoteZapTargetOwned { - note_id: to_noteid(note_id), - zap_recipient: Pubkey::new(*note_pubkey), - }; - - if zap_state.is_err() { - break 's Some(NoteAction::Zap(ZapAction::ClearError(target))); - } - - Some(NoteAction::Zap(ZapAction::Send(target))) - }) -} - -fn secondary_label(ui: &mut egui::Ui, s: impl Into) { - let color = ui.style().visuals.noninteractive().fg_stroke.color; - ui.add(Label::new(RichText::new(s).size(10.0).color(color))); -} - -#[profiling::function] -fn render_reltime( - ui: &mut egui::Ui, - note_cache: &mut CachedNote, - before: bool, -) -> egui::InnerResponse<()> { - ui.horizontal(|ui| { - if before { - secondary_label(ui, "⋅"); - } - - secondary_label(ui, note_cache.reltime_str_mut()); - - if !before { - secondary_label(ui, "⋅"); - } - }) -} - -fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { - let img_data = if ui.style().visuals.dark_mode { - egui::include_image!("../../../../../assets/icons/reply.png") - } else { - egui::include_image!("../../../../../assets/icons/reply-dark.png") - }; - - let (rect, size, resp) = - ui::anim::hover_expand_small(ui, ui.id().with(("reply_anim", note_key))); - - // align rect to note contents - let expand_size = 5.0; // from hover_expand_small - let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); - - let put_resp = ui.put(rect, egui::Image::new(img_data).max_width(size)); - - resp.union(put_resp) -} - -fn repost_icon(dark_mode: bool) -> egui::Image<'static> { - let img_data = if dark_mode { - egui::include_image!("../../../../../assets/icons/repost_icon_4x.png") - } else { - egui::include_image!("../../../../../assets/icons/repost_light_4x.png") - }; - egui::Image::new(img_data) -} - -fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { - let size = 14.0; - let expand_size = 5.0; - let anim_speed = 0.05; - let id = ui.id().with(("repost_anim", note_key)); - - let (rect, size, resp) = ui::anim::hover_expand(ui, id, size, expand_size, anim_speed); - - let rect = rect.translate(egui::vec2(-(expand_size / 2.0), -1.0)); - - let put_resp = ui.put(rect, repost_icon(ui.visuals().dark_mode).max_width(size)); - - resp.union(put_resp) -} - -fn zap_button(state: AnyZapState, noteid: &[u8; 32]) -> impl egui::Widget + use<'_> { - move |ui: &mut egui::Ui| -> egui::Response { - let img_data = egui::include_image!("../../../../../assets/icons/zap_4x.png"); - - let (rect, size, resp) = ui::anim::hover_expand_small(ui, ui.id().with("zap")); - - let mut img = egui::Image::new(img_data).max_width(size); - let id = ui.id().with(("pulse", noteid)); - let ctx = ui.ctx().clone(); - - match state { - AnyZapState::None => { - if !ui.visuals().dark_mode { - img = img.tint(egui::Color32::BLACK); - } - } - AnyZapState::Pending => { - let alpha_min = if ui.visuals().dark_mode { 50 } else { 180 }; - img = ImagePulseTint::new(&ctx, id, img, &[0xFF, 0xB7, 0x57], alpha_min, 255) - .with_speed(0.35) - .animate(); - } - AnyZapState::LocalOnly => { - img = img.tint(egui::Color32::from_rgb(0xFF, 0xB7, 0x57)); - } - AnyZapState::Confirmed => {} - } - - // align rect to note contents - let expand_size = 5.0; // from hover_expand_small - let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); - - let put_resp = ui.put(rect, img); - - resp.union(put_resp) - } -} diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs index 6d7859d..22d85b1 100644 --- a/crates/notedeck_columns/src/ui/note/post.rs +++ b/crates/notedeck_columns/src/ui/note/post.rs @@ -1,28 +1,29 @@ -use crate::actionbar::NoteAction; use crate::draft::{Draft, Drafts, MentionHint}; use crate::media_upload::{nostrbuild_nip96_upload, MediaPath}; use crate::post::{downcast_post_buffer, MentionType, NewPost}; -use crate::profile::get_display_name; use crate::ui::search_results::SearchResultsView; use crate::ui::{self, Preview, PreviewConfig}; use crate::Result; -use egui::text::{CCursorRange, LayoutJob}; -use egui::text_edit::TextEditOutput; -use egui::widgets::text_edit::TextEdit; -use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer}; + +use egui::{ + text::{CCursorRange, LayoutJob}, + text_edit::TextEditOutput, + vec2, + widgets::text_edit::TextEdit, + Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer, +}; use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; use notedeck_ui::{ gif::{handle_repaint, retrieve_latest_texture}, images::render_images, + note::render_note_preview, + NoteOptions, ProfilePic, }; -use notedeck::supported_mime_hosted_at_url; +use notedeck::{name::get_display_name, supported_mime_hosted_at_url, NoteAction, NoteContext}; use tracing::error; -use super::contents::{render_note_preview, NoteContext}; -use super::NoteOptions; - pub struct PostView<'a, 'd> { note_context: &'a mut NoteContext<'d>, draft: &'a mut Draft, @@ -133,14 +134,14 @@ impl<'a, 'd> PostView<'a, 'd> { .as_ref() .ok() .and_then(|p| { - Some(ui::ProfilePic::from_profile(self.note_context.img_cache, p)?.size(pfp_size)) + Some(ProfilePic::from_profile(self.note_context.img_cache, p)?.size(pfp_size)) }); if let Some(pfp) = poster_pfp { ui.add(pfp); } else { ui.add( - ui::ProfilePic::new(self.note_context.img_cache, ui::ProfilePic::no_pfp_url()) + ProfilePic::new(self.note_context.img_cache, notedeck::profile::no_pfp_url()) .size(pfp_size), ); } diff --git a/crates/notedeck_columns/src/ui/note/quote_repost.rs b/crates/notedeck_columns/src/ui/note/quote_repost.rs index 9d6620d..4d327ad 100644 --- a/crates/notedeck_columns/src/ui/note/quote_repost.rs +++ b/crates/notedeck_columns/src/ui/note/quote_repost.rs @@ -1,11 +1,12 @@ -use enostr::{FilledKeypair, NoteId}; - +use super::{PostResponse, PostType}; use crate::{ draft::Draft, ui::{self}, }; -use super::{contents::NoteContext, NoteOptions, PostResponse, PostType}; +use enostr::{FilledKeypair, NoteId}; +use notedeck::NoteContext; +use notedeck_ui::NoteOptions; pub struct QuoteRepostView<'a, 'd> { note_context: &'a mut NoteContext<'d>, diff --git a/crates/notedeck_columns/src/ui/note/reply.rs b/crates/notedeck_columns/src/ui/note/reply.rs index 7300a84..a73bf7b 100644 --- a/crates/notedeck_columns/src/ui/note/reply.rs +++ b/crates/notedeck_columns/src/ui/note/reply.rs @@ -1,10 +1,12 @@ use crate::draft::Draft; -use crate::ui; -use crate::ui::note::{PostAction, PostResponse, PostType}; -use enostr::{FilledKeypair, NoteId}; +use crate::ui::{ + self, + note::{PostAction, PostResponse, PostType}, +}; -use super::contents::NoteContext; -use super::NoteOptions; +use enostr::{FilledKeypair, NoteId}; +use notedeck::NoteContext; +use notedeck_ui::{NoteOptions, NoteView, ProfilePic}; pub struct PostReplyView<'a, 'd> { note_context: &'a mut NoteContext<'d>, @@ -56,15 +58,15 @@ impl<'a, 'd> PostReplyView<'a, 'd> { // to indent things so that the reply line is aligned let pfp_offset: i8 = ui::PostView::outer_margin() + ui::PostView::inner_margin() - + ui::ProfilePic::small_size() / 2; + + ProfilePic::small_size() / 2; let note_offset: i8 = - pfp_offset - ui::ProfilePic::medium_size() / 2 - ui::NoteView::expand_size() / 2; + pfp_offset - ProfilePic::medium_size() / 2 - NoteView::expand_size() / 2; let quoted_note = egui::Frame::NONE .outer_margin(egui::Margin::same(note_offset)) .show(ui, |ui| { - ui::NoteView::new( + NoteView::new( self.note_context, &Some(self.poster.into()), self.note, @@ -113,9 +115,9 @@ impl<'a, 'd> PostReplyView<'a, 'd> { // honestly don't know what the fuck I'm doing here. just trying // to get the line under the profile picture rect.min.y = avail_rect.min.y - + (ui::ProfilePic::medium_size() as f32 / 2.0 - + ui::ProfilePic::medium_size() as f32 - + ui::NoteView::expand_size() as f32 * 2.0) + + (ProfilePic::medium_size() as f32 / 2.0 + + ProfilePic::medium_size() as f32 + + NoteView::expand_size() as f32 * 2.0) + 1.0; // For some reason we need to nudge the reply line's height a diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs index dd693dc..2705993 100644 --- a/crates/notedeck_columns/src/ui/profile/edit.rs +++ b/crates/notedeck_columns/src/ui/profile/edit.rs @@ -1,13 +1,9 @@ use core::f32; -use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit}; -use notedeck::{Images, NotedeckTextStyle}; - use crate::profile_state::ProfileState; - -use super::banner; - -use notedeck_ui::{profile::unwrap_profile_url, ProfilePic}; +use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit}; +use notedeck::{profile::unwrap_profile_url, Images, NotedeckTextStyle}; +use notedeck_ui::{profile::banner, ProfilePic}; pub struct EditProfileView<'a> { state: &'a mut ProfileState, @@ -26,14 +22,14 @@ impl<'a> EditProfileView<'a> { banner(ui, Some(&self.state.banner), 188.0); let padding = 24.0; - crate::ui::padding(padding, ui, |ui| { + notedeck_ui::padding(padding, ui, |ui| { self.inner(ui, padding); }); ui.separator(); let mut save = false; - crate::ui::padding(padding, ui, |ui| { + notedeck_ui::padding(padding, ui, |ui| { ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| { if ui .add(button("Save changes", 119.0).fill(notedeck_ui::colors::PINK)) diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index 3711ac6..614b51f 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -1,27 +1,23 @@ pub mod edit; -pub mod preview; pub use edit::EditProfileView; -use egui::load::TexturePoll; -use egui::{vec2, Color32, CornerRadius, Label, Layout, Rect, RichText, ScrollArea, Sense, Stroke}; +use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke}; use enostr::Pubkey; use nostrdb::{ProfileRecord, Transaction}; -pub use preview::ProfilePreview; use tracing::error; use crate::{ - actionbar::NoteAction, - profile::get_display_name, timeline::{TimelineCache, TimelineKind}, ui::timeline::{tabs_ui, TimelineTabView}, - NostrName, }; - -use notedeck::{Accounts, MuteFun, NotedeckTextStyle, UnknownIds}; -use notedeck_ui::{images, profile::get_profile_url, ProfilePic}; - -use super::note::contents::NoteContext; -use super::note::NoteOptions; +use notedeck::{ + name::get_display_name, profile::get_profile_url, Accounts, MuteFun, NoteAction, NoteContext, + NotedeckTextStyle, UnknownIds, +}; +use notedeck_ui::{ + profile::{about_section_widget, banner, display_name_widget}, + NoteOptions, ProfilePic, +}; pub struct ProfileView<'a, 'd> { pubkey: &'a Pubkey, @@ -137,7 +133,7 @@ impl<'a, 'd> ProfileView<'a, 'd> { ); let padding = 12.0; - crate::ui::padding(padding, ui, |ui| { + notedeck_ui::padding(padding, ui, |ui| { let mut pfp_rect = ui.available_rect_before_wrap(); let size = 80.0; pfp_rect.set_width(size); @@ -342,110 +338,3 @@ fn edit_profile_button() -> impl egui::Widget + 'static { resp } } - -fn display_name_widget<'a>( - name: &'a NostrName<'a>, - add_placeholder_space: bool, -) -> impl egui::Widget + 'a { - move |ui: &mut egui::Ui| -> egui::Response { - let disp_resp = name.display_name.map(|disp_name| { - ui.add( - Label::new( - RichText::new(disp_name).text_style(NotedeckTextStyle::Heading3.text_style()), - ) - .selectable(false), - ) - }); - - let (username_resp, nip05_resp) = ui - .horizontal(|ui| { - let username_resp = name.username.map(|username| { - ui.add( - Label::new( - RichText::new(format!("@{}", username)) - .size(16.0) - .color(notedeck_ui::colors::MID_GRAY), - ) - .selectable(false), - ) - }); - - let nip05_resp = name.nip05.map(|nip05| { - ui.image(egui::include_image!( - "../../../../../assets/icons/verified_4x.png" - )); - ui.add(Label::new( - RichText::new(nip05) - .size(16.0) - .color(notedeck_ui::colors::TEAL), - )) - }); - - (username_resp, nip05_resp) - }) - .inner; - - let resp = match (disp_resp, username_resp, nip05_resp) { - (Some(disp), Some(username), Some(nip05)) => disp.union(username).union(nip05), - (Some(disp), Some(username), None) => disp.union(username), - (Some(disp), None, None) => disp, - (None, Some(username), Some(nip05)) => username.union(nip05), - (None, Some(username), None) => username, - _ => ui.add(Label::new(RichText::new(name.name()))), - }; - - if add_placeholder_space { - ui.add_space(16.0); - } - - resp - } -} - -fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b -where - 'b: 'a, -{ - move |ui: &mut egui::Ui| { - if let Some(about) = profile.record().profile().and_then(|p| p.about()) { - let resp = ui.label(about); - ui.add_space(8.0); - resp - } else { - // need any Response so we dont need an Option - ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()) - } - } -} - -fn banner_texture(ui: &mut egui::Ui, banner_url: &str) -> Option { - // TODO: cache banner - if !banner_url.is_empty() { - let texture_load_res = - egui::Image::new(banner_url).load_for_size(ui.ctx(), ui.available_size()); - if let Ok(texture_poll) = texture_load_res { - match texture_poll { - TexturePoll::Pending { .. } => {} - TexturePoll::Ready { texture, .. } => return Some(texture), - } - } - } - - None -} - -fn banner(ui: &mut egui::Ui, banner_url: Option<&str>, height: f32) -> egui::Response { - ui.add_sized([ui.available_size().x, height], |ui: &mut egui::Ui| { - banner_url - .and_then(|url| banner_texture(ui, url)) - .map(|texture| { - images::aspect_fill( - ui, - Sense::hover(), - texture.id, - texture.size.x / texture.size.y, - ) - }) - .unwrap_or_else(|| ui.label("")) - }) -} diff --git a/crates/notedeck_columns/src/ui/relay.rs b/crates/notedeck_columns/src/ui/relay.rs index fcc59ab..d352719 100644 --- a/crates/notedeck_columns/src/ui/relay.rs +++ b/crates/notedeck_columns/src/ui/relay.rs @@ -1,18 +1,15 @@ use std::collections::HashMap; use crate::relay_pool_manager::{RelayPoolManager, RelayStatus}; -use crate::ui::{Preview, PreviewConfig, View}; +use crate::ui::{Preview, PreviewConfig}; use egui::{ Align, Button, CornerRadius, Frame, Id, Image, Layout, Margin, Rgba, RichText, Ui, Vec2, }; -use notedeck_ui::colors::PINK; - use enostr::RelayPool; use notedeck::{Accounts, NotedeckTextStyle}; - +use notedeck_ui::{colors::PINK, padding, View}; use tracing::debug; -use super::padding; use super::widgets::styled_button; pub struct RelayView<'a> { diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs index 1469107..a49e264 100644 --- a/crates/notedeck_columns/src/ui/search/mod.rs +++ b/crates/notedeck_columns/src/ui/search/mod.rs @@ -1,15 +1,11 @@ use egui::{vec2, Align, Color32, CornerRadius, RichText, Stroke, TextEdit}; use enostr::KeypairUnowned; -use super::{note::contents::NoteContext, padding}; -use crate::{ - actionbar::NoteAction, - ui::{note::NoteOptions, timeline::TimelineTabView}, -}; +use crate::ui::timeline::TimelineTabView; use egui_winit::clipboard::Clipboard; use nostrdb::{Filter, Transaction}; -use notedeck::{MuteFun, NoteRef}; -use notedeck_ui::icons::search_icon; +use notedeck::{MuteFun, NoteAction, NoteContext, NoteRef}; +use notedeck_ui::{icons::search_icon, padding, NoteOptions}; use std::time::{Duration, Instant}; use tracing::{error, info, warn}; diff --git a/crates/notedeck_columns/src/ui/search_results.rs b/crates/notedeck_columns/src/ui/search_results.rs index 2cf6789..deee9cd 100644 --- a/crates/notedeck_columns/src/ui/search_results.rs +++ b/crates/notedeck_columns/src/ui/search_results.rs @@ -1,15 +1,15 @@ use egui::{vec2, FontId, Layout, Pos2, Rect, ScrollArea, UiBuilder, Vec2b}; use nostrdb::{Ndb, ProfileRecord, Transaction}; -use notedeck::{fonts::get_font_size, Images, NotedeckTextStyle}; -use tracing::error; - -use crate::{ - profile::get_display_name, - ui::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, +use notedeck::{ + fonts::get_font_size, name::get_display_name, profile::get_profile_url, Images, + NotedeckTextStyle, }; - -use super::{widgets::x_button, ProfilePic}; -use notedeck_ui::profile::get_profile_url; +use notedeck_ui::{ + anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, + widgets::x_button, + ProfilePic, +}; +use tracing::error; pub struct SearchResultsView<'a> { ndb: &'a Ndb, diff --git a/crates/notedeck_columns/src/ui/side_panel.rs b/crates/notedeck_columns/src/ui/side_panel.rs index c029d36..6246ddb 100644 --- a/crates/notedeck_columns/src/ui/side_panel.rs +++ b/crates/notedeck_columns/src/ui/side_panel.rs @@ -12,14 +12,13 @@ use crate::{ }; use notedeck::{Accounts, UserAccount}; -use notedeck_ui::colors; - -use super::{ +use notedeck_ui::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, - configure_deck::deck_icon, - View, + colors, View, }; +use super::configure_deck::deck_icon; + pub static SIDE_PANEL_WIDTH: f32 = 68.0; static ICON_WIDTH: f32 = 40.0; diff --git a/crates/notedeck_columns/src/ui/support.rs b/crates/notedeck_columns/src/ui/support.rs index 252bcc8..84c0278 100644 --- a/crates/notedeck_columns/src/ui/support.rs +++ b/crates/notedeck_columns/src/ui/support.rs @@ -1,11 +1,9 @@ use egui::{vec2, Button, Label, Layout, RichText}; +use notedeck::{NamedFontFamily, NotedeckTextStyle}; +use notedeck_ui::{colors::PINK, padding}; use tracing::error; use crate::support::Support; -use notedeck_ui::colors::PINK; - -use super::padding; -use notedeck::{NamedFontFamily, NotedeckTextStyle}; pub struct SupportView<'a> { support: &'a mut Support, diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs index d2afe4e..938fdfb 100644 --- a/crates/notedeck_columns/src/ui/thread.rs +++ b/crates/notedeck_columns/src/ui/thread.rs @@ -1,17 +1,11 @@ -use crate::{ - actionbar::NoteAction, - timeline::{ThreadSelection, TimelineCache, TimelineKind}, -}; - use enostr::KeypairUnowned; use nostrdb::Transaction; -use notedeck::{MuteFun, RootNoteId, UnknownIds}; +use notedeck::{MuteFun, NoteAction, NoteContext, RootNoteId, UnknownIds}; +use notedeck_ui::NoteOptions; use tracing::error; -use super::{ - note::{contents::NoteContext, NoteOptions}, - timeline::TimelineTabView, -}; +use crate::timeline::{ThreadSelection, TimelineCache, TimelineKind}; +use crate::ui::timeline::TimelineTabView; pub struct ThreadView<'a, 'd> { timeline_cache: &'a mut TimelineCache, diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs index 828c1f7..6d3f3d6 100644 --- a/crates/notedeck_columns/src/ui/timeline.rs +++ b/crates/notedeck_columns/src/ui/timeline.rs @@ -1,23 +1,17 @@ -use std::f32::consts::PI; - -use crate::actionbar::NoteAction; -use crate::timeline::TimelineTab; -use crate::{ - timeline::{TimelineCache, TimelineKind, ViewFilter}, - ui, -}; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{vec2, Direction, Layout, Pos2, Stroke}; use egui_tabs::TabColor; use enostr::KeypairUnowned; use nostrdb::Transaction; -use notedeck::note::root_note_id_from_selected_id; -use notedeck::MuteFun; +use std::f32::consts::PI; use tracing::{error, warn}; -use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}; -use super::note::contents::NoteContext; -use super::note::NoteOptions; +use crate::timeline::{TimelineCache, TimelineKind, TimelineTab, ViewFilter}; +use notedeck::{note::root_note_id_from_selected_id, MuteFun, NoteAction, NoteContext}; +use notedeck_ui::{ + anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, + show_pointer, NoteOptions, NoteView, +}; pub struct TimelineView<'a, 'd> { timeline_id: &'a TimelineKind, @@ -134,7 +128,7 @@ fn timeline_ui( if goto_top_resp.clicked() { scroll_area = scroll_area.vertical_scroll_offset(0.0); } else if goto_top_resp.hovered() { - ui::show_pointer(ui); + show_pointer(ui); } } @@ -271,7 +265,7 @@ pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usi }); //ui.add_space(0.5); - ui::hline(ui); + notedeck_ui::hline(ui); let sel = tab_res.selected().unwrap_or_default(); @@ -395,8 +389,8 @@ impl<'a, 'd> TimelineTabView<'a, 'd> { }; if !muted { - ui::padding(8.0, ui, |ui| { - let resp = ui::NoteView::new( + notedeck_ui::padding(8.0, ui, |ui| { + let resp = NoteView::new( self.note_context, self.cur_acc, ¬e, @@ -409,7 +403,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> { } }); - ui::hline(ui); + notedeck_ui::hline(ui); } 1 diff --git a/crates/notedeck_columns/src/ui/widgets.rs b/crates/notedeck_columns/src/ui/widgets.rs index a109f7b..54a1120 100644 --- a/crates/notedeck_columns/src/ui/widgets.rs +++ b/crates/notedeck_columns/src/ui/widgets.rs @@ -1,41 +1,6 @@ -use egui::{emath::GuiRounding, Button, Pos2, Stroke, Widget}; +use egui::{Button, Widget}; use notedeck::NotedeckTextStyle; -use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}; - -pub fn x_button(rect: egui::Rect) -> impl egui::Widget { - move |ui: &mut egui::Ui| -> egui::Response { - let max_width = rect.width(); - let helper = AnimationHelper::new_from_rect(ui, "user_search_close", rect); - - let fill_color = ui.visuals().text_color(); - - let radius = max_width / (2.0 * ICON_EXPANSION_MULTIPLE); - - let painter = ui.painter(); - let ppp = ui.ctx().pixels_per_point(); - let nw_edge = helper - .scale_pos_from_center(Pos2::new(-radius, radius)) - .round_to_pixel_center(ppp); - let se_edge = helper - .scale_pos_from_center(Pos2::new(radius, -radius)) - .round_to_pixel_center(ppp); - let sw_edge = helper - .scale_pos_from_center(Pos2::new(-radius, -radius)) - .round_to_pixel_center(ppp); - let ne_edge = helper - .scale_pos_from_center(Pos2::new(radius, radius)) - .round_to_pixel_center(ppp); - - let line_width = helper.scale_1d_pos(2.0); - - painter.line_segment([nw_edge, se_edge], Stroke::new(line_width, fill_color)); - painter.line_segment([ne_edge, sw_edge], Stroke::new(line_width, fill_color)); - - helper.take_animation_response() - } -} - /// Sized and styled to match the figma design pub fn styled_button(text: &str, fill_color: egui::Color32) -> impl Widget + '_ { move |ui: &mut egui::Ui| -> egui::Response { diff --git a/crates/notedeck_ui/Cargo.toml b/crates/notedeck_ui/Cargo.toml index c13d390..0a9af43 100644 --- a/crates/notedeck_ui/Cargo.toml +++ b/crates/notedeck_ui/Cargo.toml @@ -14,3 +14,5 @@ profiling = { workspace = true } tokio = { workspace = true } notedeck = { workspace = true } image = { workspace = true } +bitflags = { workspace = true } +enostr = { workspace = true } diff --git a/crates/notedeck_ui/src/anim.rs b/crates/notedeck_ui/src/anim.rs index 203a741..1d4c966 100644 --- a/crates/notedeck_ui/src/anim.rs +++ b/crates/notedeck_ui/src/anim.rs @@ -1,6 +1,5 @@ use egui::{Pos2, Rect, Response, Sense}; -/* pub fn hover_expand( ui: &mut egui::Ui, id: egui::Id, @@ -28,7 +27,6 @@ pub fn hover_expand_small(ui: &mut egui::Ui, id: egui::Id) -> (egui::Rect, f32, hover_expand(ui, id, size, expand_size, anim_speed) } -*/ pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; pub static ANIM_SPEED: f32 = 0.05; diff --git a/crates/notedeck_ui/src/images.rs b/crates/notedeck_ui/src/images.rs index bac18e8..1a26085 100644 --- a/crates/notedeck_ui/src/images.rs +++ b/crates/notedeck_ui/src/images.rs @@ -1,4 +1,3 @@ -use crate::ProfilePic; use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint}; use image::codecs::gif::GifDecoder; use image::imageops::FilterType; @@ -474,7 +473,7 @@ fn render_media_cache( let no_pfp = crate::images::fetch_img( cache, ui.ctx(), - ProfilePic::no_pfp_url(), + notedeck::profile::no_pfp_url(), ImageType::Profile(128), cache_type, ); diff --git a/crates/notedeck_ui/src/lib.rs b/crates/notedeck_ui/src/lib.rs index 6960097..0c8469c 100644 --- a/crates/notedeck_ui/src/lib.rs +++ b/crates/notedeck_ui/src/lib.rs @@ -1,10 +1,55 @@ -mod anim; +pub mod anim; pub mod colors; pub mod constants; pub mod gif; pub mod icons; pub mod images; +pub mod mention; +pub mod note; pub mod profile; +mod username; +pub mod widgets; pub use anim::{AnimationHelper, ImagePulseTint}; -pub use profile::ProfilePic; +pub use mention::Mention; +pub use note::{NoteContents, NoteOptions, NoteView}; +pub use profile::{ProfilePic, ProfilePreview}; +pub use username::Username; + +use egui::Margin; + +/// This is kind of like the Widget trait but is meant for larger top-level +/// views that are typically stateful. +/// +/// The Widget trait forces us to add mutable +/// implementations at the type level, which screws us when generating Previews +/// for a Widget. I would have just Widget instead of making this Trait otherwise. +/// +/// There is some precendent for this, it looks like there's a similar trait +/// in the egui demo library. +pub trait View { + fn ui(&mut self, ui: &mut egui::Ui); +} + +pub fn padding( + amount: impl Into, + ui: &mut egui::Ui, + add_contents: impl FnOnce(&mut egui::Ui) -> R, +) -> egui::InnerResponse { + egui::Frame::new() + .inner_margin(amount) + .show(ui, add_contents) +} + +pub fn hline(ui: &egui::Ui) { + // pixel perfect horizontal line + let rect = ui.available_rect_before_wrap(); + #[allow(deprecated)] + let resize_y = ui.painter().round_to_pixel(rect.top()) - 0.5; + let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke; + ui.painter().hline(rect.x_range(), resize_y, stroke); +} + +pub fn show_pointer(ui: &egui::Ui) { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); +} diff --git a/crates/notedeck_columns/src/ui/mention.rs b/crates/notedeck_ui/src/mention.rs similarity index 85% rename from crates/notedeck_columns/src/ui/mention.rs rename to crates/notedeck_ui/src/mention.rs index 76e66cf..5a57080 100644 --- a/crates/notedeck_columns/src/ui/mention.rs +++ b/crates/notedeck_ui/src/mention.rs @@ -1,9 +1,8 @@ -use crate::ui; -use crate::{actionbar::NoteAction, profile::get_display_name, timeline::TimelineKind}; +use crate::{show_pointer, ProfilePreview}; use egui::Sense; use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; -use notedeck::Images; +use notedeck::{name::get_display_name, Images, NoteAction}; pub struct Mention<'a> { ndb: &'a Ndb, @@ -87,12 +86,10 @@ fn mention_ui( ); let note_action = if resp.clicked() { - ui::show_pointer(ui); - Some(NoteAction::OpenTimeline(TimelineKind::profile( - Pubkey::new(*pk), - ))) + show_pointer(ui); + Some(NoteAction::Profile(Pubkey::new(*pk))) } else if resp.hovered() { - ui::show_pointer(ui); + show_pointer(ui); None } else { None @@ -101,7 +98,7 @@ fn mention_ui( if let Some(rec) = profile.as_ref() { resp.on_hover_ui_at_pointer(|ui| { ui.set_max_width(300.0); - ui.add(ui::ProfilePreview::new(rec, img_cache)); + ui.add(ProfilePreview::new(rec, img_cache)); }); } diff --git a/crates/notedeck_columns/src/ui/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs similarity index 94% rename from crates/notedeck_columns/src/ui/note/contents.rs rename to crates/notedeck_ui/src/note/contents.rs index dbf7d32..476a44f 100644 --- a/crates/notedeck_columns/src/ui/note/contents.rs +++ b/crates/notedeck_ui/src/note/contents.rs @@ -1,29 +1,15 @@ -use crate::ui::{ - self, - note::{NoteOptions, NoteResponse}, -}; -use crate::{actionbar::NoteAction, timeline::TimelineKind}; -use egui::{Button, Color32, Hyperlink, Image, Response, RichText, Sense, Window}; -use enostr::{KeypairUnowned, RelayPool}; -use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; -use notedeck_ui::images::ImageType; -use notedeck_ui::{ +use crate::{ gif::{handle_repaint, retrieve_latest_texture}, - images::render_images, + images::{render_images, ImageType}, + note::{NoteAction, NoteOptions, NoteResponse, NoteView}, }; + +use egui::{Button, Color32, Hyperlink, Image, Response, RichText, Sense, Window}; +use enostr::KeypairUnowned; +use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use tracing::warn; -use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteCache, Zaps}; - -/// Aggregates dependencies to reduce the number of parameters -/// passed to inner UI elements, minimizing prop drilling. -pub struct NoteContext<'d> { - pub ndb: &'d Ndb, - pub img_cache: &'d mut Images, - pub note_cache: &'d mut NoteCache, - pub zaps: &'d mut Zaps, - pub pool: &'d mut RelayPool, -} +use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteContext}; pub struct NoteContents<'a, 'd> { note_context: &'a mut NoteContext<'d>, @@ -41,7 +27,7 @@ impl<'a, 'd> NoteContents<'a, 'd> { cur_acc: &'a Option>, txn: &'a Transaction, note: &'a Note, - options: ui::note::NoteOptions, + options: NoteOptions, ) -> Self { NoteContents { note_context, @@ -119,7 +105,7 @@ pub fn render_note_preview( ui.visuals().noninteractive().bg_stroke.color, )) .show(ui, |ui| { - ui::NoteView::new(note_context, cur_acc, ¬e, note_options) + NoteView::new(note_context, cur_acc, ¬e, note_options) .actionbar(false) .small_pfp(true) .wide(true) @@ -134,7 +120,7 @@ pub fn render_note_preview( #[allow(clippy::too_many_arguments)] #[profiling::function] -fn render_note_contents( +pub fn render_note_contents( ui: &mut egui::Ui, note_context: &mut NoteContext, cur_acc: &Option, @@ -170,7 +156,7 @@ fn render_note_contents( match block.blocktype() { BlockType::MentionBech32 => match block.as_mention().unwrap() { Mention::Profile(profile) => { - let act = ui::Mention::new( + let act = crate::Mention::new( note_context.ndb, note_context.img_cache, txn, @@ -184,7 +170,7 @@ fn render_note_contents( } Mention::Pubkey(npub) => { - let act = ui::Mention::new( + let act = crate::Mention::new( note_context.ndb, note_context.img_cache, txn, @@ -214,11 +200,9 @@ fn render_note_contents( let resp = ui.colored_label(link_color, format!("#{}", block.as_str())); if resp.clicked() { - note_action = Some(NoteAction::OpenTimeline(TimelineKind::Hashtag( - block.as_str().to_string(), - ))); + note_action = Some(NoteAction::Hashtag(block.as_str().to_string())); } else if resp.hovered() { - ui::show_pointer(ui); + crate::show_pointer(ui); } } diff --git a/crates/notedeck_columns/src/ui/note/context.rs b/crates/notedeck_ui/src/note/context.rs similarity index 73% rename from crates/notedeck_columns/src/ui/note/context.rs rename to crates/notedeck_ui/src/note/context.rs index 635007c..b111ec9 100644 --- a/crates/notedeck_columns/src/ui/note/context.rs +++ b/crates/notedeck_ui/src/note/context.rs @@ -1,59 +1,6 @@ use egui::{Rect, Vec2}; -use enostr::{ClientMessage, NoteId, Pubkey, RelayPool}; -use nostrdb::{Note, NoteKey}; -use tracing::error; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum BroadcastContext { - LocalNetwork, - Everywhere, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -#[allow(clippy::enum_variant_names)] -pub enum NoteContextSelection { - CopyText, - CopyPubkey, - CopyNoteId, - CopyNoteJSON, - Broadcast(BroadcastContext), -} - -impl NoteContextSelection { - pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>, pool: &mut RelayPool) { - match self { - NoteContextSelection::Broadcast(context) => { - tracing::info!("Broadcasting note {}", hex::encode(note.id())); - match context { - BroadcastContext::LocalNetwork => { - pool.send_to(&ClientMessage::event(note).unwrap(), "multicast"); - } - - BroadcastContext::Everywhere => { - pool.send(&ClientMessage::event(note).unwrap()); - } - } - } - NoteContextSelection::CopyText => { - ui.ctx().copy_text(note.content().to_string()); - } - NoteContextSelection::CopyPubkey => { - if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() { - ui.ctx().copy_text(bech); - } - } - NoteContextSelection::CopyNoteId => { - if let Some(bech) = NoteId::new(*note.id()).to_bech() { - ui.ctx().copy_text(bech); - } - } - NoteContextSelection::CopyNoteJSON => match note.json() { - Ok(json) => ui.ctx().copy_text(json), - Err(err) => error!("error copying note json: {err}"), - }, - } - } -} +use nostrdb::NoteKey; +use notedeck::{BroadcastContext, NoteContextSelection}; pub struct NoteContextButton { put_at: Option, diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs new file mode 100644 index 0000000..83d55b5 --- /dev/null +++ b/crates/notedeck_ui/src/note/mod.rs @@ -0,0 +1,744 @@ +pub mod contents; +pub mod context; +pub mod options; +pub mod reply_description; + +use crate::{ + profile::name::one_line_display_name_widget, widgets::x_button, ImagePulseTint, ProfilePic, + ProfilePreview, Username, +}; + +pub use contents::{render_note_contents, render_note_preview, NoteContents}; +pub use context::NoteContextButton; +pub use options::NoteOptions; +pub use reply_description::reply_desc; + +use egui::emath::{pos2, Vec2}; +use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense}; +use enostr::{KeypairUnowned, NoteId, Pubkey}; +use nostrdb::{Ndb, Note, NoteKey, Transaction}; +use notedeck::{ + name::get_display_name, + note::{NoteAction, NoteContext, ZapAction}, + AnyZapState, CachedNote, ContextSelection, NoteCache, NoteZapTarget, NoteZapTargetOwned, + NotedeckTextStyle, ZapTarget, Zaps, +}; + +pub struct NoteView<'a, 'd> { + note_context: &'a mut NoteContext<'d>, + cur_acc: &'a Option>, + parent: Option, + note: &'a nostrdb::Note<'a>, + flags: NoteOptions, +} + +pub struct NoteResponse { + pub response: egui::Response, + pub action: Option, +} + +impl NoteResponse { + pub fn new(response: egui::Response) -> Self { + Self { + response, + action: None, + } + } + + pub fn with_action(mut self, action: Option) -> Self { + self.action = action; + self + } +} + +/* +impl View for NoteView<'_, '_> { + fn ui(&mut self, ui: &mut egui::Ui) { + self.show(ui); + } +} +*/ + +impl<'a, 'd> NoteView<'a, 'd> { + pub fn new( + note_context: &'a mut NoteContext<'d>, + cur_acc: &'a Option>, + note: &'a nostrdb::Note<'a>, + mut flags: NoteOptions, + ) -> Self { + flags.set_actionbar(true); + flags.set_note_previews(true); + + let parent: Option = None; + Self { + note_context, + cur_acc, + parent, + note, + flags, + } + } + + pub fn textmode(mut self, enable: bool) -> Self { + self.options_mut().set_textmode(enable); + self + } + + pub fn actionbar(mut self, enable: bool) -> Self { + self.options_mut().set_actionbar(enable); + self + } + + pub fn small_pfp(mut self, enable: bool) -> Self { + self.options_mut().set_small_pfp(enable); + self + } + + pub fn medium_pfp(mut self, enable: bool) -> Self { + self.options_mut().set_medium_pfp(enable); + self + } + + pub fn note_previews(mut self, enable: bool) -> Self { + self.options_mut().set_note_previews(enable); + self + } + + pub fn selectable_text(mut self, enable: bool) -> Self { + self.options_mut().set_selectable_text(enable); + self + } + + pub fn wide(mut self, enable: bool) -> Self { + self.options_mut().set_wide(enable); + self + } + + pub fn options_button(mut self, enable: bool) -> Self { + self.options_mut().set_options_button(enable); + self + } + + pub fn options(&self) -> NoteOptions { + self.flags + } + + pub fn options_mut(&mut self) -> &mut NoteOptions { + &mut self.flags + } + + pub fn parent(mut self, parent: NoteKey) -> Self { + self.parent = Some(parent); + self + } + + pub fn is_preview(mut self, is_preview: bool) -> Self { + self.options_mut().set_is_preview(is_preview); + self + } + + fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response { + let note_key = self.note.key().expect("todo: implement non-db notes"); + let txn = self.note.txn().expect("todo: implement non-db notes"); + + ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { + let profile = self + .note_context + .ndb + .get_profile_by_pubkey(txn, self.note.pubkey()); + + //ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 2.0; + + let cached_note = self + .note_context + .note_cache + .cached_note_or_insert_mut(note_key, self.note); + + let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0)); + ui.allocate_rect(rect, Sense::hover()); + ui.put(rect, |ui: &mut egui::Ui| { + render_reltime(ui, cached_note, false).response + }); + let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0)); + ui.allocate_rect(rect, Sense::hover()); + ui.put(rect, |ui: &mut egui::Ui| { + ui.add( + Username::new(profile.as_ref().ok(), self.note.pubkey()) + .abbreviated(6) + .pk_colored(true), + ) + }); + + ui.add(&mut NoteContents::new( + self.note_context, + self.cur_acc, + txn, + self.note, + self.flags, + )); + //}); + }) + .response + } + + pub fn expand_size() -> i8 { + 5 + } + + fn pfp( + &mut self, + note_key: NoteKey, + profile: &Result, nostrdb::Error>, + ui: &mut egui::Ui, + ) -> egui::Response { + if !self.options().has_wide() { + ui.spacing_mut().item_spacing.x = 16.0; + } else { + ui.spacing_mut().item_spacing.x = 4.0; + } + + let pfp_size = self.options().pfp_size(); + + let sense = Sense::click(); + match profile + .as_ref() + .ok() + .and_then(|p| p.record().profile()?.picture()) + { + // these have different lifetimes and types, + // so the calls must be separate + Some(pic) => { + let anim_speed = 0.05; + let profile_key = profile.as_ref().unwrap().record().note_key(); + let note_key = note_key.as_u64(); + + let (rect, size, resp) = crate::anim::hover_expand( + ui, + egui::Id::new((profile_key, note_key)), + pfp_size as f32, + NoteView::expand_size() as f32, + anim_speed, + ); + + ui.put( + rect, + ProfilePic::new(self.note_context.img_cache, pic).size(size), + ) + .on_hover_ui_at_pointer(|ui| { + ui.set_max_width(300.0); + ui.add(ProfilePreview::new( + profile.as_ref().unwrap(), + self.note_context.img_cache, + )); + }); + + if resp.hovered() || resp.clicked() { + crate::show_pointer(ui); + } + + resp + } + + None => { + // This has to match the expand size from the above case to + // prevent bounciness + let size = (pfp_size + NoteView::expand_size()) as f32; + let (rect, _response) = ui.allocate_exact_size(egui::vec2(size, size), sense); + + ui.put( + rect, + ProfilePic::new(self.note_context.img_cache, notedeck::profile::no_pfp_url()) + .size(pfp_size as f32), + ) + .interact(sense) + } + } + } + + pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse { + if self.options().has_textmode() { + NoteResponse::new(self.textmode_ui(ui)) + } else { + let txn = self.note.txn().expect("txn"); + if let Some(note_to_repost) = get_reposted_note(self.note_context.ndb, txn, self.note) { + let profile = self + .note_context + .ndb + .get_profile_by_pubkey(txn, self.note.pubkey()); + + let style = NotedeckTextStyle::Small; + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.add_space(2.0); + ui.add_sized([20.0, 20.0], repost_icon(ui.visuals().dark_mode)); + }); + ui.add_space(6.0); + let resp = ui.add(one_line_display_name_widget( + ui.visuals(), + get_display_name(profile.as_ref().ok()), + style, + )); + if let Ok(rec) = &profile { + resp.on_hover_ui_at_pointer(|ui| { + ui.set_max_width(300.0); + ui.add(ProfilePreview::new(rec, self.note_context.img_cache)); + }); + } + let color = ui.style().visuals.noninteractive().fg_stroke.color; + ui.add_space(4.0); + ui.label( + RichText::new("Reposted") + .color(color) + .text_style(style.text_style()), + ); + }); + NoteView::new(self.note_context, self.cur_acc, ¬e_to_repost, self.flags).show(ui) + } else { + self.show_standard(ui) + } + } + } + + #[profiling::function] + fn note_header( + ui: &mut egui::Ui, + note_cache: &mut NoteCache, + note: &Note, + profile: &Result, nostrdb::Error>, + ) { + let note_key = note.key().unwrap(); + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 2.0; + ui.add(Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20)); + + let cached_note = note_cache.cached_note_or_insert_mut(note_key, note); + render_reltime(ui, cached_note, true); + }); + } + + #[profiling::function] + fn show_standard(&mut self, ui: &mut egui::Ui) -> NoteResponse { + let note_key = self.note.key().expect("todo: support non-db notes"); + let txn = self.note.txn().expect("todo: support non-db notes"); + + let mut note_action: Option = None; + + let hitbox_id = note_hitbox_id(note_key, self.options(), self.parent); + let profile = self + .note_context + .ndb + .get_profile_by_pubkey(txn, self.note.pubkey()); + let maybe_hitbox = maybe_note_hitbox(ui, hitbox_id); + + // wide design + let response = if self.options().has_wide() { + ui.vertical(|ui| { + ui.horizontal(|ui| { + if self.pfp(note_key, &profile, ui).clicked() { + note_action = Some(NoteAction::Profile(Pubkey::new(*self.note.pubkey()))); + }; + + let size = ui.available_size(); + ui.vertical(|ui| { + ui.add_sized( + [size.x, self.options().pfp_size() as f32], + |ui: &mut egui::Ui| { + ui.horizontal_centered(|ui| { + NoteView::note_header( + ui, + self.note_context.note_cache, + self.note, + &profile, + ); + }) + .response + }, + ); + + let note_reply = self + .note_context + .note_cache + .cached_note_or_insert_mut(note_key, self.note) + .reply + .borrow(self.note.tags()); + + if note_reply.reply().is_some() { + let action = ui + .horizontal(|ui| { + reply_desc( + ui, + self.cur_acc, + txn, + ¬e_reply, + self.note_context, + self.flags, + ) + }) + .inner; + + if action.is_some() { + note_action = action; + } + } + }); + }); + + let mut contents = + NoteContents::new(self.note_context, self.cur_acc, txn, self.note, self.flags); + + ui.add(&mut contents); + + if let Some(action) = contents.action() { + note_action = Some(action.clone()); + } + + if self.options().has_actionbar() { + if let Some(action) = render_note_actionbar( + ui, + self.note_context.zaps, + self.cur_acc.as_ref(), + self.note.id(), + self.note.pubkey(), + note_key, + ) + .inner + { + note_action = Some(action); + } + } + }) + .response + } else { + // main design + ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { + if self.pfp(note_key, &profile, ui).clicked() { + note_action = Some(NoteAction::Profile(Pubkey::new(*self.note.pubkey()))); + }; + + ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { + NoteView::note_header(ui, self.note_context.note_cache, self.note, &profile); + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 2.0; + + let note_reply = self + .note_context + .note_cache + .cached_note_or_insert_mut(note_key, self.note) + .reply + .borrow(self.note.tags()); + + if note_reply.reply().is_some() { + let action = reply_desc( + ui, + self.cur_acc, + txn, + ¬e_reply, + self.note_context, + self.flags, + ); + + if action.is_some() { + note_action = action; + } + } + }); + + let mut contents = NoteContents::new( + self.note_context, + self.cur_acc, + txn, + self.note, + self.flags, + ); + ui.add(&mut contents); + + if let Some(action) = contents.action() { + note_action = Some(action.clone()); + } + + if self.options().has_actionbar() { + if let Some(action) = render_note_actionbar( + ui, + self.note_context.zaps, + self.cur_acc.as_ref(), + self.note.id(), + self.note.pubkey(), + note_key, + ) + .inner + { + note_action = Some(action); + } + } + }); + }) + .response + }; + + if self.options().has_options_button() { + let context_pos = { + let size = NoteContextButton::max_width(); + let top_right = response.rect.right_top(); + let min = Pos2::new(top_right.x - size, top_right.y); + Rect::from_min_size(min, egui::vec2(size, size)) + }; + + let resp = ui.add(NoteContextButton::new(note_key).place_at(context_pos)); + if let Some(action) = NoteContextButton::menu(ui, resp.clone()) { + note_action = Some(NoteAction::Context(ContextSelection { note_key, action })); + } + } + + let note_action = if note_hitbox_clicked(ui, hitbox_id, &response.rect, maybe_hitbox) { + Some(NoteAction::Note(NoteId::new(*self.note.id()))) + } else { + note_action + }; + + NoteResponse::new(response).with_action(note_action) + } +} + +fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option> { + let new_note_id: &[u8; 32] = if note.kind() == 6 { + let mut res = None; + for tag in note.tags().iter() { + if tag.count() == 0 { + continue; + } + + if let Some("e") = tag.get(0).and_then(|t| t.variant().str()) { + if let Some(note_id) = tag.get(1).and_then(|f| f.variant().id()) { + res = Some(note_id); + break; + } + } + } + res? + } else { + return None; + }; + + let note = ndb.get_note_by_id(txn, new_note_id).ok(); + note.filter(|note| note.kind() == 1) +} + +fn note_hitbox_id( + note_key: NoteKey, + note_options: NoteOptions, + parent: Option, +) -> egui::Id { + Id::new(("note_size", note_key, note_options, parent)) +} + +fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option { + ui.ctx() + .data_mut(|d| d.get_persisted(hitbox_id)) + .map(|note_size: Vec2| { + // The hitbox should extend the entire width of the + // container. The hitbox height was cached last layout. + let container_rect = ui.max_rect(); + let rect = Rect { + min: pos2(container_rect.min.x, container_rect.min.y), + max: pos2(container_rect.max.x, container_rect.min.y + note_size.y), + }; + + let response = ui.interact(rect, ui.id().with(hitbox_id), egui::Sense::click()); + + response + .widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Other, true, "hitbox")); + + response + }) +} + +fn note_hitbox_clicked( + ui: &mut egui::Ui, + hitbox_id: egui::Id, + note_rect: &Rect, + maybe_hitbox: Option, +) -> bool { + // Stash the dimensions of the note content so we can render the + // hitbox in the next frame + ui.ctx().data_mut(|d| { + d.insert_persisted(hitbox_id, note_rect.size()); + }); + + // If there was an hitbox and it was clicked open the thread + match maybe_hitbox { + Some(hitbox) => hitbox.clicked(), + _ => false, + } +} + +#[profiling::function] +fn render_note_actionbar( + ui: &mut egui::Ui, + zaps: &Zaps, + cur_acc: Option<&KeypairUnowned>, + note_id: &[u8; 32], + note_pubkey: &[u8; 32], + note_key: NoteKey, +) -> egui::InnerResponse> { + ui.horizontal(|ui| 's: { + let reply_resp = reply_button(ui, note_key); + let quote_resp = quote_repost_button(ui, note_key); + + let zap_target = ZapTarget::Note(NoteZapTarget { + note_id, + zap_recipient: note_pubkey, + }); + + let zap_state = cur_acc.map_or_else( + || Ok(AnyZapState::None), + |kp| zaps.any_zap_state_for(kp.pubkey.bytes(), zap_target), + ); + let zap_resp = cur_acc + .filter(|k| k.secret_key.is_some()) + .map(|_| match &zap_state { + Ok(any_zap_state) => ui.add(zap_button(any_zap_state.clone(), note_id)), + Err(zapping_error) => { + let (rect, _) = + ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click()); + ui.add(x_button(rect)) + .on_hover_text(format!("{zapping_error}")) + } + }); + + let to_noteid = |id: &[u8; 32]| NoteId::new(*id); + + if reply_resp.clicked() { + break 's Some(NoteAction::Reply(to_noteid(note_id))); + } + + if quote_resp.clicked() { + break 's Some(NoteAction::Quote(to_noteid(note_id))); + } + + let Some(zap_resp) = zap_resp else { + break 's None; + }; + + if !zap_resp.clicked() { + break 's None; + } + + let target = NoteZapTargetOwned { + note_id: to_noteid(note_id), + zap_recipient: Pubkey::new(*note_pubkey), + }; + + if zap_state.is_err() { + break 's Some(NoteAction::Zap(ZapAction::ClearError(target))); + } + + Some(NoteAction::Zap(ZapAction::Send(target))) + }) +} + +fn secondary_label(ui: &mut egui::Ui, s: impl Into) { + let color = ui.style().visuals.noninteractive().fg_stroke.color; + ui.add(Label::new(RichText::new(s).size(10.0).color(color))); +} + +#[profiling::function] +fn render_reltime( + ui: &mut egui::Ui, + note_cache: &mut CachedNote, + before: bool, +) -> egui::InnerResponse<()> { + ui.horizontal(|ui| { + if before { + secondary_label(ui, "⋅"); + } + + secondary_label(ui, note_cache.reltime_str_mut()); + + if !before { + secondary_label(ui, "⋅"); + } + }) +} + +fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { + let img_data = if ui.style().visuals.dark_mode { + egui::include_image!("../../../../assets/icons/reply.png") + } else { + egui::include_image!("../../../../assets/icons/reply-dark.png") + }; + + let (rect, size, resp) = + crate::anim::hover_expand_small(ui, ui.id().with(("reply_anim", note_key))); + + // align rect to note contents + let expand_size = 5.0; // from hover_expand_small + let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); + + let put_resp = ui.put(rect, egui::Image::new(img_data).max_width(size)); + + resp.union(put_resp) +} + +fn repost_icon(dark_mode: bool) -> egui::Image<'static> { + let img_data = if dark_mode { + egui::include_image!("../../../../assets/icons/repost_icon_4x.png") + } else { + egui::include_image!("../../../../assets/icons/repost_light_4x.png") + }; + egui::Image::new(img_data) +} + +fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { + let size = 14.0; + let expand_size = 5.0; + let anim_speed = 0.05; + let id = ui.id().with(("repost_anim", note_key)); + + let (rect, size, resp) = crate::anim::hover_expand(ui, id, size, expand_size, anim_speed); + + let rect = rect.translate(egui::vec2(-(expand_size / 2.0), -1.0)); + + let put_resp = ui.put(rect, repost_icon(ui.visuals().dark_mode).max_width(size)); + + resp.union(put_resp) +} + +fn zap_button(state: AnyZapState, noteid: &[u8; 32]) -> impl egui::Widget + use<'_> { + move |ui: &mut egui::Ui| -> egui::Response { + let img_data = egui::include_image!("../../../../assets/icons/zap_4x.png"); + + let (rect, size, resp) = crate::anim::hover_expand_small(ui, ui.id().with("zap")); + + let mut img = egui::Image::new(img_data).max_width(size); + let id = ui.id().with(("pulse", noteid)); + let ctx = ui.ctx().clone(); + + match state { + AnyZapState::None => { + if !ui.visuals().dark_mode { + img = img.tint(egui::Color32::BLACK); + } + } + AnyZapState::Pending => { + let alpha_min = if ui.visuals().dark_mode { 50 } else { 180 }; + img = ImagePulseTint::new(&ctx, id, img, &[0xFF, 0xB7, 0x57], alpha_min, 255) + .with_speed(0.35) + .animate(); + } + AnyZapState::LocalOnly => { + img = img.tint(egui::Color32::from_rgb(0xFF, 0xB7, 0x57)); + } + AnyZapState::Confirmed => {} + } + + // align rect to note contents + let expand_size = 5.0; // from hover_expand_small + let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); + + let put_resp = ui.put(rect, img); + + resp.union(put_resp) + } +} diff --git a/crates/notedeck_columns/src/ui/note/options.rs b/crates/notedeck_ui/src/note/options.rs similarity index 99% rename from crates/notedeck_columns/src/ui/note/options.rs rename to crates/notedeck_ui/src/note/options.rs index eb9e1db..2cf0e0a 100644 --- a/crates/notedeck_columns/src/ui/note/options.rs +++ b/crates/notedeck_ui/src/note/options.rs @@ -1,4 +1,4 @@ -use crate::ui::ProfilePic; +use crate::ProfilePic; use bitflags::bitflags; bitflags! { diff --git a/crates/notedeck_columns/src/ui/note/reply_description.rs b/crates/notedeck_ui/src/note/reply_description.rs similarity index 92% rename from crates/notedeck_columns/src/ui/note/reply_description.rs rename to crates/notedeck_ui/src/note/reply_description.rs index 09239f3..3c3e6f0 100644 --- a/crates/notedeck_columns/src/ui/note/reply_description.rs +++ b/crates/notedeck_ui/src/note/reply_description.rs @@ -1,12 +1,10 @@ -use crate::{ - actionbar::NoteAction, - ui::{self}, -}; use egui::{Label, RichText, Sense}; -use enostr::KeypairUnowned; use nostrdb::{Note, NoteReply, Transaction}; -use super::{contents::NoteContext, NoteOptions}; +use super::NoteOptions; +use crate::{note::NoteView, Mention}; +use enostr::KeypairUnowned; +use notedeck::{NoteAction, NoteContext}; #[must_use = "Please handle the resulting note action"] #[profiling::function] @@ -41,7 +39,7 @@ pub fn reply_desc( if r.hovered() { r.on_hover_ui_at_pointer(|ui| { ui.set_max_width(400.0); - ui::NoteView::new(note_context, cur_acc, note, note_options) + NoteView::new(note_context, cur_acc, note, note_options) .actionbar(false) .wide(true) .show(ui); @@ -62,7 +60,7 @@ pub fn reply_desc( if note_reply.is_reply_to_root() { // We're replying to the root, let's show this - let action = ui::Mention::new( + let action = Mention::new( note_context.ndb, note_context.img_cache, txn, @@ -86,7 +84,7 @@ pub fn reply_desc( if let Ok(root_note) = note_context.ndb.get_note_by_id(txn, root.id) { if root_note.pubkey() == reply_note.pubkey() { // simply "replying to bob's note" when replying to bob in his thread - let action = ui::Mention::new( + let action = Mention::new( note_context.ndb, note_context.img_cache, txn, @@ -109,7 +107,7 @@ pub fn reply_desc( } else { // replying to bob in alice's thread - let action = ui::Mention::new( + let action = Mention::new( note_context.ndb, note_context.img_cache, txn, @@ -134,7 +132,7 @@ pub fn reply_desc( Label::new(RichText::new("in").size(size).color(color)).selectable(selectable), ); - let action = ui::Mention::new( + let action = Mention::new( note_context.ndb, note_context.img_cache, txn, @@ -156,7 +154,7 @@ pub fn reply_desc( note_link(ui, note_context, "thread", &root_note); } } else { - let action = ui::Mention::new( + let action = Mention::new( note_context.ndb, note_context.img_cache, txn, diff --git a/crates/notedeck_ui/src/profile/mod.rs b/crates/notedeck_ui/src/profile/mod.rs index f631931..ccb9235 100644 --- a/crates/notedeck_ui/src/profile/mod.rs +++ b/crates/notedeck_ui/src/profile/mod.rs @@ -1,17 +1,116 @@ use nostrdb::ProfileRecord; +pub mod name; pub mod picture; +pub mod preview; pub use picture::ProfilePic; +pub use preview::ProfilePreview; -pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { - unwrap_profile_url(profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture()))) -} +use egui::{load::TexturePoll, Label, RichText}; +use notedeck::{NostrName, NotedeckTextStyle}; -pub fn unwrap_profile_url(maybe_url: Option<&str>) -> &str { - if let Some(url) = maybe_url { - url - } else { - ProfilePic::no_pfp_url() +pub fn display_name_widget<'a>( + name: &'a NostrName<'a>, + add_placeholder_space: bool, +) -> impl egui::Widget + 'a { + move |ui: &mut egui::Ui| -> egui::Response { + let disp_resp = name.display_name.map(|disp_name| { + ui.add( + Label::new( + RichText::new(disp_name).text_style(NotedeckTextStyle::Heading3.text_style()), + ) + .selectable(false), + ) + }); + + let (username_resp, nip05_resp) = ui + .horizontal(|ui| { + let username_resp = name.username.map(|username| { + ui.add( + Label::new( + RichText::new(format!("@{}", username)) + .size(16.0) + .color(crate::colors::MID_GRAY), + ) + .selectable(false), + ) + }); + + let nip05_resp = name.nip05.map(|nip05| { + ui.image(egui::include_image!( + "../../../../assets/icons/verified_4x.png" + )); + ui.add(Label::new( + RichText::new(nip05).size(16.0).color(crate::colors::TEAL), + )) + }); + + (username_resp, nip05_resp) + }) + .inner; + + let resp = match (disp_resp, username_resp, nip05_resp) { + (Some(disp), Some(username), Some(nip05)) => disp.union(username).union(nip05), + (Some(disp), Some(username), None) => disp.union(username), + (Some(disp), None, None) => disp, + (None, Some(username), Some(nip05)) => username.union(nip05), + (None, Some(username), None) => username, + _ => ui.add(Label::new(RichText::new(name.name()))), + }; + + if add_placeholder_space { + ui.add_space(16.0); + } + + resp } } + +pub fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b +where + 'b: 'a, +{ + move |ui: &mut egui::Ui| { + if let Some(about) = profile.record().profile().and_then(|p| p.about()) { + let resp = ui.label(about); + ui.add_space(8.0); + resp + } else { + // need any Response so we dont need an Option + ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()) + } + } +} + +pub fn banner_texture(ui: &mut egui::Ui, banner_url: &str) -> Option { + // TODO: cache banner + if !banner_url.is_empty() { + let texture_load_res = + egui::Image::new(banner_url).load_for_size(ui.ctx(), ui.available_size()); + if let Ok(texture_poll) = texture_load_res { + match texture_poll { + TexturePoll::Pending { .. } => {} + TexturePoll::Ready { texture, .. } => return Some(texture), + } + } + } + + None +} + +pub fn banner(ui: &mut egui::Ui, banner_url: Option<&str>, height: f32) -> egui::Response { + ui.add_sized([ui.available_size().x, height], |ui: &mut egui::Ui| { + banner_url + .and_then(|url| banner_texture(ui, url)) + .map(|texture| { + crate::images::aspect_fill( + ui, + egui::Sense::hover(), + texture.id, + texture.size.x / texture.size.y, + ) + }) + .unwrap_or_else(|| ui.label("")) + }) +} diff --git a/crates/notedeck_ui/src/profile/name.rs b/crates/notedeck_ui/src/profile/name.rs new file mode 100644 index 0000000..afc2f14 --- /dev/null +++ b/crates/notedeck_ui/src/profile/name.rs @@ -0,0 +1,19 @@ +use egui::RichText; +use notedeck::{NostrName, NotedeckTextStyle}; + +pub fn one_line_display_name_widget<'a>( + visuals: &egui::Visuals, + display_name: NostrName<'a>, + style: NotedeckTextStyle, +) -> impl egui::Widget + 'a { + let text_style = style.text_style(); + let color = visuals.noninteractive().fg_stroke.color; + + move |ui: &mut egui::Ui| -> egui::Response { + ui.label( + RichText::new(display_name.name()) + .text_style(text_style) + .color(color), + ) + } +} diff --git a/crates/notedeck_ui/src/profile/picture.rs b/crates/notedeck_ui/src/profile/picture.rs index c4b04d3..e2e1040 100644 --- a/crates/notedeck_ui/src/profile/picture.rs +++ b/crates/notedeck_ui/src/profile/picture.rs @@ -58,11 +58,6 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> { 24 } - #[inline] - pub fn no_pfp_url() -> &'static str { - "https://damus.io/img/no-profile.svg" - } - #[inline] pub fn size(mut self, size: f32) -> Self { self.size = size; diff --git a/crates/notedeck_columns/src/ui/profile/preview.rs b/crates/notedeck_ui/src/profile/preview.rs similarity index 63% rename from crates/notedeck_columns/src/ui/profile/preview.rs rename to crates/notedeck_ui/src/profile/preview.rs index 6ba7bfe..201ae72 100644 --- a/crates/notedeck_columns/src/ui/profile/preview.rs +++ b/crates/notedeck_ui/src/profile/preview.rs @@ -1,13 +1,11 @@ -use crate::ui::ProfilePic; -use crate::NostrName; -use egui::{Frame, Label, RichText, Widget}; +use crate::ProfilePic; +use egui::{Frame, Label, RichText}; use egui_extras::Size; use nostrdb::ProfileRecord; -use notedeck::{Images, NotedeckTextStyle}; +use notedeck::{name::get_display_name, profile::get_profile_url, Images, NotedeckTextStyle}; -use super::{about_section_widget, banner, display_name_widget, get_display_name}; -use notedeck_ui::profile::get_profile_url; +use super::{about_section_widget, banner, display_name_widget}; pub struct ProfilePreview<'a, 'cache> { profile: &'a ProfileRecord<'a>, @@ -31,7 +29,7 @@ impl<'a, 'cache> ProfilePreview<'a, 'cache> { fn body(self, ui: &mut egui::Ui) { let padding = 12.0; - crate::ui::padding(padding, ui, |ui| { + crate::padding(padding, ui, |ui| { let mut pfp_rect = ui.available_rect_before_wrap(); let size = 80.0; pfp_rect.set_width(size); @@ -113,59 +111,3 @@ impl egui::Widget for SimpleProfilePreview<'_, '_> { .response } } - -mod previews { - use super::*; - use crate::test_data::test_profile_record; - use crate::ui::{Preview, PreviewConfig}; - use notedeck::{App, AppContext}; - - pub struct ProfilePreviewPreview<'a> { - profile: ProfileRecord<'a>, - } - - impl ProfilePreviewPreview<'_> { - pub fn new() -> Self { - let profile = test_profile_record(); - ProfilePreviewPreview { profile } - } - } - - impl Default for ProfilePreviewPreview<'_> { - fn default() -> Self { - ProfilePreviewPreview::new() - } - } - - impl App for ProfilePreviewPreview<'_> { - fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) { - ProfilePreview::new(&self.profile, app.img_cache).ui(ui); - } - } - - impl<'a> Preview for ProfilePreview<'a, '_> { - /// A preview of the profile preview :D - type Prev = ProfilePreviewPreview<'a>; - - fn preview(_cfg: PreviewConfig) -> Self::Prev { - ProfilePreviewPreview::new() - } - } -} - -pub fn one_line_display_name_widget<'a>( - visuals: &egui::Visuals, - display_name: NostrName<'a>, - style: NotedeckTextStyle, -) -> impl egui::Widget + 'a { - let text_style = style.text_style(); - let color = visuals.noninteractive().fg_stroke.color; - - move |ui: &mut egui::Ui| -> egui::Response { - ui.label( - RichText::new(display_name.name()) - .text_style(text_style) - .color(color), - ) - } -} diff --git a/crates/notedeck_columns/src/ui/username.rs b/crates/notedeck_ui/src/username.rs similarity index 97% rename from crates/notedeck_columns/src/ui/username.rs rename to crates/notedeck_ui/src/username.rs index 2444a67..9ec0036 100644 --- a/crates/notedeck_columns/src/ui/username.rs +++ b/crates/notedeck_ui/src/username.rs @@ -76,7 +76,7 @@ fn colored_name(name: &str, color: Option) -> RichText { fn ui_abbreviate_name(ui: &mut egui::Ui, name: &str, len: usize, color: Option) { let should_abbrev = name.len() > len; let name = if should_abbrev { - let closest = crate::abbrev::floor_char_boundary(name, len); + let closest = notedeck::abbrev::floor_char_boundary(name, len); &name[..closest] } else { name diff --git a/crates/notedeck_ui/src/widgets.rs b/crates/notedeck_ui/src/widgets.rs new file mode 100644 index 0000000..dfa0f3f --- /dev/null +++ b/crates/notedeck_ui/src/widgets.rs @@ -0,0 +1,35 @@ +use crate::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}; +use egui::{emath::GuiRounding, Pos2, Stroke}; + +pub fn x_button(rect: egui::Rect) -> impl egui::Widget { + move |ui: &mut egui::Ui| -> egui::Response { + let max_width = rect.width(); + let helper = AnimationHelper::new_from_rect(ui, "user_search_close", rect); + + let fill_color = ui.visuals().text_color(); + + let radius = max_width / (2.0 * ICON_EXPANSION_MULTIPLE); + + let painter = ui.painter(); + let ppp = ui.ctx().pixels_per_point(); + let nw_edge = helper + .scale_pos_from_center(Pos2::new(-radius, radius)) + .round_to_pixel_center(ppp); + let se_edge = helper + .scale_pos_from_center(Pos2::new(radius, -radius)) + .round_to_pixel_center(ppp); + let sw_edge = helper + .scale_pos_from_center(Pos2::new(-radius, -radius)) + .round_to_pixel_center(ppp); + let ne_edge = helper + .scale_pos_from_center(Pos2::new(radius, radius)) + .round_to_pixel_center(ppp); + + let line_width = helper.scale_1d_pos(2.0); + + painter.line_segment([nw_edge, se_edge], Stroke::new(line_width, fill_color)); + painter.line_segment([ne_edge, sw_edge], Stroke::new(line_width, fill_color)); + + helper.take_animation_response() + } +}