i18n: make localization context non-global

- Simplify Localization{Context,Manager} to just Localization
- Fixed a bunch of lifetime issueo
- Removed all Arcs and Locks
- Removed globals
  * widgets now need access to &mut Localization for i18n

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-06-29 11:05:31 -07:00
parent d1e222f732
commit 3d4db820b4
47 changed files with 1414 additions and 1166 deletions

31
Cargo.lock generated
View File

@@ -4339,6 +4339,12 @@ dependencies = [
"toml_edit", "toml_edit",
] ]
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@@ -6064,6 +6070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
dependencies = [ dependencies = [
"unic-langid-impl", "unic-langid-impl",
"unic-langid-macros",
] ]
[[package]] [[package]]
@@ -6075,6 +6082,30 @@ dependencies = [
"tinystr", "tinystr",
] ]
[[package]]
name = "unic-langid-macros"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25"
dependencies = [
"proc-macro-hack",
"tinystr",
"unic-langid-impl",
"unic-langid-macros-impl",
]
[[package]]
name = "unic-langid-macros-impl"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5"
dependencies = [
"proc-macro-hack",
"quote",
"syn 2.0.104",
"unic-langid-impl",
]
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.8.1" version = "2.8.1"

View File

@@ -65,7 +65,7 @@ tracing = { version = "0.1.40", features = ["log"] }
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tempfile = "3.13.0" tempfile = "3.13.0"
unic-langid = "0.9.6" unic-langid = { version = "0.9.6", features = ["macros"] }
url = "2.5.2" url = "2.5.2"
urlencoding = "2.1.3" urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4"] } uuid = { version = "1.10.0", features = ["v4"] }

View File

@@ -1,5 +1,5 @@
use crate::account::FALLBACK_PUBKEY; use crate::account::FALLBACK_PUBKEY;
use crate::i18n::{LocalizationContext, LocalizationManager}; use crate::i18n::Localization;
use crate::persist::{AppSizeHandler, ZoomHandler}; use crate::persist::{AppSizeHandler, ZoomHandler};
use crate::wallet::GlobalWallet; use crate::wallet::GlobalWallet;
use crate::zaps::Zaps; use crate::zaps::Zaps;
@@ -18,7 +18,6 @@ use std::cell::RefCell;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::path::Path; use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use tracing::{error, info}; use tracing::{error, info};
pub enum AppAction { pub enum AppAction {
@@ -50,7 +49,7 @@ pub struct Notedeck {
zaps: Zaps, zaps: Zaps,
frame_history: FrameHistory, frame_history: FrameHistory,
job_pool: JobPool, job_pool: JobPool,
i18n: LocalizationContext, i18n: Localization,
} }
/// Our chrome, which is basically nothing /// Our chrome, which is basically nothing
@@ -231,19 +230,10 @@ impl Notedeck {
let job_pool = JobPool::default(); let job_pool = JobPool::default();
// Initialize localization // Initialize localization
let i18n_resource_dir = Path::new("assets/translations"); let i18n = Localization::new();
let localization_manager = Arc::new(
LocalizationManager::new(i18n_resource_dir).unwrap_or_else(|e| {
error!("Failed to initialize localization manager: {}", e);
// Create a fallback manager with a temporary directory
LocalizationManager::new(&std::env::temp_dir().join("notedeck_i18n_fallback"))
.expect("Failed to create fallback localization manager")
}),
);
let i18n = LocalizationContext::new(localization_manager);
// Initialize global i18n context // Initialize global i18n context
crate::i18n::init_global_i18n(i18n.clone()); //crate::i18n::init_global_i18n(i18n.clone());
Self { Self {
ndb, ndb,
@@ -289,7 +279,7 @@ impl Notedeck {
zaps: &mut self.zaps, zaps: &mut self.zaps,
frame_history: &mut self.frame_history, frame_history: &mut self.frame_history,
job_pool: &mut self.job_pool, job_pool: &mut self.job_pool,
i18n: &self.i18n, i18n: &mut self.i18n,
} }
} }

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
account::accounts::Accounts, frame_history::FrameHistory, i18n::LocalizationContext, account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler, wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
UnknownIds, UnknownIds,
}; };
@@ -25,5 +25,5 @@ pub struct AppContext<'a> {
pub zaps: &'a mut Zaps, pub zaps: &'a mut Zaps,
pub frame_history: &'a mut FrameHistory, pub frame_history: &'a mut FrameHistory,
pub job_pool: &'a mut JobPool, pub job_pool: &'a mut JobPool,
pub i18n: &'a LocalizationContext, pub i18n: &'a mut Localization,
} }

View File

@@ -0,0 +1,24 @@
use super::IntlKeyBuf;
use unic_langid::LanguageIdentifier;
/// App related errors
#[derive(thiserror::Error, Debug)]
pub enum IntlError {
#[error("message not found: {0}")]
NotFound(IntlKeyBuf),
#[error("message has no value: {0}")]
NoValue(IntlKeyBuf),
#[error("Locale({0}) parse error: {1}")]
LocaleParse(LanguageIdentifier, String),
#[error("locale not available: {0}")]
LocaleNotAvailable(LanguageIdentifier),
#[error("FTL for '{0}' is not available")]
NoFtl(LanguageIdentifier),
#[error("Bundle for '{0}' is not available")]
NoBundle(LanguageIdentifier),
}

View File

@@ -0,0 +1,47 @@
use std::fmt;
/// An owned key used to lookup i18n translations. Mostly used for errors
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct IntlKeyBuf(String);
/// A key used to lookup i18n translations
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub struct IntlKey<'a>(&'a str);
impl fmt::Display for IntlKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", self.0)
}
}
impl fmt::Display for IntlKeyBuf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", &self.0)
}
}
impl IntlKeyBuf {
pub fn new(string: impl Into<String>) -> Self {
IntlKeyBuf(string.into())
}
pub fn borrow<'a>(&'a self) -> IntlKey<'a> {
IntlKey::new(&self.0)
}
}
impl<'a> IntlKey<'a> {
pub fn new(string: &'a str) -> IntlKey<'a> {
IntlKey(string)
}
pub fn to_owned(&self) -> IntlKeyBuf {
IntlKeyBuf::new(self.0)
}
pub fn as_str(&self) -> &'a str {
self.0
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,104 +4,22 @@
//! It handles loading translation files, managing locales, and providing //! It handles loading translation files, managing locales, and providing
//! localized strings throughout the application. //! localized strings throughout the application.
mod error;
mod key;
pub mod manager; pub mod manager;
pub use error::IntlError;
pub use key::{IntlKey, IntlKeyBuf};
pub use manager::CacheStats; pub use manager::CacheStats;
pub use manager::LocalizationContext; pub use manager::Localization;
pub use manager::LocalizationManager; pub use manager::StringCacheResult;
/// Re-export commonly used types for convenience /// Re-export commonly used types for convenience
pub use fluent::FluentArgs; pub use fluent::FluentArgs;
pub use fluent::FluentValue; pub use fluent::FluentValue;
pub use unic_langid::LanguageIdentifier; pub use unic_langid::LanguageIdentifier;
use md5;
use once_cell::sync::OnceCell;
use regex::Regex;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use tracing::info;
/// Global localization manager for easy access from anywhere
static GLOBAL_I18N: OnceCell<Arc<LocalizationManager>> = OnceCell::new();
/// Cache for normalized FTL keys to avoid repeated normalization
static NORMALIZED_KEY_CACHE: OnceCell<Mutex<HashMap<String, String>>> = OnceCell::new();
/// Initialize the global localization context
pub fn init_global_i18n(context: LocalizationContext) {
info!("Initializing global i18n context");
let _ = GLOBAL_I18N.set(context.manager().clone());
// Initialize the normalized key cache
let _ = NORMALIZED_KEY_CACHE.set(Mutex::new(HashMap::new()));
info!("Global i18n context initialized successfully");
}
/// Get the global localization manager
pub fn get_global_i18n() -> Option<Arc<LocalizationManager>> {
GLOBAL_I18N.get().cloned()
}
fn simple_hash(s: &str) -> String {
let digest = md5::compute(s.as_bytes());
// Take the first 2 bytes and convert to 4 hex characters
format!("{:02x}{:02x}", digest[0], digest[1])
}
pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
// Try to get from cache first
let cache_key = if let Some(comment) = comment {
format!("{key}:{comment}")
} else {
key.to_string()
};
if let Some(cache) = NORMALIZED_KEY_CACHE.get() {
if let Ok(cache) = cache.lock() {
if let Some(cached) = cache.get(&cache_key) {
return cached.clone();
}
}
}
// Replace each invalid character with exactly one underscore
// This matches the behavior of the Python extraction script
let re = Regex::new(r"[^a-zA-Z0-9_-]").unwrap();
let mut result = re.replace_all(key, "_").to_string();
// Remove leading/trailing underscores
result = result.trim_matches('_').to_string();
// Ensure the key starts with a letter (Fluent requirement)
if result.is_empty() || !result.chars().next().unwrap().is_ascii_alphabetic() {
result = format!("k_{result}");
}
// If we have a comment, append a hash of it to reduce collisions
if let Some(comment) = comment {
let hash_str = format!("_{}", simple_hash(comment));
result.push_str(&hash_str);
}
// Cache the result
if let Some(cache) = NORMALIZED_KEY_CACHE.get() {
if let Ok(mut cache) = cache.lock() {
cache.insert(cache_key, result.clone());
}
}
tracing::debug!(
"normalize_ftl_key: original='{}', comment='{:?}', final='{}'",
key,
comment,
result
);
result
}
/// Macro for getting localized strings with format-like syntax /// Macro for getting localized strings with format-like syntax
/// ///
/// Syntax: tr!("message", comment) /// Syntax: tr!("message", comment)
@@ -114,53 +32,36 @@ pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
/// All placeholders must be named and start with a letter (a-zA-Z). /// All placeholders must be named and start with a letter (a-zA-Z).
#[macro_export] #[macro_export]
macro_rules! tr { macro_rules! tr {
// Simple case: just message and comment ($i18n:expr, $message:expr, $comment:expr) => {
($message:expr, $comment:expr) => {
{ {
let norm_key = $crate::i18n::normalize_ftl_key($message, Some($comment)); let key = $i18n.normalized_ftl_key($message, $comment);
if let Some(i18n) = $crate::i18n::get_global_i18n() { match $i18n.get_string(key.borrow()) {
let result = i18n.get_string(&norm_key); Ok(r) => r,
match result { Err(_err) => {
Ok(ref s) if s != $message => s.clone(), $message.to_string()
_ => {
tracing::warn!("FALLBACK: Using key '{}' as string (not found in FTL)", $message);
$message.to_string()
}
} }
} else {
tracing::warn!("FALLBACK: Global i18n not initialized, using key '{}' as string", $message);
$message.to_string()
} }
} }
}; };
// Case with named parameters: message, comment, param=value, ... // Case with named parameters: message, comment, param=value, ...
($message:expr, $comment:expr, $($param:ident = $value:expr),*) => { ($i18n:expr, $message:expr, $comment:expr, $($param:ident = $value:expr),*) => {
{ {
let norm_key = $crate::i18n::normalize_ftl_key($message, Some($comment)); let key = $i18n.normalized_ftl_key($message, $comment);
if let Some(i18n) = $crate::i18n::get_global_i18n() { let mut args = $crate::i18n::FluentArgs::new();
let mut args = $crate::i18n::FluentArgs::new(); $(
$( args.set(stringify!($param), $value);
args.set(stringify!($param), $value); )*
)* match $i18n.get_cached_string(key.borrow(), Some(&args)) {
match i18n.get_string_with_args(&norm_key, Some(&args)) { Ok(r) => r,
Ok(s) => s, Err(_) => {
Err(_) => { // Fallback: replace placeholders with values
// Fallback: replace placeholders with values let mut result = $message.to_string();
let mut result = $message.to_string(); $(
$( result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string()); )*
)* result
result
}
} }
} else {
// Fallback: replace placeholders with values
let mut result = $message.to_string();
$(
result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
)*
result
} }
} }
}; };
@@ -177,42 +78,27 @@ macro_rules! tr {
#[macro_export] #[macro_export]
macro_rules! tr_plural { macro_rules! tr_plural {
// With named parameters // With named parameters
($one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{ ($i18n:expr, $one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{
let norm_key = $crate::i18n::normalize_ftl_key($other, Some($comment)); let norm_key = $i18n.normalized_ftl_key($other, $comment);
if let Some(i18n) = $crate::i18n::get_global_i18n() { let mut args = $crate::i18n::FluentArgs::new();
let mut args = $crate::i18n::FluentArgs::new(); args.set("count", $count);
args.set("count", $count); $(args.set(stringify!($param), $value);)*
$(args.set(stringify!($param), $value);)* match $i18n.get_cached_string(norm_key.borrow(), Some(&args)) {
match i18n.get_string_with_args(&norm_key, Some(&args)) { Ok(s) => s,
Ok(s) => s, Err(_) => {
Err(_) => { // Fallback: use simple pluralization
// Fallback: use simple pluralization if $count == 1 {
if $count == 1 { let mut result = $one.to_string();
let mut result = $one.to_string(); $(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)* result = result.replace("{count}", &$count.to_string());
result = result.replace("{count}", &$count.to_string()); result
result } else {
} else { let mut result = $other.to_string();
let mut result = $other.to_string(); $(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)* result = result.replace("{count}", &$count.to_string());
result = result.replace("{count}", &$count.to_string()); result
result
}
} }
} }
} else {
// Fallback: use simple pluralization
if $count == 1 {
let mut result = $one.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
} else {
let mut result = $other.to_string();
$(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
result = result.replace("{count}", &$count.to_string());
result
}
} }
}}; }};
// Without named parameters // Without named parameters

View File

@@ -45,11 +45,7 @@ pub use context::AppContext;
pub use error::{show_one_error_message, Error, FilterError, ZapError}; pub use error::{show_one_error_message, Error, FilterError, ZapError};
pub use filter::{FilterState, FilterStates, UnifiedSubscription}; pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily; pub use fonts::NamedFontFamily;
pub use i18n::manager::Localizable; pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
pub use i18n::{
CacheStats, FluentArgs, FluentValue, LanguageIdentifier, LocalizationContext,
LocalizationManager,
};
pub use imgcache::{ pub use imgcache::{
Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache, Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache,
MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache, MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache,
@@ -89,5 +85,3 @@ pub use enostr;
pub use nostrdb; pub use nostrdb;
pub use zaps::Zaps; pub use zaps::Zaps;
pub use crate::i18n::{get_global_i18n, init_global_i18n};

View File

@@ -6,6 +6,7 @@ pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
use crate::Accounts; use crate::Accounts;
use crate::JobPool; use crate::JobPool;
use crate::Localization;
use crate::UnknownIds; use crate::UnknownIds;
use crate::{notecache::NoteCache, zaps::Zaps, Images}; use crate::{notecache::NoteCache, zaps::Zaps, Images};
use enostr::{NoteId, RelayPool}; use enostr::{NoteId, RelayPool};
@@ -19,6 +20,7 @@ use std::fmt;
pub struct NoteContext<'d> { pub struct NoteContext<'d> {
pub ndb: &'d Ndb, pub ndb: &'d Ndb,
pub accounts: &'d Accounts, pub accounts: &'d Accounts,
pub i18n: &'d mut Localization,
pub img_cache: &'d mut Images, pub img_cache: &'d mut Images,
pub note_cache: &'d mut NoteCache, pub note_cache: &'d mut NoteCache,
pub zaps: &'d mut Zaps, pub zaps: &'d mut Zaps,

View File

@@ -1,7 +1,5 @@
use crate::{time_ago_since, TimeCached};
use nostrdb::{Note, NoteKey, NoteReply, NoteReplyBuf}; use nostrdb::{Note, NoteKey, NoteReply, NoteReplyBuf};
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration;
#[derive(Default)] #[derive(Default)]
pub struct NoteCache { pub struct NoteCache {
@@ -32,7 +30,7 @@ impl NoteCache {
#[derive(Clone)] #[derive(Clone)]
pub struct CachedNote { pub struct CachedNote {
reltime: TimeCached<String>, //reltime: TimeCached<String>,
pub client: Option<String>, pub client: Option<String>,
pub reply: NoteReplyBuf, pub reply: NoteReplyBuf,
} }
@@ -41,22 +39,25 @@ impl CachedNote {
pub fn new(note: &Note) -> Self { pub fn new(note: &Note) -> Self {
use crate::note::event_tag; use crate::note::event_tag;
/*
let created_at = note.created_at(); let created_at = note.created_at();
let reltime = TimeCached::new( let reltime = TimeCached::new(
Duration::from_secs(1), Duration::from_secs(1),
Box::new(move || time_ago_since(created_at)), Box::new(move || time_ago_since(i18n, created_at)),
); );
*/
let reply = NoteReply::new(note.tags()).to_owned(); let reply = NoteReply::new(note.tags()).to_owned();
let client = event_tag(note, "client"); let client = event_tag(note, "client");
CachedNote { CachedNote {
client: client.map(|c| c.to_string()), client: client.map(|c| c.to_string()),
reltime, // reltime,
reply, reply,
} }
} }
/*
pub fn reltime_str_mut(&mut self) -> &str { pub fn reltime_str_mut(&mut self) -> &str {
self.reltime.get_mut() self.reltime.get_mut()
} }
@@ -64,4 +65,5 @@ impl CachedNote {
pub fn reltime_str(&self) -> Option<&str> { pub fn reltime_str(&self) -> Option<&str> {
self.reltime.get().map(|x| x.as_str()) self.reltime.get().map(|x| x.as_str())
} }
*/
} }

View File

@@ -1,4 +1,4 @@
use crate::tr; use crate::{tr, Localization};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
// Time duration constants in seconds // Time duration constants in seconds
@@ -18,7 +18,7 @@ const MAX_SECONDS_FOR_WEEKS: u64 = ONE_MONTH_IN_SECONDS - 1;
const MAX_SECONDS_FOR_MONTHS: u64 = ONE_YEAR_IN_SECONDS - 1; const MAX_SECONDS_FOR_MONTHS: u64 = ONE_YEAR_IN_SECONDS - 1;
/// Calculate relative time between two timestamps /// Calculate relative time between two timestamps
fn time_ago_between(timestamp: u64, now: u64) -> String { fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String {
// Determine if the timestamp is in the future or the past // Determine if the timestamp is in the future or the past
let duration = if now >= timestamp { let duration = if now >= timestamp {
now.saturating_sub(timestamp) now.saturating_sub(timestamp)
@@ -28,36 +28,48 @@ fn time_ago_between(timestamp: u64, now: u64) -> String {
let time_str = match duration { let time_str = match duration {
0..=2 => tr!( 0..=2 => tr!(
i18n,
"now", "now",
"Relative time for very recent events (less than 3 seconds)" "Relative time for very recent events (less than 3 seconds)"
), ),
3..=MAX_SECONDS => tr!("{count}s", "Relative time in seconds", count = duration), 3..=MAX_SECONDS => tr!(
i18n,
"{count}s",
"Relative time in seconds",
count = duration
),
ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!( ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!(
i18n,
"{count}m", "{count}m",
"Relative time in minutes", "Relative time in minutes",
count = duration / ONE_MINUTE_IN_SECONDS count = duration / ONE_MINUTE_IN_SECONDS
), ),
ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!( ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!(
i18n,
"{count}h", "{count}h",
"Relative time in hours", "Relative time in hours",
count = duration / ONE_HOUR_IN_SECONDS count = duration / ONE_HOUR_IN_SECONDS
), ),
ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!( ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!(
i18n,
"{count}d", "{count}d",
"Relative time in days", "Relative time in days",
count = duration / ONE_DAY_IN_SECONDS count = duration / ONE_DAY_IN_SECONDS
), ),
ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!( ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!(
i18n,
"{count}w", "{count}w",
"Relative time in weeks", "Relative time in weeks",
count = duration / ONE_WEEK_IN_SECONDS count = duration / ONE_WEEK_IN_SECONDS
), ),
ONE_MONTH_IN_SECONDS..=MAX_SECONDS_FOR_MONTHS => tr!( ONE_MONTH_IN_SECONDS..=MAX_SECONDS_FOR_MONTHS => tr!(
i18n,
"{count}mo", "{count}mo",
"Relative time in months", "Relative time in months",
count = duration / ONE_MONTH_IN_SECONDS count = duration / ONE_MONTH_IN_SECONDS
), ),
_ => tr!( _ => tr!(
i18n,
"{count}y", "{count}y",
"Relative time in years", "Relative time in years",
count = duration / ONE_YEAR_IN_SECONDS count = duration / ONE_YEAR_IN_SECONDS
@@ -65,19 +77,19 @@ fn time_ago_between(timestamp: u64, now: u64) -> String {
}; };
if timestamp > now { if timestamp > now {
format!("+{}", time_str) format!("+{time_str}")
} else { } else {
time_str time_str
} }
} }
pub fn time_ago_since(timestamp: u64) -> String { pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Time went backwards") .expect("Time went backwards")
.as_secs(); .as_secs();
time_ago_between(timestamp, now) time_ago_between(i18n, timestamp, now)
} }
#[cfg(test)] #[cfg(test)]
@@ -95,9 +107,10 @@ mod tests {
#[test] #[test]
fn test_now_condition() { fn test_now_condition() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut intl = Localization::default();
// Test 0 seconds ago // Test 0 seconds ago
let result = time_ago_between(now, now); let result = time_ago_between(&mut intl, now, now);
assert_eq!( assert_eq!(
result, "now", result, "now",
"Expected 'now' for 0 seconds, got: {}", "Expected 'now' for 0 seconds, got: {}",
@@ -105,7 +118,7 @@ mod tests {
); );
// Test 1 second ago // Test 1 second ago
let result = time_ago_between(now - 1, now); let result = time_ago_between(&mut intl, now - 1, now);
assert_eq!( assert_eq!(
result, "now", result, "now",
"Expected 'now' for 1 second, got: {}", "Expected 'now' for 1 second, got: {}",
@@ -113,7 +126,7 @@ mod tests {
); );
// Test 2 seconds ago // Test 2 seconds ago
let result = time_ago_between(now - 2, now); let result = time_ago_between(&mut intl, now - 2, now);
assert_eq!( assert_eq!(
result, "now", result, "now",
"Expected 'now' for 2 seconds, got: {}", "Expected 'now' for 2 seconds, got: {}",
@@ -124,13 +137,14 @@ mod tests {
#[test] #[test]
fn test_seconds_condition() { fn test_seconds_condition() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 3 seconds ago // Test 3 seconds ago
let result = time_ago_between(now - 3, now); let result = time_ago_between(&mut i18n, now - 3, now);
assert_eq!(result, "3s", "Expected '3s' for 3 seconds, got: {}", result); assert_eq!(result, "3s", "Expected '3s' for 3 seconds, got: {}", result);
// Test 30 seconds ago // Test 30 seconds ago
let result = time_ago_between(now - 30, now); let result = time_ago_between(&mut i18n, now - 30, now);
assert_eq!( assert_eq!(
result, "30s", result, "30s",
"Expected '30s' for 30 seconds, got: {}", "Expected '30s' for 30 seconds, got: {}",
@@ -138,7 +152,7 @@ mod tests {
); );
// Test 59 seconds ago (max for seconds) // Test 59 seconds ago (max for seconds)
let result = time_ago_between(now - 59, now); let result = time_ago_between(&mut i18n, now - 59, now);
assert_eq!( assert_eq!(
result, "59s", result, "59s",
"Expected '59s' for 59 seconds, got: {}", "Expected '59s' for 59 seconds, got: {}",
@@ -149,13 +163,14 @@ mod tests {
#[test] #[test]
fn test_minutes_condition() { fn test_minutes_condition() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 minute ago // Test 1 minute ago
let result = time_ago_between(now - ONE_MINUTE_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - ONE_MINUTE_IN_SECONDS, now);
assert_eq!(result, "1m", "Expected '1m' for 1 minute, got: {}", result); assert_eq!(result, "1m", "Expected '1m' for 1 minute, got: {}", result);
// Test 30 minutes ago // Test 30 minutes ago
let result = time_ago_between(now - 30 * ONE_MINUTE_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 30 * ONE_MINUTE_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "30m", result, "30m",
"Expected '30m' for 30 minutes, got: {}", "Expected '30m' for 30 minutes, got: {}",
@@ -163,7 +178,7 @@ mod tests {
); );
// Test 59 minutes ago (max for minutes) // Test 59 minutes ago (max for minutes)
let result = time_ago_between(now - 59 * ONE_MINUTE_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 59 * ONE_MINUTE_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "59m", result, "59m",
"Expected '59m' for 59 minutes, got: {}", "Expected '59m' for 59 minutes, got: {}",
@@ -174,13 +189,14 @@ mod tests {
#[test] #[test]
fn test_hours_condition() { fn test_hours_condition() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 hour ago // Test 1 hour ago
let result = time_ago_between(now - ONE_HOUR_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - ONE_HOUR_IN_SECONDS, now);
assert_eq!(result, "1h", "Expected '1h' for 1 hour, got: {}", result); assert_eq!(result, "1h", "Expected '1h' for 1 hour, got: {}", result);
// Test 12 hours ago // Test 12 hours ago
let result = time_ago_between(now - 12 * ONE_HOUR_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 12 * ONE_HOUR_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "12h", result, "12h",
"Expected '12h' for 12 hours, got: {}", "Expected '12h' for 12 hours, got: {}",
@@ -188,7 +204,7 @@ mod tests {
); );
// Test 23 hours ago (max for hours) // Test 23 hours ago (max for hours)
let result = time_ago_between(now - 23 * ONE_HOUR_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 23 * ONE_HOUR_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "23h", result, "23h",
"Expected '23h' for 23 hours, got: {}", "Expected '23h' for 23 hours, got: {}",
@@ -199,43 +215,46 @@ mod tests {
#[test] #[test]
fn test_days_condition() { fn test_days_condition() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 day ago // Test 1 day ago
let result = time_ago_between(now - ONE_DAY_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "1d", "Expected '1d' for 1 day, got: {}", result); assert_eq!(result, "1d", "Expected '1d' for 1 day, got: {}", result);
// Test 3 days ago // Test 3 days ago
let result = time_ago_between(now - 3 * ONE_DAY_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 3 * ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "3d", "Expected '3d' for 3 days, got: {}", result); assert_eq!(result, "3d", "Expected '3d' for 3 days, got: {}", result);
// Test 6 days ago (max for days, before weeks) // Test 6 days ago (max for days, before weeks)
let result = time_ago_between(now - 6 * ONE_DAY_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 6 * ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "6d", "Expected '6d' for 6 days, got: {}", result); assert_eq!(result, "6d", "Expected '6d' for 6 days, got: {}", result);
} }
#[test] #[test]
fn test_weeks_condition() { fn test_weeks_condition() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 week ago // Test 1 week ago
let result = time_ago_between(now - ONE_WEEK_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - ONE_WEEK_IN_SECONDS, now);
assert_eq!(result, "1w", "Expected '1w' for 1 week, got: {}", result); assert_eq!(result, "1w", "Expected '1w' for 1 week, got: {}", result);
// Test 4 weeks ago // Test 4 weeks ago
let result = time_ago_between(now - 4 * ONE_WEEK_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 4 * ONE_WEEK_IN_SECONDS, now);
assert_eq!(result, "4w", "Expected '4w' for 4 weeks, got: {}", result); assert_eq!(result, "4w", "Expected '4w' for 4 weeks, got: {}", result);
} }
#[test] #[test]
fn test_months_condition() { fn test_months_condition() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 month ago // Test 1 month ago
let result = time_ago_between(now - ONE_MONTH_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - ONE_MONTH_IN_SECONDS, now);
assert_eq!(result, "1mo", "Expected '1mo' for 1 month, got: {}", result); assert_eq!(result, "1mo", "Expected '1mo' for 1 month, got: {}", result);
// Test 11 months ago (max for months, before years) // Test 11 months ago (max for months, before years)
let result = time_ago_between(now - 11 * ONE_MONTH_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 11 * ONE_MONTH_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "11mo", result, "11mo",
"Expected '11mo' for 11 months, got: {}", "Expected '11mo' for 11 months, got: {}",
@@ -246,17 +265,18 @@ mod tests {
#[test] #[test]
fn test_years_condition() { fn test_years_condition() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 year ago // Test 1 year ago
let result = time_ago_between(now - ONE_YEAR_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - ONE_YEAR_IN_SECONDS, now);
assert_eq!(result, "1y", "Expected '1y' for 1 year, got: {}", result); assert_eq!(result, "1y", "Expected '1y' for 1 year, got: {}", result);
// Test 5 years ago // Test 5 years ago
let result = time_ago_between(now - 5 * ONE_YEAR_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 5 * ONE_YEAR_IN_SECONDS, now);
assert_eq!(result, "5y", "Expected '5y' for 5 years, got: {}", result); assert_eq!(result, "5y", "Expected '5y' for 5 years, got: {}", result);
// Test 10 years ago (reduced from 100 to avoid overflow) // Test 10 years ago (reduced from 100 to avoid overflow)
let result = time_ago_between(now - 10 * ONE_YEAR_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now - 10 * ONE_YEAR_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "10y", result, "10y",
"Expected '10y' for 10 years, got: {}", "Expected '10y' for 10 years, got: {}",
@@ -267,9 +287,10 @@ mod tests {
#[test] #[test]
fn test_future_timestamps() { fn test_future_timestamps() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test 1 minute in the future // Test 1 minute in the future
let result = time_ago_between(now + ONE_MINUTE_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now + ONE_MINUTE_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "+1m", result, "+1m",
"Expected '+1m' for 1 minute in future, got: {}", "Expected '+1m' for 1 minute in future, got: {}",
@@ -277,7 +298,7 @@ mod tests {
); );
// Test 1 hour in the future // Test 1 hour in the future
let result = time_ago_between(now + ONE_HOUR_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now + ONE_HOUR_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "+1h", result, "+1h",
"Expected '+1h' for 1 hour in future, got: {}", "Expected '+1h' for 1 hour in future, got: {}",
@@ -285,7 +306,7 @@ mod tests {
); );
// Test 1 day in the future // Test 1 day in the future
let result = time_ago_between(now + ONE_DAY_IN_SECONDS, now); let result = time_ago_between(&mut i18n, now + ONE_DAY_IN_SECONDS, now);
assert_eq!( assert_eq!(
result, "+1d", result, "+1d",
"Expected '+1d' for 1 day in future, got: {}", "Expected '+1d' for 1 day in future, got: {}",
@@ -296,9 +317,10 @@ mod tests {
#[test] #[test]
fn test_boundary_conditions() { fn test_boundary_conditions() {
let now = get_current_timestamp(); let now = get_current_timestamp();
let mut i18n = Localization::default();
// Test boundary between seconds and minutes // Test boundary between seconds and minutes
let result = time_ago_between(now - 60, now); let result = time_ago_between(&mut i18n, now - 60, now);
assert_eq!( assert_eq!(
result, "1m", result, "1m",
"Expected '1m' for exactly 60 seconds, got: {}", "Expected '1m' for exactly 60 seconds, got: {}",
@@ -306,7 +328,7 @@ mod tests {
); );
// Test boundary between minutes and hours // Test boundary between minutes and hours
let result = time_ago_between(now - 3600, now); let result = time_ago_between(&mut i18n, now - 3600, now);
assert_eq!( assert_eq!(
result, "1h", result, "1h",
"Expected '1h' for exactly 3600 seconds, got: {}", "Expected '1h' for exactly 3600 seconds, got: {}",
@@ -314,7 +336,7 @@ mod tests {
); );
// Test boundary between hours and days // Test boundary between hours and days
let result = time_ago_between(now - 86400, now); let result = time_ago_between(&mut i18n, now - 86400, now);
assert_eq!( assert_eq!(
result, "1d", result, "1d",
"Expected '1d' for exactly 86400 seconds, got: {}", "Expected '1d' for exactly 86400 seconds, got: {}",

View File

@@ -5,7 +5,9 @@ use crate::app::NotedeckApp;
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget}; use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction}; use nostrdb::{ProfileRecord, Transaction};
use notedeck::{tr, App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType}; use notedeck::{
tr, App, AppAction, AppContext, Localization, NotedeckTextStyle, UserAccount, WalletType,
};
use notedeck_columns::{ use notedeck_columns::{
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus, column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
}; };
@@ -59,14 +61,17 @@ pub enum ChromePanelAction {
} }
impl ChromePanelAction { impl ChromePanelAction {
fn columns_switch(ctx: &AppContext, chrome: &mut Chrome, kind: &TimelineKind) { fn columns_switch(ctx: &mut AppContext, chrome: &mut Chrome, kind: &TimelineKind) {
chrome.switch_to_columns(); chrome.switch_to_columns();
let Some(columns_app) = chrome.get_columns_app() else { let Some(columns_app) = chrome.get_columns_app() else {
return; return;
}; };
if let Some(active_columns) = columns_app.decks_cache.active_columns_mut(ctx.accounts) { if let Some(active_columns) = columns_app
.decks_cache
.active_columns_mut(ctx.i18n, ctx.accounts)
{
match active_columns.select_by_kind(kind) { match active_columns.select_by_kind(kind) {
SelectionResult::NewSelection(_index) => { SelectionResult::NewSelection(_index) => {
// great! no need to go to top yet // great! no need to go to top yet
@@ -85,13 +90,14 @@ impl ChromePanelAction {
} }
} }
fn columns_navigate(ctx: &AppContext, chrome: &mut Chrome, route: notedeck_columns::Route) { fn columns_navigate(ctx: &mut AppContext, chrome: &mut Chrome, route: notedeck_columns::Route) {
chrome.switch_to_columns(); chrome.switch_to_columns();
if let Some(c) = chrome if let Some(c) = chrome.get_columns_app().and_then(|columns| {
.get_columns_app() columns
.and_then(|columns| columns.decks_cache.selected_column_mut(ctx.accounts)) .decks_cache
{ .selected_column_mut(ctx.i18n, ctx.accounts)
}) {
if c.router().routes().iter().any(|r| r == &route) { if c.router().routes().iter().any(|r| r == &route) {
// return if we are already routing to accounts // return if we are already routing to accounts
c.router_mut().go_back(); c.router_mut().go_back();
@@ -102,7 +108,7 @@ impl ChromePanelAction {
}; };
} }
fn process(&self, ctx: &AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) { fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) {
match self { match self {
Self::SaveTheme(theme) => { Self::SaveTheme(theme) => {
ui.ctx().options_mut(|o| { ui.ctx().options_mut(|o| {
@@ -244,7 +250,7 @@ impl Chrome {
.vertical(|mut vstrip| { .vertical(|mut vstrip| {
vstrip.cell(|ui| { vstrip.cell(|ui| {
_ = ui.vertical_centered(|ui| { _ = ui.vertical_centered(|ui| {
self.topdown_sidebar(ui); self.topdown_sidebar(ui, app_ctx.i18n);
}) })
}); });
vstrip.cell(|ui| { vstrip.cell(|ui| {
@@ -401,7 +407,7 @@ impl Chrome {
} }
} }
fn topdown_sidebar(&mut self, ui: &mut egui::Ui) { fn topdown_sidebar(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
// macos needs a bit of space to make room for window // macos needs a bit of space to make room for window
// minimize/close buttons // minimize/close buttons
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
@@ -417,7 +423,7 @@ impl Chrome {
} }
ui.add_space(4.0); ui.add_space(4.0);
ui.add(milestone_name()); ui.add(milestone_name(i18n));
ui.add_space(16.0); ui.add_space(16.0);
//let dark_mode = ui.ctx().style().visuals.dark_mode; //let dark_mode = ui.ctx().style().visuals.dark_mode;
{ {
@@ -451,7 +457,7 @@ impl notedeck::App for Chrome {
} }
} }
fn milestone_name() -> impl Widget { fn milestone_name<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
|ui: &mut egui::Ui| -> egui::Response { |ui: &mut egui::Ui| -> egui::Response {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let font = egui::FontId::new( let font = egui::FontId::new(
@@ -460,13 +466,14 @@ fn milestone_name() -> impl Widget {
); );
ui.add( ui.add(
Label::new( Label::new(
RichText::new(tr!("BETA", "Beta version label")) RichText::new(tr!(i18n, "BETA", "Beta version label"))
.color(ui.style().visuals.noninteractive().fg_stroke.color) .color(ui.style().visuals.noninteractive().fg_stroke.color)
.font(font), .font(font),
) )
.selectable(false), .selectable(false),
) )
.on_hover_text(tr!( .on_hover_text(tr!(
i18n,
"Notedeck is a beta product. Expect bugs and contact us when you run into issues.", "Notedeck is a beta product. Expect bugs and contact us when you run into issues.",
"Beta product warning message" "Beta product warning message"
)) ))
@@ -657,7 +664,7 @@ fn chrome_handle_app_action(
let cols = columns let cols = columns
.decks_cache .decks_cache
.active_columns_mut(ctx.accounts) .active_columns_mut(ctx.i18n, ctx.accounts)
.unwrap(); .unwrap();
let m_action = notedeck_columns::actionbar::execute_and_process_note_action( let m_action = notedeck_columns::actionbar::execute_and_process_note_action(
note_action, note_action,
@@ -721,6 +728,7 @@ fn bottomup_sidebar(
.add(Button::new("").frame(false)) .add(Button::new("").frame(false))
.on_hover_cursor(egui::CursorIcon::PointingHand) .on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(tr!( .on_hover_text(tr!(
ctx.i18n,
"Switch to light mode", "Switch to light mode",
"Hover text for light mode toggle button" "Hover text for light mode toggle button"
)); ));
@@ -735,6 +743,7 @@ fn bottomup_sidebar(
.add(Button::new("🌙").frame(false)) .add(Button::new("🌙").frame(false))
.on_hover_cursor(egui::CursorIcon::PointingHand) .on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(tr!( .on_hover_text(tr!(
ctx.i18n,
"Switch to dark mode", "Switch to dark mode",
"Hover text for dark mode toggle button" "Hover text for dark mode toggle button"
)); ));

View File

@@ -1,7 +1,7 @@
use enostr::{FullKeypair, Pubkey}; use enostr::{FullKeypair, Pubkey};
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::{Accounts, AppContext, SingleUnkIdAction, UnknownIds}; use notedeck::{Accounts, AppContext, Localization, SingleUnkIdAction, UnknownIds};
use crate::app::get_active_columns_mut; use crate::app::get_active_columns_mut;
use crate::decks::DecksCache; use crate::decks::DecksCache;
@@ -72,23 +72,34 @@ pub fn render_accounts_route(
route: AccountsRoute, route: AccountsRoute,
) -> AddAccountAction { ) -> AddAccountAction {
let resp = match route { let resp = match route {
AccountsRoute::Accounts => { AccountsRoute::Accounts => AccountsView::new(
AccountsView::new(app_ctx.ndb, app_ctx.accounts, app_ctx.img_cache) app_ctx.ndb,
app_ctx.accounts,
app_ctx.img_cache,
app_ctx.i18n,
)
.ui(ui)
.inner
.map(AccountsRouteResponse::Accounts),
AccountsRoute::AddAccount => {
AccountLoginView::new(login_state, app_ctx.clipboard, app_ctx.i18n)
.ui(ui) .ui(ui)
.inner .inner
.map(AccountsRouteResponse::Accounts) .map(AccountsRouteResponse::AddAccount)
} }
AccountsRoute::AddAccount => AccountLoginView::new(login_state, app_ctx.clipboard)
.ui(ui)
.inner
.map(AccountsRouteResponse::AddAccount),
}; };
if let Some(resp) = resp { if let Some(resp) = resp {
match resp { match resp {
AccountsRouteResponse::Accounts(response) => { AccountsRouteResponse::Accounts(response) => {
let action = process_accounts_view_response(app_ctx.accounts, decks, col, response); let action = process_accounts_view_response(
app_ctx.i18n,
app_ctx.accounts,
decks,
col,
response,
);
AddAccountAction { AddAccountAction {
accounts_action: action, accounts_action: action,
unk_id_action: SingleUnkIdAction::no_action(), unk_id_action: SingleUnkIdAction::no_action(),
@@ -98,7 +109,7 @@ pub fn render_accounts_route(
let action = let action =
process_login_view_response(app_ctx, timeline_cache, decks, col, response); process_login_view_response(app_ctx, timeline_cache, decks, col, response);
*login_state = Default::default(); *login_state = Default::default();
let router = get_active_columns_mut(app_ctx.accounts, decks) let router = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, decks)
.column_mut(col) .column_mut(col)
.router_mut(); .router_mut();
router.go_back(); router.go_back();
@@ -114,12 +125,13 @@ pub fn render_accounts_route(
} }
pub fn process_accounts_view_response( pub fn process_accounts_view_response(
i18n: &mut Localization,
accounts: &mut Accounts, accounts: &mut Accounts,
decks: &mut DecksCache, decks: &mut DecksCache,
col: usize, col: usize,
response: AccountsViewResponse, response: AccountsViewResponse,
) -> Option<AccountsAction> { ) -> Option<AccountsAction> {
let router = get_active_columns_mut(accounts, decks) let router = get_active_columns_mut(i18n, accounts, decks)
.column_mut(col) .column_mut(col)
.router_mut(); .router_mut();
let mut action = None; let mut action = None;

View File

@@ -284,7 +284,7 @@ impl NewNotes {
let timeline = if let Some(profile) = timeline_cache.get_mut(&self.id) { let timeline = if let Some(profile) = timeline_cache.get_mut(&self.id) {
profile profile
} else { } else {
error!("NewNotes: could not get timeline for key {}", self.id); error!("NewNotes: could not get timeline for key {:?}", self.id);
return; return;
}; };

View File

@@ -20,7 +20,7 @@ use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPo
use nostrdb::Transaction; use nostrdb::Transaction;
use notedeck::{ use notedeck::{
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
UnknownIds, Localization, UnknownIds,
}; };
use notedeck_ui::{jobs::JobsCache, NoteOptions}; use notedeck_ui::{jobs::JobsCache, NoteOptions};
use std::collections::{BTreeSet, HashMap}; use std::collections::{BTreeSet, HashMap};
@@ -92,7 +92,8 @@ fn try_process_event(
app_ctx: &mut AppContext<'_>, app_ctx: &mut AppContext<'_>,
ctx: &egui::Context, ctx: &egui::Context,
) -> Result<()> { ) -> Result<()> {
let current_columns = get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache); let current_columns =
get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache);
ctx.input(|i| handle_key_events(i, current_columns)); ctx.input(|i| handle_key_events(i, current_columns));
let ctx2 = ctx.clone(); let ctx2 = ctx.clone();
@@ -187,7 +188,9 @@ fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Con
app_ctx.img_cache.urls.cache.handle_io(); app_ctx.img_cache.urls.cache.handle_io();
if damus.columns(app_ctx.accounts).columns().is_empty() { if damus.columns(app_ctx.accounts).columns().is_empty() {
damus.columns_mut(app_ctx.accounts).new_column_picker(); damus
.columns_mut(app_ctx.i18n, app_ctx.accounts)
.new_column_picker();
} }
match damus.state { match damus.state {
@@ -262,7 +265,7 @@ fn handle_eose(
tl tl
} else { } else {
error!( error!(
"timeline uid:{} not found for FetchingContactList", "timeline uid:{:?} not found for FetchingContactList",
timeline_uid timeline_uid
); );
return Ok(()); return Ok(());
@@ -427,9 +430,9 @@ impl Damus {
} }
} }
columns_to_decks_cache(columns, account) columns_to_decks_cache(ctx.i18n, columns, account)
} else if let Some(decks_cache) = } else if let Some(decks_cache) =
crate::storage::load_decks_cache(ctx.path, ctx.ndb, &mut timeline_cache) crate::storage::load_decks_cache(ctx.path, ctx.ndb, &mut timeline_cache, ctx.i18n)
{ {
info!( info!(
"DecksCache: loading from disk {}", "DecksCache: loading from disk {}",
@@ -495,8 +498,8 @@ impl Damus {
self.options.insert(AppOptions::ScrollToTop) self.options.insert(AppOptions::ScrollToTop)
} }
pub fn columns_mut(&mut self, accounts: &Accounts) -> &mut Columns { pub fn columns_mut(&mut self, i18n: &mut Localization, accounts: &Accounts) -> &mut Columns {
get_active_columns_mut(accounts, &mut self.decks_cache) get_active_columns_mut(i18n, accounts, &mut self.decks_cache)
} }
pub fn columns(&self, accounts: &Accounts) -> &Columns { pub fn columns(&self, accounts: &Accounts) -> &Columns {
@@ -512,7 +515,8 @@ impl Damus {
} }
pub fn mock<P: AsRef<Path>>(data_path: P) -> Self { pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
let decks_cache = DecksCache::default(); let mut i18n = Localization::default();
let decks_cache = DecksCache::default_decks_cache(&mut i18n);
let path = DataPath::new(&data_path); let path = DataPath::new(&data_path);
let imgcache_dir = path.path(DataPathType::Cache); let imgcache_dir = path.path(DataPathType::Cache);
@@ -567,7 +571,7 @@ fn render_damus_mobile(
let rect = ui.available_rect_before_wrap(); let rect = ui.available_rect_before_wrap();
let mut app_action: Option<AppAction> = None; let mut app_action: Option<AppAction> = None;
let active_col = app.columns_mut(app_ctx.accounts).selected as usize; let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
if !app.columns(app_ctx.accounts).columns().is_empty() { if !app.columns(app_ctx.accounts).columns().is_empty() {
let r = nav::render_nav( let r = nav::render_nav(
active_col, active_col,
@@ -623,6 +627,7 @@ fn hovering_post_button(
&mut app.decks_cache, &mut app.decks_cache,
app_ctx.accounts, app_ctx.accounts,
SidePanelAction::ComposeNote, SidePanelAction::ComposeNote,
app_ctx.i18n,
); );
} }
} }
@@ -715,9 +720,12 @@ fn timelines_view(
.horizontal(|mut strip| { .horizontal(|mut strip| {
strip.cell(|ui| { strip.cell(|ui| {
let rect = ui.available_rect_before_wrap(); let rect = ui.available_rect_before_wrap();
let side_panel = let side_panel = DesktopSidePanel::new(
DesktopSidePanel::new(ctx.accounts.get_selected_account(), &app.decks_cache) ctx.accounts.get_selected_account(),
.show(ui); &app.decks_cache,
ctx.i18n,
)
.show(ui);
if let Some(side_panel) = side_panel { if let Some(side_panel) = side_panel {
if side_panel.response.clicked() || side_panel.response.secondary_clicked() { if side_panel.response.clicked() || side_panel.response.secondary_clicked() {
@@ -725,6 +733,7 @@ fn timelines_view(
&mut app.decks_cache, &mut app.decks_cache,
ctx.accounts, ctx.accounts,
side_panel.action, side_panel.action,
ctx.i18n,
) { ) {
side_panel_action = Some(action); side_panel_action = Some(action);
} }
@@ -833,27 +842,32 @@ pub fn get_decks<'a>(accounts: &Accounts, decks_cache: &'a DecksCache) -> &'a De
} }
pub fn get_active_columns_mut<'a>( pub fn get_active_columns_mut<'a>(
i18n: &mut Localization,
accounts: &Accounts, accounts: &Accounts,
decks_cache: &'a mut DecksCache, decks_cache: &'a mut DecksCache,
) -> &'a mut Columns { ) -> &'a mut Columns {
get_decks_mut(accounts, decks_cache) get_decks_mut(i18n, accounts, decks_cache)
.active_mut() .active_mut()
.columns_mut() .columns_mut()
} }
pub fn get_decks_mut<'a>(accounts: &Accounts, decks_cache: &'a mut DecksCache) -> &'a mut Decks { pub fn get_decks_mut<'a>(
decks_cache.decks_mut(accounts.selected_account_pubkey()) i18n: &mut Localization,
accounts: &Accounts,
decks_cache: &'a mut DecksCache,
) -> &'a mut Decks {
decks_cache.decks_mut(i18n, accounts.selected_account_pubkey())
} }
fn columns_to_decks_cache(cols: Columns, key: &[u8; 32]) -> DecksCache { fn columns_to_decks_cache(i18n: &mut Localization, cols: Columns, key: &[u8; 32]) -> DecksCache {
let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default(); let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default();
let decks = Decks::new(crate::decks::Deck::new_with_columns( let decks = Decks::new(crate::decks::Deck::new_with_columns(
crate::decks::Deck::default().icon, crate::decks::Deck::default_icon(),
tr!("My Deck", "Title for the user's deck"), tr!(i18n, "My Deck", "Title for the user's deck"),
cols, cols,
)); ));
let account = Pubkey::new(*key); let account = Pubkey::new(*key);
account_to_decks.insert(account, decks); account_to_decks.insert(account, decks);
DecksCache::new(account_to_decks) DecksCache::new(account_to_decks, i18n)
} }

View File

@@ -2,7 +2,7 @@ use std::collections::{hash_map::ValuesMut, HashMap};
use enostr::{Pubkey, RelayPool}; use enostr::{Pubkey, RelayPool};
use nostrdb::Transaction; use nostrdb::Transaction;
use notedeck::{tr, AppContext, FALLBACK_PUBKEY}; use notedeck::{tr, AppContext, Localization, FALLBACK_PUBKEY};
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
@@ -21,18 +21,20 @@ pub struct DecksCache {
fallback_pubkey: Pubkey, fallback_pubkey: Pubkey,
} }
impl Default for DecksCache {
fn default() -> Self {
let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default();
account_to_decks.insert(FALLBACK_PUBKEY(), Decks::default());
DecksCache::new(account_to_decks)
}
}
impl DecksCache { impl DecksCache {
pub fn default_decks_cache(i18n: &mut Localization) -> Self {
let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default();
account_to_decks.insert(FALLBACK_PUBKEY(), Decks::default_decks(i18n));
DecksCache::new(account_to_decks, i18n)
}
/// Gets the first column in the currently active user's active deck /// Gets the first column in the currently active user's active deck
pub fn selected_column_mut(&mut self, accounts: &notedeck::Accounts) -> Option<&mut Column> { pub fn selected_column_mut(
self.active_columns_mut(accounts) &mut self,
i18n: &mut Localization,
accounts: &notedeck::Accounts,
) -> Option<&mut Column> {
self.active_columns_mut(i18n, accounts)
.and_then(|ad| ad.selected_mut()) .and_then(|ad| ad.selected_mut())
} }
@@ -45,10 +47,14 @@ impl DecksCache {
} }
/// Gets a mutable reference to the active columns /// Gets a mutable reference to the active columns
pub fn active_columns_mut(&mut self, accounts: &notedeck::Accounts) -> Option<&mut Columns> { pub fn active_columns_mut(
&mut self,
i18n: &mut Localization,
accounts: &notedeck::Accounts,
) -> Option<&mut Columns> {
let account = accounts.get_selected_account(); let account = accounts.get_selected_account();
self.decks_mut(&account.key.pubkey) self.decks_mut(i18n, &account.key.pubkey)
.active_deck_mut() .active_deck_mut()
.map(|ad| ad.columns_mut()) .map(|ad| ad.columns_mut())
} }
@@ -62,9 +68,11 @@ impl DecksCache {
.map(|ad| ad.columns()) .map(|ad| ad.columns())
} }
pub fn new(mut account_to_decks: HashMap<Pubkey, Decks>) -> Self { pub fn new(mut account_to_decks: HashMap<Pubkey, Decks>, i18n: &mut Localization) -> Self {
let fallback_pubkey = FALLBACK_PUBKEY(); let fallback_pubkey = FALLBACK_PUBKEY();
account_to_decks.entry(fallback_pubkey).or_default(); account_to_decks
.entry(fallback_pubkey)
.or_insert_with(|| Decks::default_decks(i18n));
Self { Self {
account_to_decks, account_to_decks,
@@ -79,7 +87,7 @@ impl DecksCache {
fallback_pubkey, fallback_pubkey,
demo_decks(fallback_pubkey, timeline_cache, ctx), demo_decks(fallback_pubkey, timeline_cache, ctx),
); );
DecksCache::new(account_to_decks) DecksCache::new(account_to_decks, ctx.i18n)
} }
pub fn decks(&self, key: &Pubkey) -> &Decks { pub fn decks(&self, key: &Pubkey) -> &Decks {
@@ -88,8 +96,10 @@ impl DecksCache {
.unwrap_or_else(|| self.fallback()) .unwrap_or_else(|| self.fallback())
} }
pub fn decks_mut(&mut self, key: &Pubkey) -> &mut Decks { pub fn decks_mut(&mut self, i18n: &mut Localization, key: &Pubkey) -> &mut Decks {
self.account_to_decks.entry(*key).or_default() self.account_to_decks
.entry(*key)
.or_insert_with(|| Decks::default_decks(i18n))
} }
pub fn fallback(&self) -> &Decks { pub fn fallback(&self) -> &Decks {
@@ -110,7 +120,7 @@ impl DecksCache {
timeline_cache: &mut TimelineCache, timeline_cache: &mut TimelineCache,
pubkey: Pubkey, pubkey: Pubkey,
) { ) {
let mut decks = Decks::default(); let mut decks = Decks::default_decks(ctx.i18n);
// add home and notifications for new accounts // add home and notifications for new accounts
add_demo_columns( add_demo_columns(
@@ -157,6 +167,7 @@ impl DecksCache {
pub fn remove( pub fn remove(
&mut self, &mut self,
i18n: &mut Localization,
key: &Pubkey, key: &Pubkey,
timeline_cache: &mut TimelineCache, timeline_cache: &mut TimelineCache,
ndb: &mut nostrdb::Ndb, ndb: &mut nostrdb::Ndb,
@@ -171,7 +182,7 @@ impl DecksCache {
if !self.account_to_decks.contains_key(&self.fallback_pubkey) { if !self.account_to_decks.contains_key(&self.fallback_pubkey) {
self.account_to_decks self.account_to_decks
.insert(self.fallback_pubkey, Decks::default()); .insert(self.fallback_pubkey, Decks::default_decks(i18n));
} }
} }
@@ -194,13 +205,11 @@ pub struct Decks {
decks: Vec<Deck>, decks: Vec<Deck>,
} }
impl Default for Decks {
fn default() -> Self {
Decks::new(Deck::default())
}
}
impl Decks { impl Decks {
pub fn default_decks(i18n: &mut Localization) -> Self {
Decks::new(Deck::default_deck(i18n))
}
pub fn new(deck: Deck) -> Self { pub fn new(deck: Deck) -> Self {
let decks = vec![deck]; let decks = vec![deck];
@@ -381,24 +390,22 @@ pub struct Deck {
columns: Columns, columns: Columns,
} }
impl Default for Deck {
fn default() -> Self {
let columns = Columns::default();
Self {
columns,
icon: Deck::default_icon(),
name: Deck::default_name().to_string(),
}
}
}
impl Deck { impl Deck {
pub fn default_icon() -> char { pub fn default_icon() -> char {
'🇩' '🇩'
} }
pub fn default_name() -> String { fn default_deck(i18n: &mut Localization) -> Self {
tr!("Default Deck", "Name of the default deck feed") let columns = Columns::default();
Self {
columns,
icon: Deck::default_icon(),
name: Deck::default_name(i18n).to_string(),
}
}
pub fn default_name(i18n: &mut Localization) -> String {
tr!(i18n, "Default Deck", "Name of the default deck feed")
} }
pub fn new(icon: char, name: String) -> Self { pub fn new(icon: char, name: String) -> Self {
@@ -482,7 +489,7 @@ pub fn demo_decks(
Deck { Deck {
icon: Deck::default_icon(), icon: Deck::default_icon(),
name: Deck::default_name().to_string(), name: Deck::default_name(ctx.i18n).to_string(),
columns, columns,
} }
}; };

View File

@@ -2,7 +2,7 @@ use crate::key_parsing::perform_key_retrieval;
use crate::key_parsing::AcquireKeyError; use crate::key_parsing::AcquireKeyError;
use egui::{TextBuffer, TextEdit}; use egui::{TextBuffer, TextEdit};
use enostr::Keypair; use enostr::Keypair;
use notedeck::tr; use notedeck::{tr, Localization};
use poll_promise::Promise; use poll_promise::Promise;
/// The state data for acquiring a nostr key /// The state data for acquiring a nostr key
@@ -24,7 +24,7 @@ impl<'a> AcquireKeyState {
/// Get the textedit for the UI without exposing the key variable /// Get the textedit for the UI without exposing the key variable
pub fn get_acquire_textedit( pub fn get_acquire_textedit(
&'a mut self, &'a mut self,
textedit_closure: fn(&'a mut dyn TextBuffer) -> TextEdit<'a>, textedit_closure: impl FnOnce(&'a mut dyn TextBuffer) -> TextEdit<'a>,
) -> TextEdit<'a> { ) -> TextEdit<'a> {
textedit_closure(&mut self.desired_key) textedit_closure(&mut self.desired_key)
} }
@@ -106,7 +106,7 @@ impl<'a> AcquireKeyState {
self.should_create_new self.should_create_new
} }
pub fn loading_and_error_ui(&mut self, ui: &mut egui::Ui) { pub fn loading_and_error_ui(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
ui.add_space(8.0); ui.add_space(8.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@@ -116,7 +116,7 @@ impl<'a> AcquireKeyState {
}); });
if let Some(err) = self.check_for_error() { if let Some(err) = self.check_for_error() {
show_error(ui, err); show_error(ui, i18n, err);
} }
ui.add_space(8.0); ui.add_space(8.0);
@@ -131,12 +131,16 @@ impl<'a> AcquireKeyState {
} }
} }
fn show_error(ui: &mut egui::Ui, err: &AcquireKeyError) { fn show_error(ui: &mut egui::Ui, i18n: &mut Localization, err: &AcquireKeyError) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let error_label = match err { let error_label = match err {
AcquireKeyError::InvalidKey => egui::Label::new( AcquireKeyError::InvalidKey => egui::Label::new(
egui::RichText::new(tr!("Invalid key.", "Error message for invalid key input")) egui::RichText::new(tr!(
.color(ui.visuals().error_fg_color), i18n,
"Invalid key.",
"Error message for invalid key input"
))
.color(ui.visuals().error_fg_color),
), ),
AcquireKeyError::Nip05Failed(e) => { AcquireKeyError::Nip05Failed(e) => {
egui::Label::new(egui::RichText::new(e).color(ui.visuals().error_fg_color)) egui::Label::new(egui::RichText::new(e).color(ui.visuals().error_fg_color))

View File

@@ -89,7 +89,7 @@ impl SwitchingAction {
ui_ctx, ui_ctx,
); );
// pop nav after switch // pop nav after switch
get_active_columns_mut(ctx.accounts, decks_cache) get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
.column_mut(switch_action.source_column) .column_mut(switch_action.source_column)
.router_mut() .router_mut()
.go_back(); .go_back();
@@ -102,13 +102,13 @@ impl SwitchingAction {
break 's; break 's;
} }
decks_cache.remove(to_remove, timeline_cache, ctx.ndb, ctx.pool); decks_cache.remove(ctx.i18n, to_remove, timeline_cache, ctx.ndb, ctx.pool);
} }
}, },
SwitchingAction::Columns(columns_action) => match *columns_action { SwitchingAction::Columns(columns_action) => match *columns_action {
ColumnsAction::Remove(index) => { ColumnsAction::Remove(index) => {
let kinds_to_pop = let kinds_to_pop = get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
get_active_columns_mut(ctx.accounts, decks_cache).delete_column(index); .delete_column(index);
for kind in &kinds_to_pop { for kind in &kinds_to_pop {
if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) { if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
error!("error popping timeline: {err}"); error!("error popping timeline: {err}");
@@ -117,15 +117,15 @@ impl SwitchingAction {
} }
ColumnsAction::Switch(from, to) => { ColumnsAction::Switch(from, to) => {
get_active_columns_mut(ctx.accounts, decks_cache).move_col(from, to); get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache).move_col(from, to);
} }
}, },
SwitchingAction::Decks(decks_action) => match *decks_action { SwitchingAction::Decks(decks_action) => match *decks_action {
DecksAction::Switch(index) => { DecksAction::Switch(index) => {
get_decks_mut(ctx.accounts, decks_cache).set_active(index) get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).set_active(index)
} }
DecksAction::Removing(index) => { DecksAction::Removing(index) => {
get_decks_mut(ctx.accounts, decks_cache).remove_deck( get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).remove_deck(
index, index,
timeline_cache, timeline_cache,
ctx.ndb, ctx.ndb,
@@ -206,10 +206,10 @@ fn process_popup_resp(
} }
if let Some(NavAction::Returned(_)) = action.action { if let Some(NavAction::Returned(_)) = action.action {
let column = app.columns_mut(ctx.accounts).column_mut(col); let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
column.sheet_router.clear(); column.sheet_router.clear();
} else if let Some(NavAction::Navigating) = action.action { } else if let Some(NavAction::Navigating) = action.action {
let column = app.columns_mut(ctx.accounts).column_mut(col); let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
column.sheet_router.navigating = false; column.sheet_router.navigating = false;
} }
@@ -235,7 +235,7 @@ fn process_nav_resp(
match action { match action {
NavAction::Returned(return_type) => { NavAction::Returned(return_type) => {
let r = app let r = app
.columns_mut(ctx.accounts) .columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.pop(); .pop();
@@ -260,7 +260,10 @@ fn process_nav_resp(
} }
NavAction::Navigated => { NavAction::Navigated => {
let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut(); let cur_router = app
.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut();
cur_router.navigating = false; cur_router.navigating = false;
if cur_router.is_replacing() { if cur_router.is_replacing() {
cur_router.remove_previous_routes(); cur_router.remove_previous_routes();
@@ -414,7 +417,7 @@ fn process_render_nav_action(
RenderNavAction::Back => Some(RouterAction::GoBack), RenderNavAction::Back => Some(RouterAction::GoBack),
RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked), RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked),
RenderNavAction::RemoveColumn => { RenderNavAction::RemoveColumn => {
let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col); let kinds_to_pop = app.columns_mut(ctx.i18n, ctx.accounts).delete_column(col);
for kind in &kinds_to_pop { for kind in &kinds_to_pop {
if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) { if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
@@ -439,7 +442,7 @@ fn process_render_nav_action(
crate::actionbar::execute_and_process_note_action( crate::actionbar::execute_and_process_note_action(
note_action, note_action,
ctx.ndb, ctx.ndb,
get_active_columns_mut(ctx.accounts, &mut app.decks_cache), get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
col, col,
&mut app.timeline_cache, &mut app.timeline_cache,
&mut app.threads, &mut app.threads,
@@ -480,7 +483,8 @@ fn process_render_nav_action(
}; };
if let Some(action) = router_action { if let Some(action) = router_action {
let cols = get_active_columns_mut(ctx.accounts, &mut app.decks_cache).column_mut(col); let cols =
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache).column_mut(col);
let router = &mut cols.router; let router = &mut cols.router;
let sheet_router = &mut cols.sheet_router; let sheet_router = &mut cols.sheet_router;
@@ -511,6 +515,7 @@ fn render_nav_body(
unknown_ids: ctx.unknown_ids, unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard, clipboard: ctx.clipboard,
current_account_has_wallet, current_account_has_wallet,
i18n: ctx.i18n,
}; };
match top { match top {
Route::Timeline(kind) => { Route::Timeline(kind) => {
@@ -565,7 +570,7 @@ fn render_nav_body(
.accounts_action .accounts_action
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f))) .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
} }
Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map) Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
.ui(ui) .ui(ui)
.map(RenderNavAction::RelayAction), .map(RenderNavAction::RelayAction),
Route::Reply(id) => { Route::Reply(id) => {
@@ -573,6 +578,7 @@ fn render_nav_body(
txn txn
} else { } else {
ui.label(tr!( ui.label(tr!(
note_context.i18n,
"Reply to unknown note", "Reply to unknown note",
"Error message when reply note cannot be found" "Error message when reply note cannot be found"
)); ));
@@ -583,6 +589,7 @@ fn render_nav_body(
note note
} else { } else {
ui.label(tr!( ui.label(tr!(
note_context.i18n,
"Reply to unknown note", "Reply to unknown note",
"Error message when reply note cannot be found" "Error message when reply note cannot be found"
)); ));
@@ -623,6 +630,7 @@ fn render_nav_body(
note note
} else { } else {
ui.label(tr!( ui.label(tr!(
note_context.i18n,
"Quote of unknown note", "Quote of unknown note",
"Error message when quote note cannot be found" "Error message when quote note cannot be found"
)); ));
@@ -676,15 +684,16 @@ fn render_nav_body(
None None
} }
Route::Support => { Route::Support => {
SupportView::new(&mut app.support).show(ui); SupportView::new(&mut app.support, ctx.i18n).show(ui);
None None
} }
Route::Search => { Route::Search => {
let id = ui.id().with(("search", depth, col)); let id = ui.id().with(("search", depth, col));
let navigating = get_active_columns_mut(ctx.accounts, &mut app.decks_cache) let navigating =
.column(col) get_active_columns_mut(note_context.i18n, ctx.accounts, &mut app.decks_cache)
.router() .column(col)
.navigating; .router()
.navigating;
let search_buffer = app.view_state.searches.entry(id).or_default(); let search_buffer = app.view_state.searches.entry(id).or_default();
let txn = Transaction::new(ctx.ndb).expect("txn"); let txn = Transaction::new(ctx.ndb).expect("txn");
@@ -711,13 +720,13 @@ fn render_nav_body(
let id = ui.id().with("new-deck"); let id = ui.id().with("new-deck");
let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default(); let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
let mut resp = None; let mut resp = None;
if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) { if let Some(config_resp) = ConfigureDeckView::new(new_deck_state, ctx.i18n).ui(ui) {
let cur_acc = ctx.accounts.selected_account_pubkey(); let cur_acc = ctx.accounts.selected_account_pubkey();
app.decks_cache app.decks_cache
.add_deck(*cur_acc, Deck::new(config_resp.icon, config_resp.name)); .add_deck(*cur_acc, Deck::new(config_resp.icon, config_resp.name));
// set new deck as active // set new deck as active
let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache) let cur_index = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.decks() .decks()
.len() .len()
- 1; - 1;
@@ -726,7 +735,7 @@ fn render_nav_body(
))); )));
new_deck_state.clear(); new_deck_state.clear();
get_active_columns_mut(ctx.accounts, &mut app.decks_cache) get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.get_first_router() .get_first_router()
.go_back(); .go_back();
} }
@@ -734,7 +743,7 @@ fn render_nav_body(
} }
Route::EditDeck(index) => { Route::EditDeck(index) => {
let mut action = None; let mut action = None;
let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache) let cur_deck = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.decks_mut() .decks_mut()
.get_mut(*index) .get_mut(*index)
.expect("index wasn't valid"); .expect("index wasn't valid");
@@ -746,7 +755,7 @@ fn render_nav_body(
.id_to_deck_state .id_to_deck_state
.entry(id) .entry(id)
.or_insert_with(|| DeckState::from_deck(cur_deck)); .or_insert_with(|| DeckState::from_deck(cur_deck));
if let Some(resp) = EditDeckView::new(deck_state).ui(ui) { if let Some(resp) = EditDeckView::new(deck_state, ctx.i18n).ui(ui) {
match resp { match resp {
EditDeckResponse::Edit(configure_deck_response) => { EditDeckResponse::Edit(configure_deck_response) => {
cur_deck.edit(configure_deck_response); cur_deck.edit(configure_deck_response);
@@ -757,7 +766,7 @@ fn render_nav_body(
))); )));
} }
} }
get_active_columns_mut(ctx.accounts, &mut app.decks_cache) get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.get_first_router() .get_first_router()
.go_back(); .go_back();
} }
@@ -778,7 +787,7 @@ fn render_nav_body(
return action; return action;
}; };
if EditProfileView::new(state, ctx.img_cache).ui(ui) { if EditProfileView::new(ctx.i18n, state, ctx.img_cache).ui(ui) {
if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) { if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) {
action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges( action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
SaveProfileChanges::new(kp.to_full(), state.clone()), SaveProfileChanges::new(kp.to_full(), state.clone()),
@@ -833,7 +842,7 @@ fn render_nav_body(
} }
}; };
WalletView::new(state) WalletView::new(state, ctx.i18n)
.ui(ui) .ui(ui)
.map(RenderNavAction::WalletAction) .map(RenderNavAction::WalletAction)
} }
@@ -841,6 +850,7 @@ fn render_nav_body(
let txn = Transaction::new(ctx.ndb).expect("txn"); let txn = Transaction::new(ctx.ndb).expect("txn");
let default_msats = get_current_default_msats(ctx.accounts, ctx.global_wallet); let default_msats = get_current_default_msats(ctx.accounts, ctx.global_wallet);
CustomZapView::new( CustomZapView::new(
ctx.i18n,
ctx.img_cache, ctx.img_cache,
ctx.ndb, ctx.ndb,
&txn, &txn,
@@ -849,7 +859,7 @@ fn render_nav_body(
) )
.ui(ui) .ui(ui)
.map(|msats| { .map(|msats| {
get_active_columns_mut(ctx.accounts, &mut app.decks_cache) get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.go_back(); .go_back();
@@ -904,9 +914,10 @@ pub fn render_nav(
NavUiType::Title => NavTitle::new( NavUiType::Title => NavTitle::new(
ctx.ndb, ctx.ndb,
ctx.img_cache, ctx.img_cache,
get_active_columns_mut(ctx.accounts, &mut app.decks_cache), get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
&[route.clone()], &[route.clone()],
col, col,
ctx.i18n,
) )
.show_move_button(!narrow) .show_move_button(!narrow)
.show_delete_button(!narrow) .show_delete_button(!narrow)
@@ -926,13 +937,13 @@ pub fn render_nav(
.clone(), .clone(),
) )
.navigating( .navigating(
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.navigating, .navigating,
) )
.returning( .returning(
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.returning, .returning,
@@ -942,9 +953,10 @@ pub fn render_nav(
NavUiType::Title => NavTitle::new( NavUiType::Title => NavTitle::new(
ctx.ndb, ctx.ndb,
ctx.img_cache, ctx.img_cache,
get_active_columns_mut(ctx.accounts, &mut app.decks_cache), get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
nav.routes(), nav.routes(),
col, col,
ctx.i18n,
) )
.show_move_button(!narrow) .show_move_button(!narrow)
.show_delete_button(!narrow) .show_delete_button(!narrow)

View File

@@ -1,16 +1,10 @@
use enostr::{NoteId, Pubkey}; use enostr::{NoteId, Pubkey};
use notedeck::{tr, NoteZapTargetOwned, RootNoteIdBuf, WalletType}; use notedeck::{tr, Localization, NoteZapTargetOwned, RootNoteIdBuf, WalletType};
use std::{ use std::ops::Range;
fmt::{self},
ops::Range,
};
use crate::{ use crate::{
accounts::AccountsRoute, accounts::AccountsRoute,
timeline::{ timeline::{kind::ColumnTitle, ThreadSelection, TimelineKind},
kind::{AlgoTimeline, ColumnTitle, ListKind},
ThreadSelection, TimelineKind,
},
ui::add_column::{AddAlgoRoute, AddColumnRoute}, ui::add_column::{AddAlgoRoute, AddColumnRoute},
}; };
@@ -241,85 +235,104 @@ impl Route {
) )
} }
pub fn title(&self) -> ColumnTitle<'_> { pub fn title(&self, i18n: &mut Localization) -> ColumnTitle<'_> {
match self { match self {
Route::Timeline(kind) => kind.to_title(), Route::Timeline(kind) => kind.to_title(i18n),
Route::Thread(_) => { Route::Thread(_) => {
ColumnTitle::formatted(tr!("Thread", "Column title for note thread view")) ColumnTitle::formatted(tr!(i18n, "Thread", "Column title for note thread view"))
} }
Route::Reply(_id) => { Route::Reply(_id) => {
ColumnTitle::formatted(tr!("Reply", "Column title for reply composition")) ColumnTitle::formatted(tr!(i18n, "Reply", "Column title for reply composition"))
} }
Route::Quote(_id) => { Route::Quote(_id) => {
ColumnTitle::formatted(tr!("Quote", "Column title for quote composition")) ColumnTitle::formatted(tr!(i18n, "Quote", "Column title for quote composition"))
} }
Route::Relays => { Route::Relays => {
ColumnTitle::formatted(tr!("Relays", "Column title for relay management")) ColumnTitle::formatted(tr!(i18n, "Relays", "Column title for relay management"))
} }
Route::Accounts(amr) => match amr { Route::Accounts(amr) => match amr {
AccountsRoute::Accounts => { AccountsRoute::Accounts => ColumnTitle::formatted(tr!(
ColumnTitle::formatted(tr!("Accounts", "Column title for account management")) i18n,
} "Accounts",
"Column title for account management"
)),
AccountsRoute::AddAccount => ColumnTitle::formatted(tr!( AccountsRoute::AddAccount => ColumnTitle::formatted(tr!(
i18n,
"Add Account", "Add Account",
"Column title for adding new account" "Column title for adding new account"
)), )),
}, },
Route::ComposeNote => { Route::ComposeNote => ColumnTitle::formatted(tr!(
ColumnTitle::formatted(tr!("Compose Note", "Column title for note composition")) i18n,
} "Compose Note",
"Column title for note composition"
)),
Route::AddColumn(c) => match c { Route::AddColumn(c) => match c {
AddColumnRoute::Base => { AddColumnRoute::Base => ColumnTitle::formatted(tr!(
ColumnTitle::formatted(tr!("Add Column", "Column title for adding new column")) i18n,
} "Add Column",
"Column title for adding new column"
)),
AddColumnRoute::Algo(r) => match r { AddColumnRoute::Algo(r) => match r {
AddAlgoRoute::Base => ColumnTitle::formatted(tr!( AddAlgoRoute::Base => ColumnTitle::formatted(tr!(
i18n,
"Add Algo Column", "Add Algo Column",
"Column title for adding algorithm column" "Column title for adding algorithm column"
)), )),
AddAlgoRoute::LastPerPubkey => ColumnTitle::formatted(tr!( AddAlgoRoute::LastPerPubkey => ColumnTitle::formatted(tr!(
i18n,
"Add Last Notes Column", "Add Last Notes Column",
"Column title for adding last notes column" "Column title for adding last notes column"
)), )),
}, },
AddColumnRoute::UndecidedNotification => ColumnTitle::formatted(tr!( AddColumnRoute::UndecidedNotification => ColumnTitle::formatted(tr!(
i18n,
"Add Notifications Column", "Add Notifications Column",
"Column title for adding notifications column" "Column title for adding notifications column"
)), )),
AddColumnRoute::ExternalNotification => ColumnTitle::formatted(tr!( AddColumnRoute::ExternalNotification => ColumnTitle::formatted(tr!(
i18n,
"Add External Notifications Column", "Add External Notifications Column",
"Column title for adding external notifications column" "Column title for adding external notifications column"
)), )),
AddColumnRoute::Hashtag => ColumnTitle::formatted(tr!( AddColumnRoute::Hashtag => ColumnTitle::formatted(tr!(
i18n,
"Add Hashtag Column", "Add Hashtag Column",
"Column title for adding hashtag column" "Column title for adding hashtag column"
)), )),
AddColumnRoute::UndecidedIndividual => ColumnTitle::formatted(tr!( AddColumnRoute::UndecidedIndividual => ColumnTitle::formatted(tr!(
i18n,
"Subscribe to someone's notes", "Subscribe to someone's notes",
"Column title for subscribing to individual user" "Column title for subscribing to individual user"
)), )),
AddColumnRoute::ExternalIndividual => ColumnTitle::formatted(tr!( AddColumnRoute::ExternalIndividual => ColumnTitle::formatted(tr!(
i18n,
"Subscribe to someone else's notes", "Subscribe to someone else's notes",
"Column title for subscribing to external user" "Column title for subscribing to external user"
)), )),
}, },
Route::Support => { Route::Support => {
ColumnTitle::formatted(tr!("Damus Support", "Column title for support page")) ColumnTitle::formatted(tr!(i18n, "Damus Support", "Column title for support page"))
} }
Route::NewDeck => { Route::NewDeck => {
ColumnTitle::formatted(tr!("Add Deck", "Column title for adding new deck")) ColumnTitle::formatted(tr!(i18n, "Add Deck", "Column title for adding new deck"))
} }
Route::EditDeck(_) => { Route::EditDeck(_) => {
ColumnTitle::formatted(tr!("Edit Deck", "Column title for editing deck")) ColumnTitle::formatted(tr!(i18n, "Edit Deck", "Column title for editing deck"))
} }
Route::EditProfile(_) => { Route::EditProfile(_) => ColumnTitle::formatted(tr!(
ColumnTitle::formatted(tr!("Edit Profile", "Column title for profile editing")) i18n,
"Edit Profile",
"Column title for profile editing"
)),
Route::Search => {
ColumnTitle::formatted(tr!(i18n, "Search", "Column title for search page"))
} }
Route::Search => ColumnTitle::formatted(tr!("Search", "Column title for search page")),
Route::Wallet(_) => { Route::Wallet(_) => {
ColumnTitle::formatted(tr!("Wallet", "Column title for wallet management")) ColumnTitle::formatted(tr!(i18n, "Wallet", "Column title for wallet management"))
} }
Route::CustomizeZapAmount(_) => ColumnTitle::formatted(tr!( Route::CustomizeZapAmount(_) => ColumnTitle::formatted(tr!(
i18n,
"Customize Zap Amount", "Customize Zap Amount",
"Column title for zap amount customization" "Column title for zap amount customization"
)), )),
@@ -492,12 +505,13 @@ impl<R: Clone> Router<R> {
} }
} }
/*
impl fmt::Display for Route { impl fmt::Display for Route {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Route::Timeline(kind) => match kind { Route::Timeline(kind) => match kind {
TimelineKind::List(ListKind::Contact(_pk)) => { TimelineKind::List(ListKind::Contact(_pk)) => {
write!(f, "{}", tr!("Home", "Display name for home feed")) write!(f, "{}", i18n, "Home", "Display name for home feed"))
} }
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => { TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => {
write!( write!(
@@ -583,6 +597,7 @@ impl fmt::Display for Route {
} }
} }
} }
*/
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SingletonRouter<R: Clone> { pub struct SingletonRouter<R: Clone> {

View File

@@ -13,7 +13,7 @@ use crate::{
Error, Error,
}; };
use notedeck::{storage, DataPath, DataPathType, Directory}; use notedeck::{storage, DataPath, DataPathType, Directory, Localization};
use tokenator::{ParseError, TokenParser, TokenWriter}; use tokenator::{ParseError, TokenParser, TokenWriter};
pub static DECKS_CACHE_FILE: &str = "decks_cache.json"; pub static DECKS_CACHE_FILE: &str = "decks_cache.json";
@@ -22,6 +22,7 @@ pub fn load_decks_cache(
path: &DataPath, path: &DataPath,
ndb: &Ndb, ndb: &Ndb,
timeline_cache: &mut TimelineCache, timeline_cache: &mut TimelineCache,
i18n: &mut Localization,
) -> Option<DecksCache> { ) -> Option<DecksCache> {
let data_path = path.path(DataPathType::Setting); let data_path = path.path(DataPathType::Setting);
@@ -40,7 +41,7 @@ pub fn load_decks_cache(
serde_json::from_str::<SerializableDecksCache>(&decks_cache_str).ok()?; serde_json::from_str::<SerializableDecksCache>(&decks_cache_str).ok()?;
serializable_decks_cache serializable_decks_cache
.decks_cache(ndb, timeline_cache) .decks_cache(ndb, timeline_cache, i18n)
.ok() .ok()
} }
@@ -91,6 +92,7 @@ impl SerializableDecksCache {
self, self,
ndb: &Ndb, ndb: &Ndb,
timeline_cache: &mut TimelineCache, timeline_cache: &mut TimelineCache,
i18n: &mut Localization,
) -> Result<DecksCache, Error> { ) -> Result<DecksCache, Error> {
let account_to_decks = self let account_to_decks = self
.decks_cache .decks_cache
@@ -102,7 +104,7 @@ impl SerializableDecksCache {
}) })
.collect::<Result<HashMap<Pubkey, Decks>, Error>>()?; .collect::<Result<HashMap<Pubkey, Decks>, Error>>()?;
Ok(DecksCache::new(account_to_decks)) Ok(DecksCache::new(account_to_decks, i18n))
} }
} }

View File

@@ -6,11 +6,11 @@ use nostrdb::{Ndb, Transaction};
use notedeck::{ use notedeck::{
contacts::{contacts_filter, hybrid_contacts_filter}, contacts::{contacts_filter, hybrid_contacts_filter},
filter::{self, default_limit, default_remote_limit, HybridFilter}, filter::{self, default_limit, default_remote_limit, HybridFilter},
tr, FilterError, FilterState, NoteCache, RootIdError, RootNoteIdBuf, tr, FilterError, FilterState, Localization, NoteCache, RootIdError, RootNoteIdBuf,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::{borrow::Cow, fmt::Display};
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
use tracing::{error, warn}; use tracing::{error, warn};
@@ -254,6 +254,7 @@ impl AlgoTimeline {
} }
} }
/*
impl Display for TimelineKind { impl Display for TimelineKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@@ -301,6 +302,7 @@ impl Display for TimelineKind {
} }
} }
} }
*/
impl TimelineKind { impl TimelineKind {
pub fn pubkey(&self) -> Option<&Pubkey> { pub fn pubkey(&self) -> Option<&Pubkey> {
@@ -594,31 +596,32 @@ impl TimelineKind {
} }
} }
pub fn to_title(&self) -> ColumnTitle<'_> { pub fn to_title(&self, i18n: &mut Localization) -> ColumnTitle<'_> {
match self { match self {
TimelineKind::Search(query) => { TimelineKind::Search(query) => {
ColumnTitle::formatted(format!("Search \"{}\"", query.search)) ColumnTitle::formatted(format!("Search \"{}\"", query.search))
} }
TimelineKind::List(list_kind) => match list_kind { TimelineKind::List(list_kind) => match list_kind {
ListKind::Contact(_pubkey_source) => { ListKind::Contact(_pubkey_source) => {
ColumnTitle::formatted(tr!("Contacts", "Column title for contact lists")) ColumnTitle::formatted(tr!(i18n, "Contacts", "Column title for contact lists"))
} }
}, },
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => match list_kind { TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => match list_kind {
ListKind::Contact(_pubkey_source) => ColumnTitle::formatted(tr!( ListKind::Contact(_pubkey_source) => ColumnTitle::formatted(tr!(
i18n,
"Contacts (last notes)", "Contacts (last notes)",
"Column title for last notes per contact" "Column title for last notes per contact"
)), )),
}, },
TimelineKind::Notifications(_pubkey_source) => { TimelineKind::Notifications(_pubkey_source) => {
ColumnTitle::formatted(tr!("Notifications", "Column title for notifications")) ColumnTitle::formatted(tr!(i18n, "Notifications", "Column title for notifications"))
} }
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self), TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
TimelineKind::Universe => { TimelineKind::Universe => {
ColumnTitle::formatted(tr!("Universe", "Column title for universe feed")) ColumnTitle::formatted(tr!(i18n, "Universe", "Column title for universe feed"))
} }
TimelineKind::Generic(_) => { TimelineKind::Generic(_) => {
ColumnTitle::formatted(tr!("Custom", "Column title for custom timelines")) ColumnTitle::formatted(tr!(i18n, "Custom", "Column title for custom timelines"))
} }
TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()), TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()),
} }

View File

@@ -9,8 +9,8 @@ use crate::{
use notedeck::{ use notedeck::{
contacts::hybrid_contacts_filter, contacts::hybrid_contacts_filter,
filter::{self, HybridFilter}, filter::{self, HybridFilter},
tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, NoteCache, tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization,
NoteRef, UnknownIds, NoteCache, NoteRef, UnknownIds,
}; };
use egui_virtual_list::VirtualList; use egui_virtual_list::VirtualList;
@@ -64,11 +64,15 @@ pub enum ViewFilter {
} }
impl ViewFilter { impl ViewFilter {
pub fn name(&self) -> String { pub fn name(&self, i18n: &mut Localization) -> String {
match self { match self {
ViewFilter::Notes => tr!("Notes", "Filter label for notes only view"), ViewFilter::Notes => tr!(i18n, "Notes", "Filter label for notes only view"),
ViewFilter::NotesAndReplies => { ViewFilter::NotesAndReplies => {
tr!("Notes & Replies", "Filter label for notes and replies view") tr!(
i18n,
"Notes & Replies",
"Filter label for notes and replies view"
)
} }
} }
} }

View File

@@ -1,12 +1,11 @@
use crate::login_manager::AcquireKeyState; use crate::login_manager::AcquireKeyState;
use crate::ui::{Preview, PreviewConfig}; use crate::ui::{Preview, PreviewConfig};
use egui::{ use egui::{
Align, Button, Color32, Frame, InnerResponse, Layout, Margin, RichText, TextBuffer, TextEdit, Align, Button, Color32, Frame, InnerResponse, Layout, Margin, RichText, TextEdit, Vec2,
Vec2,
}; };
use egui_winit::clipboard::Clipboard; use egui_winit::clipboard::Clipboard;
use enostr::Keypair; use enostr::Keypair;
use notedeck::{fonts::get_font_size, tr, AppAction, NotedeckTextStyle}; use notedeck::{fonts::get_font_size, tr, AppAction, Localization, NotedeckTextStyle};
use notedeck_ui::{ use notedeck_ui::{
app_images, app_images,
context_menu::{input_context, PasteBehavior}, context_menu::{input_context, PasteBehavior},
@@ -15,6 +14,7 @@ use notedeck_ui::{
pub struct AccountLoginView<'a> { pub struct AccountLoginView<'a> {
manager: &'a mut AcquireKeyState, manager: &'a mut AcquireKeyState,
clipboard: &'a mut Clipboard, clipboard: &'a mut Clipboard,
i18n: &'a mut Localization,
} }
pub enum AccountLoginResponse { pub enum AccountLoginResponse {
@@ -23,8 +23,16 @@ pub enum AccountLoginResponse {
} }
impl<'a> AccountLoginView<'a> { impl<'a> AccountLoginView<'a> {
pub fn new(manager: &'a mut AcquireKeyState, clipboard: &'a mut Clipboard) -> Self { pub fn new(
AccountLoginView { manager, clipboard } manager: &'a mut AcquireKeyState,
clipboard: &'a mut Clipboard,
i18n: &'a mut Localization,
) -> Self {
AccountLoginView {
manager,
clipboard,
i18n,
}
} }
pub fn ui(&mut self, ui: &mut egui::Ui) -> InnerResponse<Option<AccountLoginResponse>> { pub fn ui(&mut self, ui: &mut egui::Ui) -> InnerResponse<Option<AccountLoginResponse>> {
@@ -35,11 +43,11 @@ impl<'a> AccountLoginView<'a> {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add_space(32.0); ui.add_space(32.0);
ui.label(login_title_text()); ui.label(login_title_text(self.i18n));
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(login_textedit_info_text()); ui.label(login_textedit_info_text(self.i18n));
}); });
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
@@ -48,7 +56,7 @@ impl<'a> AccountLoginView<'a> {
let button_width = 32.0; let button_width = 32.0;
let text_edit_width = available_width - button_width; let text_edit_width = available_width - button_width;
let textedit_resp = ui.add_sized([text_edit_width, 40.0], login_textedit(self.manager)); let textedit_resp = ui.add_sized([text_edit_width, 40.0], login_textedit(self.manager, self.i18n));
input_context(&textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear); input_context(&textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear);
if eye_button(ui, self.manager.password_visible()).clicked() { if eye_button(ui, self.manager.password_visible()).clicked() {
@@ -58,28 +66,28 @@ impl<'a> AccountLoginView<'a> {
ui.with_layout(Layout::left_to_right(Align::TOP), |ui| { ui.with_layout(Layout::left_to_right(Align::TOP), |ui| {
let help_text_style = NotedeckTextStyle::Small; let help_text_style = NotedeckTextStyle::Small;
ui.add(egui::Label::new( ui.add(egui::Label::new(
RichText::new(tr!("Enter your public key (npub), nostr address (e.g. {address}), or private key (nsec). You must enter your private key to be able to post, reply, etc.", "Instructions for entering Nostr credentials", address="vrod@damus.io")) RichText::new(tr!(self.i18n, "Enter your public key (npub), nostr address (e.g. {address}), or private key (nsec). You must enter your private key to be able to post, reply, etc.", "Instructions for entering Nostr credentials", address="vrod@damus.io"))
.text_style(help_text_style.text_style()) .text_style(help_text_style.text_style())
.size(get_font_size(ui.ctx(), &help_text_style)).color(ui.visuals().weak_text_color()), .size(get_font_size(ui.ctx(), &help_text_style)).color(ui.visuals().weak_text_color()),
).wrap()) ).wrap())
}); });
self.manager.loading_and_error_ui(ui); self.manager.loading_and_error_ui(ui, self.i18n);
if ui.add(login_button()).clicked() { if ui.add(login_button(self.i18n)).clicked() {
self.manager.apply_acquire(); self.manager.apply_acquire();
} }
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label( ui.label(
RichText::new(tr!("New to Nostr?", "Label asking if the user is new to Nostr. Underneath this label is a button to create an account.")) RichText::new(tr!(self.i18n,"New to Nostr?", "Label asking if the user is new to Nostr. Underneath this label is a button to create an account."))
.color(ui.style().visuals.noninteractive().fg_stroke.color) .color(ui.style().visuals.noninteractive().fg_stroke.color)
.text_style(NotedeckTextStyle::Body.text_style()), .text_style(NotedeckTextStyle::Body.text_style()),
); );
if ui if ui
.add(Button::new(RichText::new(tr!("Create Account", "Button to create a new account"))).frame(false)) .add(Button::new(RichText::new(tr!(self.i18n,"Create Account", "Button to create a new account"))).frame(false))
.clicked() .clicked()
{ {
self.manager.should_create_new(); self.manager.should_create_new();
@@ -98,21 +106,21 @@ impl<'a> AccountLoginView<'a> {
} }
} }
fn login_title_text() -> RichText { fn login_title_text(i18n: &mut Localization) -> RichText {
RichText::new(tr!("Login", "Login page title")) RichText::new(tr!(i18n, "Login", "Login page title"))
.text_style(NotedeckTextStyle::Heading2.text_style()) .text_style(NotedeckTextStyle::Heading2.text_style())
.strong() .strong()
} }
fn login_textedit_info_text() -> RichText { fn login_textedit_info_text(i18n: &mut Localization) -> RichText {
RichText::new(tr!("Enter your key", "Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).")) RichText::new(tr!(i18n, "Enter your key", "Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05)."))
.strong() .strong()
.text_style(NotedeckTextStyle::Body.text_style()) .text_style(NotedeckTextStyle::Body.text_style())
} }
fn login_button() -> Button<'static> { fn login_button(i18n: &mut Localization) -> Button<'static> {
Button::new( Button::new(
RichText::new(tr!("Login now — let's do this!", "Login button text")) RichText::new(tr!(i18n, "Login now — let's do this!", "Login button text"))
.text_style(NotedeckTextStyle::Body.text_style()) .text_style(NotedeckTextStyle::Body.text_style())
.strong(), .strong(),
) )
@@ -120,11 +128,15 @@ fn login_button() -> Button<'static> {
.min_size(Vec2::new(0.0, 40.0)) .min_size(Vec2::new(0.0, 40.0))
} }
fn login_textedit(manager: &mut AcquireKeyState) -> TextEdit { fn login_textedit<'a>(
let create_textedit: fn(&mut dyn TextBuffer) -> TextEdit = |text| { manager: &'a mut AcquireKeyState,
i18n: &'a mut Localization,
) -> TextEdit<'a> {
let create_textedit = |text| {
egui::TextEdit::singleline(text) egui::TextEdit::singleline(text)
.hint_text( .hint_text(
RichText::new(tr!( RichText::new(tr!(
i18n,
"Your key here...", "Your key here...",
"Placeholder text for key input field" "Placeholder text for key input field"
)) ))
@@ -167,7 +179,7 @@ mod preview {
impl App for AccountLoginPreview { impl App for AccountLoginPreview {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
AccountLoginView::new(&mut self.manager, ctx.clipboard).ui(ui); AccountLoginView::new(&mut self.manager, ctx.clipboard, ctx.i18n).ui(ui);
None None
} }

View File

@@ -3,16 +3,17 @@ use egui::{
}; };
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::{tr, Accounts, Images}; use notedeck::{tr, Accounts, Images, Localization};
use notedeck_ui::colors::PINK; use notedeck_ui::colors::PINK;
use notedeck_ui::profile::preview::SimpleProfilePreview;
use notedeck_ui::app_images; use notedeck_ui::app_images;
use notedeck_ui::profile::preview::SimpleProfilePreview;
pub struct AccountsView<'a> { pub struct AccountsView<'a> {
ndb: &'a Ndb, ndb: &'a Ndb,
accounts: &'a Accounts, accounts: &'a Accounts,
img_cache: &'a mut Images, img_cache: &'a mut Images,
i18n: &'a mut Localization,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -29,24 +30,30 @@ enum ProfilePreviewAction {
} }
impl<'a> AccountsView<'a> { impl<'a> AccountsView<'a> {
pub fn new(ndb: &'a Ndb, accounts: &'a Accounts, img_cache: &'a mut Images) -> Self { pub fn new(
ndb: &'a Ndb,
accounts: &'a Accounts,
img_cache: &'a mut Images,
i18n: &'a mut Localization,
) -> Self {
AccountsView { AccountsView {
ndb, ndb,
accounts, accounts,
img_cache, img_cache,
i18n,
} }
} }
pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> { pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> {
Frame::new().outer_margin(12.0).show(ui, |ui| { Frame::new().outer_margin(12.0).show(ui, |ui| {
if let Some(resp) = Self::top_section_buttons_widget(ui).inner { if let Some(resp) = Self::top_section_buttons_widget(ui, self.i18n).inner {
return Some(resp); return Some(resp);
} }
ui.add_space(8.0); ui.add_space(8.0);
scroll_area() scroll_area()
.show(ui, |ui| { .show(ui, |ui| {
Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache) Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache, self.i18n)
}) })
.inner .inner
}) })
@@ -57,6 +64,7 @@ impl<'a> AccountsView<'a> {
accounts: &Accounts, accounts: &Accounts,
ndb: &Ndb, ndb: &Ndb,
img_cache: &mut Images, img_cache: &mut Images,
i18n: &mut Localization,
) -> Option<AccountsViewResponse> { ) -> Option<AccountsViewResponse> {
let mut return_op: Option<AccountsViewResponse> = None; let mut return_op: Option<AccountsViewResponse> = None;
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
@@ -79,8 +87,12 @@ impl<'a> AccountsView<'a> {
let max_size = egui::vec2(ui.available_width(), 77.0); let max_size = egui::vec2(ui.available_width(), 77.0);
let resp = ui.allocate_response(max_size, egui::Sense::click()); let resp = ui.allocate_response(max_size, egui::Sense::click());
ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| { ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| {
let preview = let preview = SimpleProfilePreview::new(
SimpleProfilePreview::new(profile.as_ref(), img_cache, has_nsec); profile.as_ref(),
img_cache,
i18n,
has_nsec,
);
show_profile_card(ui, preview, max_size, is_selected, resp) show_profile_card(ui, preview, max_size, is_selected, resp)
}) })
.inner .inner
@@ -104,12 +116,13 @@ impl<'a> AccountsView<'a> {
fn top_section_buttons_widget( fn top_section_buttons_widget(
ui: &mut egui::Ui, ui: &mut egui::Ui,
i18n: &mut Localization,
) -> InnerResponse<Option<AccountsViewResponse>> { ) -> InnerResponse<Option<AccountsViewResponse>> {
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
Vec2::new(ui.available_size_before_wrap().x, 32.0), Vec2::new(ui.available_size_before_wrap().x, 32.0),
Layout::left_to_right(egui::Align::Center), Layout::left_to_right(egui::Align::Center),
|ui| { |ui| {
if ui.add(add_account_button()).clicked() { if ui.add(add_account_button(i18n)).clicked() {
Some(AccountsViewResponse::RouteToLogin) Some(AccountsViewResponse::RouteToLogin)
} else { } else {
None None
@@ -141,16 +154,14 @@ fn show_profile_card(
.inner_margin(8.0) .inner_margin(8.0)
.show(ui, |ui| { .show(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let btn = sign_out_button(preview.i18n);
ui.add(preview); ui.add(preview);
ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if card_resp.clicked() { if card_resp.clicked() {
op = Some(ProfilePreviewAction::SwitchTo); op = Some(ProfilePreviewAction::SwitchTo);
} }
if ui if ui.add_sized(egui::Vec2::new(84.0, 32.0), btn).clicked() {
.add_sized(egui::Vec2::new(84.0, 32.0), sign_out_button())
.clicked()
{
op = Some(ProfilePreviewAction::RemoveAccount) op = Some(ProfilePreviewAction::RemoveAccount)
} }
}); });
@@ -168,19 +179,24 @@ fn scroll_area() -> ScrollArea {
.auto_shrink([false; 2]) .auto_shrink([false; 2])
} }
fn add_account_button() -> Button<'static> { fn add_account_button(i18n: &mut Localization) -> Button<'static> {
Button::image_and_text( Button::image_and_text(
app_images::add_account_image().fit_to_exact_size(Vec2::new(48.0, 48.0)), app_images::add_account_image().fit_to_exact_size(Vec2::new(48.0, 48.0)),
RichText::new(tr!("Add account", "Button label to add a new account")) RichText::new(tr!(
.size(16.0) i18n,
// TODO: this color should not be hard coded. Find some way to add it to the visuals "Add account",
.color(PINK), "Button label to add a new account"
))
.size(16.0)
// TODO: this color should not be hard coded. Find some way to add it to the visuals
.color(PINK),
) )
.frame(false) .frame(false)
} }
fn sign_out_button() -> egui::Button<'static> { fn sign_out_button(i18n: &mut Localization) -> egui::Button<'static> {
egui::Button::new(RichText::new(tr!( egui::Button::new(RichText::new(tr!(
i18n,
"Sign out", "Sign out",
"Button label to sign out of account" "Button label to sign out of account"
))) )))

View File

@@ -17,7 +17,7 @@ use crate::{
Damus, Damus,
}; };
use notedeck::{tr, AppContext, Images, NotedeckTextStyle, UserAccount}; use notedeck::{tr, AppContext, Images, Localization, NotedeckTextStyle, UserAccount};
use notedeck_ui::{anim::ICON_EXPANSION_MULTIPLE, app_images}; use notedeck_ui::{anim::ICON_EXPANSION_MULTIPLE, app_images};
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
@@ -167,6 +167,7 @@ pub struct AddColumnView<'a> {
ndb: &'a Ndb, ndb: &'a Ndb,
img_cache: &'a mut Images, img_cache: &'a mut Images,
cur_account: &'a UserAccount, cur_account: &'a UserAccount,
i18n: &'a mut Localization,
} }
impl<'a> AddColumnView<'a> { impl<'a> AddColumnView<'a> {
@@ -175,12 +176,14 @@ impl<'a> AddColumnView<'a> {
ndb: &'a Ndb, ndb: &'a Ndb,
img_cache: &'a mut Images, img_cache: &'a mut Images,
cur_account: &'a UserAccount, cur_account: &'a UserAccount,
i18n: &'a mut Localization,
) -> Self { ) -> Self {
Self { Self {
key_state_map, key_state_map,
ndb, ndb,
img_cache, img_cache,
cur_account, cur_account,
i18n,
} }
} }
@@ -229,8 +232,9 @@ impl<'a> AddColumnView<'a> {
deck_author: Pubkey, deck_author: Pubkey,
) -> Option<AddColumnResponse> { ) -> Option<AddColumnResponse> {
let algo_option = ColumnOptionData { let algo_option = ColumnOptionData {
title: tr!("Contact List", "Title for contact list column"), title: tr!(self.i18n, "Contact List", "Title for contact list column"),
description: tr!( description: tr!(
self.i18n,
"Source the last note for each user in your contact list", "Source the last note for each user in your contact list",
"Description for contact list column" "Description for contact list column"
), ),
@@ -248,8 +252,13 @@ impl<'a> AddColumnView<'a> {
fn algo_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> { fn algo_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
let algo_option = ColumnOptionData { let algo_option = ColumnOptionData {
title: tr!("Last Note per User", "Title for last note per user column"), title: tr!(
self.i18n,
"Last Note per User",
"Title for last note per user column"
),
description: tr!( description: tr!(
self.i18n,
"Show the last note for each user from a list", "Show the last note for each user from a list",
"Description for last note per user column" "Description for last note per user column"
), ),
@@ -298,6 +307,7 @@ impl<'a> AddColumnView<'a> {
egui::TextEdit::singleline(text) egui::TextEdit::singleline(text)
.hint_text( .hint_text(
RichText::new(tr!( RichText::new(tr!(
self.i18n,
"Enter the user's key (npub, hex, nip05) here...", "Enter the user's key (npub, hex, nip05) here...",
"Hint text to prompt entering the user's public key." "Hint text to prompt entering the user's public key."
)) ))
@@ -312,9 +322,11 @@ impl<'a> AddColumnView<'a> {
ui.add(text_edit); ui.add(text_edit);
key_state.handle_input_change_after_acquire(); key_state.handle_input_change_after_acquire();
key_state.loading_and_error_ui(ui); key_state.loading_and_error_ui(ui, self.i18n);
if key_state.get_login_keypair().is_none() && ui.add(find_user_button()).clicked() { if key_state.get_login_keypair().is_none()
&& ui.add(find_user_button(self.i18n)).clicked()
{
key_state.apply_acquire(); key_state.apply_acquire();
} }
@@ -337,7 +349,7 @@ impl<'a> AddColumnView<'a> {
} }
} }
ui.add(add_column_button()) ui.add(add_column_button(self.i18n))
.clicked() .clicked()
.then(|| to_option(keypair.pubkey).take_as_response(self.cur_account)) .then(|| to_option(keypair.pubkey).take_as_response(self.cur_account))
} else { } else {
@@ -452,11 +464,12 @@ impl<'a> AddColumnView<'a> {
helper.take_animation_response() helper.take_animation_response()
} }
fn get_base_options(&self, ui: &mut Ui) -> Vec<ColumnOptionData> { fn get_base_options(&mut self, ui: &mut Ui) -> Vec<ColumnOptionData> {
let mut vec = Vec::new(); let mut vec = Vec::new();
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!("Home", "Title for Home column"), title: tr!(self.i18n, "Home", "Title for Home column"),
description: tr!( description: tr!(
self.i18n,
"See notes from your contacts", "See notes from your contacts",
"Description for Home column" "Description for Home column"
), ),
@@ -468,8 +481,9 @@ impl<'a> AddColumnView<'a> {
}), }),
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!("Notifications", "Title for notifications column"), title: tr!(self.i18n, "Notifications", "Title for notifications column"),
description: tr!( description: tr!(
self.i18n,
"Stay up to date with notifications and mentions", "Stay up to date with notifications and mentions",
"Description for notifications column" "Description for notifications column"
), ),
@@ -477,8 +491,9 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::UndecidedNotification, option: AddColumnOption::UndecidedNotification,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!("Universe", "Title for universe column"), title: tr!(self.i18n, "Universe", "Title for universe column"),
description: tr!( description: tr!(
self.i18n,
"See the whole nostr universe", "See the whole nostr universe",
"Description for universe column" "Description for universe column"
), ),
@@ -486,8 +501,9 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::Universe, option: AddColumnOption::Universe,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!("Hashtags", "Title for hashtags column"), title: tr!(self.i18n, "Hashtags", "Title for hashtags column"),
description: tr!( description: tr!(
self.i18n,
"Stay up to date with a certain hashtag", "Stay up to date with a certain hashtag",
"Description for hashtags column" "Description for hashtags column"
), ),
@@ -495,8 +511,9 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::UndecidedHashtag, option: AddColumnOption::UndecidedHashtag,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!("Individual", "Title for individual user column"), title: tr!(self.i18n, "Individual", "Title for individual user column"),
description: tr!( description: tr!(
self.i18n,
"Stay up to date with someone's notes & replies", "Stay up to date with someone's notes & replies",
"Description for individual user column" "Description for individual user column"
), ),
@@ -504,8 +521,9 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::UndecidedIndividual, option: AddColumnOption::UndecidedIndividual,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!("Algo", "Title for algorithmic feeds column"), title: tr!(self.i18n, "Algo", "Title for algorithmic feeds column"),
description: tr!( description: tr!(
self.i18n,
"Algorithmic feeds to aid in note discovery", "Algorithmic feeds to aid in note discovery",
"Description for algorithmic feeds column" "Description for algorithmic feeds column"
), ),
@@ -516,7 +534,7 @@ impl<'a> AddColumnView<'a> {
vec vec
} }
fn get_notifications_options(&self, ui: &mut Ui) -> Vec<ColumnOptionData> { fn get_notifications_options(&mut self, ui: &mut Ui) -> Vec<ColumnOptionData> {
let mut vec = Vec::new(); let mut vec = Vec::new();
let source = if self.cur_account.key.secret_key.is_some() { let source = if self.cur_account.key.secret_key.is_some() {
@@ -526,8 +544,13 @@ impl<'a> AddColumnView<'a> {
}; };
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!("Your Notifications", "Title for your notifications column"), title: tr!(
self.i18n,
"Your Notifications",
"Title for your notifications column"
),
description: tr!( description: tr!(
self.i18n,
"Stay up to date with your notifications and mentions", "Stay up to date with your notifications and mentions",
"Description for your notifications column" "Description for your notifications column"
), ),
@@ -537,10 +560,12 @@ impl<'a> AddColumnView<'a> {
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!( title: tr!(
self.i18n,
"Someone else's Notifications", "Someone else's Notifications",
"Title for someone else's notifications column" "Title for someone else's notifications column"
), ),
description: tr!( description: tr!(
self.i18n,
"Stay up to date with someone else's notifications and mentions", "Stay up to date with someone else's notifications and mentions",
"Description for someone else's notifications column" "Description for someone else's notifications column"
), ),
@@ -551,7 +576,7 @@ impl<'a> AddColumnView<'a> {
vec vec
} }
fn get_individual_options(&self) -> Vec<ColumnOptionData> { fn get_individual_options(&mut self) -> Vec<ColumnOptionData> {
let mut vec = Vec::new(); let mut vec = Vec::new();
let source = if self.cur_account.key.secret_key.is_some() { let source = if self.cur_account.key.secret_key.is_some() {
@@ -561,8 +586,9 @@ impl<'a> AddColumnView<'a> {
}; };
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!("Your Notes", "Title for your notes column"), title: tr!(self.i18n, "Your Notes", "Title for your notes column"),
description: tr!( description: tr!(
self.i18n,
"Keep track of your notes & replies", "Keep track of your notes & replies",
"Description for your notes column" "Description for your notes column"
), ),
@@ -572,10 +598,12 @@ impl<'a> AddColumnView<'a> {
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: tr!( title: tr!(
self.i18n,
"Someone else's Notes", "Someone else's Notes",
"Title for someone else's notes column" "Title for someone else's notes column"
), ),
description: tr!( description: tr!(
self.i18n,
"Stay up to date with someone else's notes & replies", "Stay up to date with someone else's notes & replies",
"Description for someone else's notes column" "Description for someone else's notes column"
), ),
@@ -587,14 +615,14 @@ impl<'a> AddColumnView<'a> {
} }
} }
fn find_user_button() -> impl Widget { fn find_user_button(i18n: &mut Localization) -> impl Widget {
let label = tr!("Find User", "Label for find user button"); let label = tr!(i18n, "Find User", "Label for find user button");
let color = notedeck_ui::colors::PINK; let color = notedeck_ui::colors::PINK;
move |ui: &mut egui::Ui| styled_button(label.as_str(), color).ui(ui) move |ui: &mut egui::Ui| styled_button(label.as_str(), color).ui(ui)
} }
fn add_column_button() -> impl Widget { fn add_column_button(i18n: &mut Localization) -> impl Widget {
let label = tr!("Add", "Label for add column button"); let label = tr!(i18n, "Add", "Label for add column button");
let color = notedeck_ui::colors::PINK; let color = notedeck_ui::colors::PINK;
move |ui: &mut egui::Ui| styled_button(label.as_str(), color).ui(ui) move |ui: &mut egui::Ui| styled_button(label.as_str(), color).ui(ui)
} }
@@ -639,6 +667,7 @@ pub fn render_add_column_routes(
ctx.ndb, ctx.ndb,
ctx.img_cache, ctx.img_cache,
ctx.accounts.get_selected_account(), ctx.accounts.get_selected_account(),
ctx.i18n,
); );
let resp = match route { let resp = match route {
AddColumnRoute::Base => add_column_view.ui(ui), AddColumnRoute::Base => add_column_view.ui(ui),
@@ -649,7 +678,7 @@ pub fn render_add_column_routes(
}, },
AddColumnRoute::UndecidedNotification => add_column_view.notifications_ui(ui), AddColumnRoute::UndecidedNotification => add_column_view.notifications_ui(ui),
AddColumnRoute::ExternalNotification => add_column_view.external_notification_ui(ui), AddColumnRoute::ExternalNotification => add_column_view.external_notification_ui(ui),
AddColumnRoute::Hashtag => hashtag_ui(ui, &mut app.view_state.id_string_map), AddColumnRoute::Hashtag => hashtag_ui(ui, ctx.i18n, &mut app.view_state.id_string_map),
AddColumnRoute::UndecidedIndividual => add_column_view.individual_ui(ui), AddColumnRoute::UndecidedIndividual => add_column_view.individual_ui(ui),
AddColumnRoute::ExternalIndividual => add_column_view.external_individual_ui(ui), AddColumnRoute::ExternalIndividual => add_column_view.external_individual_ui(ui),
}; };
@@ -677,7 +706,7 @@ pub fn render_add_column_routes(
ctx.accounts, ctx.accounts,
); );
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.route_to_replaced(Route::timeline(timeline.kind.clone())); .route_to_replaced(Route::timeline(timeline.kind.clone()));
@@ -689,7 +718,7 @@ pub fn render_add_column_routes(
// If we are undecided, we simply route to the LastPerPubkey // If we are undecided, we simply route to the LastPerPubkey
// algo route selection // algo route selection
AlgoOption::LastPerPubkey(Decision::Undecided) => { AlgoOption::LastPerPubkey(Decision::Undecided) => {
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.route_to(Route::AddColumn(AddColumnRoute::Algo( .route_to(Route::AddColumn(AddColumnRoute::Algo(
@@ -717,7 +746,7 @@ pub fn render_add_column_routes(
ctx.accounts, ctx.accounts,
); );
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.route_to_replaced(Route::timeline(timeline.kind.clone())); .route_to_replaced(Route::timeline(timeline.kind.clone()));
@@ -735,13 +764,13 @@ pub fn render_add_column_routes(
}, },
AddColumnResponse::UndecidedNotification => { AddColumnResponse::UndecidedNotification => {
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.route_to(Route::AddColumn(AddColumnRoute::UndecidedNotification)); .route_to(Route::AddColumn(AddColumnRoute::UndecidedNotification));
} }
AddColumnResponse::ExternalNotification => { AddColumnResponse::ExternalNotification => {
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.route_to(crate::route::Route::AddColumn( .route_to(crate::route::Route::AddColumn(
@@ -749,13 +778,13 @@ pub fn render_add_column_routes(
)); ));
} }
AddColumnResponse::Hashtag => { AddColumnResponse::Hashtag => {
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.route_to(crate::route::Route::AddColumn(AddColumnRoute::Hashtag)); .route_to(crate::route::Route::AddColumn(AddColumnRoute::Hashtag));
} }
AddColumnResponse::UndecidedIndividual => { AddColumnResponse::UndecidedIndividual => {
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.route_to(crate::route::Route::AddColumn( .route_to(crate::route::Route::AddColumn(
@@ -763,7 +792,7 @@ pub fn render_add_column_routes(
)); ));
} }
AddColumnResponse::ExternalIndividual => { AddColumnResponse::ExternalIndividual => {
app.columns_mut(ctx.accounts) app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col) .column_mut(col)
.router_mut() .router_mut()
.route_to(crate::route::Route::AddColumn( .route_to(crate::route::Route::AddColumn(
@@ -776,6 +805,7 @@ pub fn render_add_column_routes(
pub fn hashtag_ui( pub fn hashtag_ui(
ui: &mut Ui, ui: &mut Ui,
i18n: &mut Localization,
id_string_map: &mut HashMap<Id, String>, id_string_map: &mut HashMap<Id, String>,
) -> Option<AddColumnResponse> { ) -> Option<AddColumnResponse> {
padding(16.0, ui, |ui| { padding(16.0, ui, |ui| {
@@ -785,6 +815,7 @@ pub fn hashtag_ui(
let text_edit = egui::TextEdit::singleline(text_buffer) let text_edit = egui::TextEdit::singleline(text_buffer)
.hint_text( .hint_text(
RichText::new(tr!( RichText::new(tr!(
i18n,
"Enter the desired hashtags here (for multiple space-separated)", "Enter the desired hashtags here (for multiple space-separated)",
"Placeholder for hashtag input field" "Placeholder for hashtag input field"
)) ))
@@ -801,7 +832,7 @@ pub fn hashtag_ui(
let mut handle_user_input = false; let mut handle_user_input = false;
if ui.input(|i| i.key_released(egui::Key::Enter)) if ui.input(|i| i.key_released(egui::Key::Enter))
|| ui || ui
.add_sized(egui::vec2(50.0, 40.0), add_column_button()) .add_sized(egui::vec2(50.0, 40.0), add_column_button(i18n))
.clicked() .clicked()
{ {
handle_user_input = true; handle_user_input = true;

View File

@@ -13,7 +13,7 @@ use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder};
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::tr; use notedeck::tr;
use notedeck::{Images, NotedeckTextStyle}; use notedeck::{Images, Localization, NotedeckTextStyle};
use notedeck_ui::app_images; use notedeck_ui::app_images;
use notedeck_ui::{ use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
@@ -27,6 +27,7 @@ pub struct NavTitle<'a> {
routes: &'a [Route], routes: &'a [Route],
col_id: usize, col_id: usize,
options: u32, options: u32,
i18n: &'a mut Localization,
} }
impl<'a> NavTitle<'a> { impl<'a> NavTitle<'a> {
@@ -40,6 +41,7 @@ impl<'a> NavTitle<'a> {
columns: &'a Columns, columns: &'a Columns,
routes: &'a [Route], routes: &'a [Route],
col_id: usize, col_id: usize,
i18n: &'a mut Localization,
) -> Self { ) -> Self {
let options = Self::SHOW_MOVE | Self::SHOW_DELETE; let options = Self::SHOW_MOVE | Self::SHOW_DELETE;
NavTitle { NavTitle {
@@ -49,6 +51,7 @@ impl<'a> NavTitle<'a> {
routes, routes,
col_id, col_id,
options, options,
i18n,
} }
} }
@@ -129,7 +132,7 @@ impl<'a> NavTitle<'a> {
// NOTE(jb55): include graphic in back label as well because why // NOTE(jb55): include graphic in back label as well because why
// not it looks cool // not it looks cool
let pfp_resp = self.title_pfp(ui, prev, 32.0); let pfp_resp = self.title_pfp(ui, prev, 32.0);
let column_title = prev.title(); let column_title = prev.title(self.i18n);
let back_resp = match &column_title { let back_resp = match &column_title {
ColumnTitle::Simple(title) => ui.add(Self::back_label(title, color)), ColumnTitle::Simple(title) => ui.add(Self::back_label(title, color)),
@@ -182,7 +185,7 @@ impl<'a> NavTitle<'a> {
animation_resp animation_resp
} }
fn delete_button_section(&self, ui: &mut egui::Ui) -> bool { fn delete_button_section(&mut self, ui: &mut egui::Ui) -> bool {
let id = ui.id().with("title"); let id = ui.id().with("title");
let delete_button_resp = self.delete_column_button(ui, 32.0); let delete_button_resp = self.delete_column_button(ui, 32.0);
@@ -193,14 +196,18 @@ impl<'a> NavTitle<'a> {
if ui.data_mut(|d| *d.get_temp_mut_or_default(id)) { if ui.data_mut(|d| *d.get_temp_mut_or_default(id)) {
let mut confirm_pressed = false; let mut confirm_pressed = false;
delete_button_resp.show_tooltip_ui(|ui| { delete_button_resp.show_tooltip_ui(|ui| {
let confirm_resp = ui.button(tr!("Confirm", "Button label to confirm an action")); let confirm_resp = ui.button(tr!(
self.i18n,
"Confirm",
"Button label to confirm an action"
));
if confirm_resp.clicked() { if confirm_resp.clicked() {
confirm_pressed = true; confirm_pressed = true;
} }
if confirm_resp.clicked() if confirm_resp.clicked()
|| ui || ui
.button(tr!("Cancel", "Button label to cancel an action")) .button(tr!(self.i18n, "Cancel", "Button label to cancel an action"))
.clicked() .clicked()
{ {
ui.data_mut(|d| d.insert_temp(id, false)); ui.data_mut(|d| d.insert_temp(id, false));
@@ -211,8 +218,11 @@ impl<'a> NavTitle<'a> {
} }
confirm_pressed confirm_pressed
} else { } else {
delete_button_resp delete_button_resp.on_hover_text(tr!(
.on_hover_text(tr!("Delete this column", "Tooltip for deleting a column")); self.i18n,
"Delete this column",
"Tooltip for deleting a column"
));
false false
} }
} }
@@ -227,6 +237,7 @@ impl<'a> NavTitle<'a> {
// showing the hover text while showing the move tooltip causes some weird visuals // showing the hover text while showing the move tooltip causes some weird visuals
if ui.data(|d| d.get_temp::<bool>(cur_id).is_none()) { if ui.data(|d| d.get_temp::<bool>(cur_id).is_none()) {
move_resp = move_resp.on_hover_text(tr!( move_resp = move_resp.on_hover_text(tr!(
self.i18n,
"Moves this column to another position", "Moves this column to another position",
"Tooltip for moving a column" "Tooltip for moving a column"
)); ));
@@ -522,8 +533,8 @@ impl<'a> NavTitle<'a> {
.selectable(false) .selectable(false)
} }
fn title_label(&self, ui: &mut egui::Ui, top: &Route) { fn title_label(&mut self, ui: &mut egui::Ui, top: &Route) {
let column_title = top.title(); let column_title = top.title(self.i18n);
match &column_title { match &column_title {
ColumnTitle::Simple(title) => ui.add(Self::title_label_value(title)), ColumnTitle::Simple(title) => ui.add(Self::title_label_value(title)),

View File

@@ -1,6 +1,6 @@
use crate::{app_style::deck_icon_font_sized, deck_state::DeckState}; use crate::{app_style::deck_icon_font_sized, deck_state::DeckState};
use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget}; use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget};
use notedeck::tr; use notedeck::{tr, Localization};
use notedeck::{NamedFontFamily, NotedeckTextStyle}; use notedeck::{NamedFontFamily, NotedeckTextStyle};
use notedeck_ui::{ use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
@@ -11,6 +11,7 @@ use notedeck_ui::{
pub struct ConfigureDeckView<'a> { pub struct ConfigureDeckView<'a> {
state: &'a mut DeckState, state: &'a mut DeckState,
create_button_text: String, create_button_text: String,
pub i18n: &'a mut Localization,
} }
pub struct ConfigureDeckResponse { pub struct ConfigureDeckResponse {
@@ -19,10 +20,11 @@ pub struct ConfigureDeckResponse {
} }
impl<'a> ConfigureDeckView<'a> { impl<'a> ConfigureDeckView<'a> {
pub fn new(state: &'a mut DeckState) -> Self { pub fn new(state: &'a mut DeckState, i18n: &'a mut Localization) -> Self {
Self { Self {
state, state,
create_button_text: tr!("Create Deck", "Button label to create a new deck"), create_button_text: tr!(i18n, "Create Deck", "Button label to create a new deck"),
i18n,
} }
} }
@@ -38,14 +40,19 @@ impl<'a> ConfigureDeckView<'a> {
); );
padding(16.0, ui, |ui| { padding(16.0, ui, |ui| {
ui.add(Label::new( ui.add(Label::new(
RichText::new(tr!("Deck name", "Label for deck name input field")) RichText::new(tr!(
.font(title_font.clone()), self.i18n,
"Deck name",
"Label for deck name input field"
))
.font(title_font.clone()),
)); ));
ui.add_space(8.0); ui.add_space(8.0);
ui.text_edit_singleline(&mut self.state.deck_name); ui.text_edit_singleline(&mut self.state.deck_name);
ui.add_space(8.0); ui.add_space(8.0);
ui.add(Label::new( ui.add(Label::new(
RichText::new(tr!( RichText::new(tr!(
self.i18n,
"We recommend short names", "We recommend short names",
"Hint for deck name input field" "Hint for deck name input field"
)) ))
@@ -58,7 +65,8 @@ impl<'a> ConfigureDeckView<'a> {
ui.add_space(32.0); ui.add_space(32.0);
ui.add(Label::new( ui.add(Label::new(
RichText::new(tr!("Icon", "Label for deck icon selection")).font(title_font), RichText::new(tr!(self.i18n, "Icon", "Label for deck icon selection"))
.font(title_font),
)); ));
if ui if ui
@@ -97,7 +105,12 @@ impl<'a> ConfigureDeckView<'a> {
self.state.warn_no_title = false; self.state.warn_no_title = false;
} }
show_warnings(ui, self.state.warn_no_icon, self.state.warn_no_title); show_warnings(
ui,
self.i18n,
self.state.warn_no_icon,
self.state.warn_no_title,
);
let mut resp = None; let mut resp = None;
if ui if ui
@@ -125,19 +138,22 @@ impl<'a> ConfigureDeckView<'a> {
} }
} }
fn show_warnings(ui: &mut Ui, warn_no_icon: bool, warn_no_title: bool) { fn show_warnings(ui: &mut Ui, i18n: &mut Localization, warn_no_icon: bool, warn_no_title: bool) {
let warning = if warn_no_title && warn_no_icon { let warning = if warn_no_title && warn_no_icon {
tr!( tr!(
i18n,
"Please create a name for the deck and select an icon.", "Please create a name for the deck and select an icon.",
"Error message for missing deck name and icon" "Error message for missing deck name and icon"
) )
} else if warn_no_title { } else if warn_no_title {
tr!( tr!(
i18n,
"Please create a name for the deck.", "Please create a name for the deck.",
"Error message for missing deck name" "Error message for missing deck name"
) )
} else if warn_no_icon { } else if warn_no_icon {
tr!( tr!(
i18n,
"Please select an icon.", "Please select an icon.",
"Error message for missing deck icon" "Error message for missing deck icon"
) )
@@ -320,12 +336,8 @@ mod preview {
} }
impl App for ConfigureDeckPreview { impl App for ConfigureDeckPreview {
fn update( fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
&mut self, ConfigureDeckView::new(&mut self.state, ctx.i18n).ui(ui);
_app_ctx: &mut AppContext<'_>,
ui: &mut egui::Ui,
) -> Option<AppAction> {
ConfigureDeckView::new(&mut self.state).ui(ui);
None None
} }

View File

@@ -3,7 +3,7 @@ use egui::Widget;
use crate::deck_state::DeckState; use crate::deck_state::DeckState;
use super::configure_deck::{ConfigureDeckResponse, ConfigureDeckView}; use super::configure_deck::{ConfigureDeckResponse, ConfigureDeckView};
use notedeck::tr; use notedeck::{tr, Localization};
use notedeck_ui::padding; use notedeck_ui::padding;
pub struct EditDeckView<'a> { pub struct EditDeckView<'a> {
@@ -16,9 +16,9 @@ pub enum EditDeckResponse {
} }
impl<'a> EditDeckView<'a> { impl<'a> EditDeckView<'a> {
pub fn new(state: &'a mut DeckState) -> Self { pub fn new(state: &'a mut DeckState, i18n: &'a mut Localization) -> Self {
let config_view = ConfigureDeckView::new(state) let txt = tr!(i18n, "Edit Deck", "Button label to edit a deck");
.with_create_text(tr!("Edit Deck", "Button label to edit a deck")); let config_view = ConfigureDeckView::new(state, i18n).with_create_text(txt);
Self { config_view } Self { config_view }
} }
@@ -26,7 +26,7 @@ impl<'a> EditDeckView<'a> {
let mut edit_deck_resp = None; let mut edit_deck_resp = None;
padding(egui::Margin::symmetric(16, 4), ui, |ui| { padding(egui::Margin::symmetric(16, 4), ui, |ui| {
if ui.add(delete_button()).clicked() { if ui.add(delete_button(self.config_view.i18n)).clicked() {
edit_deck_resp = Some(EditDeckResponse::Delete); edit_deck_resp = Some(EditDeckResponse::Delete);
} }
}); });
@@ -39,12 +39,12 @@ impl<'a> EditDeckView<'a> {
} }
} }
fn delete_button() -> impl Widget { fn delete_button<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
|ui: &mut egui::Ui| { |ui: &mut egui::Ui| {
let size = egui::vec2(108.0, 40.0); let size = egui::vec2(108.0, 40.0);
ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Center), |ui| { ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Center), |ui| {
ui.add( ui.add(
egui::Button::new(tr!("Delete Deck", "Button label to delete a deck")) egui::Button::new(tr!(i18n, "Delete Deck", "Button label to delete a deck"))
.fill(ui.visuals().error_fg_color) .fill(ui.visuals().error_fg_color)
.min_size(size), .min_size(size),
) )
@@ -75,12 +75,8 @@ mod preview {
} }
impl App for EditDeckPreview { impl App for EditDeckPreview {
fn update( fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
&mut self, EditDeckView::new(&mut self.state, ctx.i18n).ui(ui);
_app_ctx: &mut AppContext<'_>,
ui: &mut egui::Ui,
) -> Option<AppAction> {
EditDeckView::new(&mut self.state).ui(ui);
None None
} }
} }

View File

@@ -1,5 +1,3 @@
use std::fmt::Display;
use egui::{ use egui::{
emath::GuiRounding, pos2, vec2, Color32, CornerRadius, FontId, Frame, Label, Layout, Slider, emath::GuiRounding, pos2, vec2, Color32, CornerRadius, FontId, Frame, Label, Layout, Slider,
Stroke, Stroke,
@@ -7,7 +5,8 @@ use egui::{
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{Ndb, ProfileRecord, Transaction}; use nostrdb::{Ndb, ProfileRecord, Transaction};
use notedeck::{ use notedeck::{
fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, NotedeckTextStyle, fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, Localization,
NotedeckTextStyle,
}; };
use notedeck_ui::{ use notedeck_ui::{
app_images, colors, profile::display_name_widget, widgets::styled_button_toggleable, app_images, colors, profile::display_name_widget, widgets::styled_button_toggleable,
@@ -20,11 +19,13 @@ pub struct CustomZapView<'a> {
txn: &'a Transaction, txn: &'a Transaction,
target_pubkey: &'a Pubkey, target_pubkey: &'a Pubkey,
default_msats: u64, default_msats: u64,
i18n: &'a mut Localization,
} }
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
impl<'a> CustomZapView<'a> { impl<'a> CustomZapView<'a> {
pub fn new( pub fn new(
i18n: &'a mut Localization,
images: &'a mut Images, images: &'a mut Images,
ndb: &'a Ndb, ndb: &'a Ndb,
txn: &'a Transaction, txn: &'a Transaction,
@@ -37,6 +38,7 @@ impl<'a> CustomZapView<'a> {
ndb, ndb,
txn, txn,
default_msats, default_msats,
i18n,
} }
} }
@@ -48,7 +50,7 @@ impl<'a> CustomZapView<'a> {
} }
fn ui_internal(&mut self, ui: &mut egui::Ui) -> Option<u64> { fn ui_internal(&mut self, ui: &mut egui::Ui) -> Option<u64> {
show_title(ui); show_title(ui, self.i18n);
ui.add_space(16.0); ui.add_space(16.0);
@@ -82,7 +84,7 @@ impl<'a> CustomZapView<'a> {
} else { } else {
(self.default_msats / 1000).to_string() (self.default_msats / 1000).to_string()
}; };
show_amount(ui, id, &mut cur_amount, slider_width); show_amount(ui, self.i18n, id, &mut cur_amount, slider_width);
let mut maybe_sats = cur_amount.parse::<u64>().ok(); let mut maybe_sats = cur_amount.parse::<u64>().ok();
let prev_slider_sats = maybe_sats.unwrap_or(default_sats).clamp(1, 100000); let prev_slider_sats = maybe_sats.unwrap_or(default_sats).clamp(1, 100000);
@@ -102,7 +104,7 @@ impl<'a> CustomZapView<'a> {
maybe_sats = Some(slider_sats); maybe_sats = Some(slider_sats);
} }
if let Some(selection) = show_selection_buttons(ui, maybe_sats) { if let Some(selection) = show_selection_buttons(ui, maybe_sats, self.i18n) {
cur_amount = selection.to_string(); cur_amount = selection.to_string();
maybe_sats = Some(selection); maybe_sats = Some(selection);
} }
@@ -110,7 +112,7 @@ impl<'a> CustomZapView<'a> {
ui.data_mut(|d| d.insert_temp(id, cur_amount)); ui.data_mut(|d| d.insert_temp(id, cur_amount));
let resp = ui.add(styled_button_toggleable( let resp = ui.add(styled_button_toggleable(
&tr!("Send", "Button label to send a zap"), &tr!(self.i18n, "Send", "Button label to send a zap"),
colors::PINK, colors::PINK,
is_valid_zap(maybe_sats), is_valid_zap(maybe_sats),
)); ));
@@ -129,7 +131,7 @@ fn is_valid_zap(amount: Option<u64>) -> bool {
amount.is_some_and(|sats| sats > 0) amount.is_some_and(|sats| sats > 0)
} }
fn show_title(ui: &mut egui::Ui) { fn show_title(ui: &mut egui::Ui, i18n: &mut Localization) {
let max_size = 32.0; let max_size = 32.0;
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
vec2(ui.available_width(), max_size), vec2(ui.available_width(), max_size),
@@ -158,7 +160,7 @@ fn show_title(ui: &mut egui::Ui) {
ui.add_space(8.0); ui.add_space(8.0);
ui.add(egui::Label::new( ui.add(egui::Label::new(
egui::RichText::new(tr!("Zap", "Heading for zap (tip) action")) egui::RichText::new(tr!(i18n, "Zap", "Heading for zap (tip) action"))
.text_style(NotedeckTextStyle::Heading2.text_style()), .text_style(NotedeckTextStyle::Heading2.text_style()),
)); ));
}, },
@@ -177,7 +179,13 @@ fn show_profile(ui: &mut egui::Ui, images: &mut Images, profile: Option<&Profile
); );
} }
fn show_amount(ui: &mut egui::Ui, id: egui::Id, user_input: &mut String, width: f32) { fn show_amount(
ui: &mut egui::Ui,
i18n: &mut Localization,
id: egui::Id,
user_input: &mut String,
width: f32,
) {
let user_input_font = NotedeckTextStyle::Heading.get_bolded_font(ui.ctx()); let user_input_font = NotedeckTextStyle::Heading.get_bolded_font(ui.ctx());
let user_input_id = id.with("sats_amount"); let user_input_id = id.with("sats_amount");
@@ -192,6 +200,7 @@ fn show_amount(ui: &mut egui::Ui, id: egui::Id, user_input: &mut String, width:
let sats_galley = painter.layout_no_wrap( let sats_galley = painter.layout_no_wrap(
tr!( tr!(
i18n,
"SATS", "SATS",
"Label for satoshis (Bitcoin unit) for custom zap amount input field" "Label for satoshis (Bitcoin unit) for custom zap amount input field"
), ),
@@ -219,7 +228,7 @@ fn show_amount(ui: &mut egui::Ui, id: egui::Id, user_input: &mut String, width:
.font(user_input_font); .font(user_input_font);
let amount_resp = ui.add(Label::new( let amount_resp = ui.add(Label::new(
egui::RichText::new(tr!("Amount", "Label for zap amount input field")) egui::RichText::new(tr!(i18n, "Amount", "Label for zap amount input field"))
.text_style(NotedeckTextStyle::Heading3.text_style()) .text_style(NotedeckTextStyle::Heading3.text_style())
.color(ui.visuals().noninteractive().text_color()), .color(ui.visuals().noninteractive().text_color()),
)); ));
@@ -300,7 +309,11 @@ const SELECTION_BUTTONS: [ZapSelectionButton; 8] = [
ZapSelectionButton::Eighth, ZapSelectionButton::Eighth,
]; ];
fn show_selection_buttons(ui: &mut egui::Ui, sats_selection: Option<u64>) -> Option<u64> { fn show_selection_buttons(
ui: &mut egui::Ui,
sats_selection: Option<u64>,
i18n: &mut Localization,
) -> Option<u64> {
let mut our_selection = None; let mut our_selection = None;
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
vec2(224.0, 116.0), vec2(224.0, 116.0),
@@ -309,7 +322,8 @@ fn show_selection_buttons(ui: &mut egui::Ui, sats_selection: Option<u64>) -> Opt
ui.spacing_mut().item_spacing = vec2(8.0, 8.0); ui.spacing_mut().item_spacing = vec2(8.0, 8.0);
for button in SELECTION_BUTTONS { for button in SELECTION_BUTTONS {
our_selection = our_selection.or(show_selection_button(ui, sats_selection, button)); our_selection =
our_selection.or(show_selection_button(ui, sats_selection, button, i18n));
} }
}, },
); );
@@ -321,6 +335,7 @@ fn show_selection_button(
ui: &mut egui::Ui, ui: &mut egui::Ui,
sats_selection: Option<u64>, sats_selection: Option<u64>,
button: ZapSelectionButton, button: ZapSelectionButton,
i18n: &mut Localization,
) -> Option<u64> { ) -> Option<u64> {
let (rect, _) = ui.allocate_exact_size(vec2(50.0, 50.0), egui::Sense::click()); let (rect, _) = ui.allocate_exact_size(vec2(50.0, 50.0), egui::Sense::click());
let helper = AnimationHelper::new_from_rect(ui, ("zap_selection_button", &button), rect); let helper = AnimationHelper::new_from_rect(ui, ("zap_selection_button", &button), rect);
@@ -353,7 +368,11 @@ fn show_selection_button(
NotedeckTextStyle::Body.font_family(), NotedeckTextStyle::Body.font_family(),
); );
let galley = painter.layout_no_wrap(button.to_string(), fontid, ui.visuals().text_color()); let galley = painter.layout_no_wrap(
button.to_desc_string(i18n),
fontid,
ui.visuals().text_color(),
);
let text_rect = { let text_rect = {
let mut galley_rect = galley.rect; let mut galley_rect = galley.rect;
galley_rect.set_center(rect.center()); galley_rect.set_center(rect.center());
@@ -394,19 +413,17 @@ impl ZapSelectionButton {
ZapSelectionButton::Eighth => 100_000, ZapSelectionButton::Eighth => 100_000,
} }
} }
}
impl Display for ZapSelectionButton { pub fn to_desc_string(&self, i18n: &mut Localization) -> String {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
ZapSelectionButton::First => write!(f, "69"), ZapSelectionButton::First => "69".to_string(),
ZapSelectionButton::Second => write!(f, "100"), ZapSelectionButton::Second => "100".to_string(),
ZapSelectionButton::Third => write!(f, "420"), ZapSelectionButton::Third => "420".to_string(),
ZapSelectionButton::Fourth => write!(f, "{}", tr!("5K", "Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.")), ZapSelectionButton::Fourth => tr!(i18n, "5K", "Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount."),
ZapSelectionButton::Fifth => write!(f, "{}", tr!("10K", "Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.")), ZapSelectionButton::Fifth => tr!(i18n, "10K", "Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount."),
ZapSelectionButton::Sixth => write!(f, "{}", tr!("20K", "Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.")), ZapSelectionButton::Sixth => tr!(i18n, "20K", "Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount."),
ZapSelectionButton::Seventh => write!(f, "{}", tr!("50K", "Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.")), ZapSelectionButton::Seventh => tr!(i18n, "50K", "Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount."),
ZapSelectionButton::Eighth => write!(f, "{}", tr!("100K", "Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.")), ZapSelectionButton::Eighth => tr!(i18n, "100K", "Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount."),
} }
} }
} }

View File

@@ -25,7 +25,9 @@ use notedeck_ui::{
NoteOptions, ProfilePic, NoteOptions, ProfilePic,
}; };
use notedeck::{name::get_display_name, supported_mime_hosted_at_url, tr, NoteAction, NoteContext}; use notedeck::{
name::get_display_name, supported_mime_hosted_at_url, tr, Localization, NoteAction, NoteContext,
};
use tracing::error; use tracing::error;
pub struct PostView<'a, 'd> { pub struct PostView<'a, 'd> {
@@ -182,6 +184,7 @@ impl<'a, 'd> PostView<'a, 'd> {
let textedit = TextEdit::multiline(&mut self.draft.buffer) let textedit = TextEdit::multiline(&mut self.draft.buffer)
.hint_text( .hint_text(
egui::RichText::new(tr!( egui::RichText::new(tr!(
self.note_context.i18n,
"Write a banger note here...", "Write a banger note here...",
"Placeholder for note input field" "Placeholder for note input field"
)) ))
@@ -411,7 +414,10 @@ impl<'a, 'd> PostView<'a, 'd> {
ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| {
let post_button_clicked = ui let post_button_clicked = ui
.add_sized([91.0, 32.0], post_button(!self.draft.buffer.is_empty())) .add_sized(
[91.0, 32.0],
post_button(self.note_context.i18n, !self.draft.buffer.is_empty()),
)
.clicked(); .clicked();
let shortcut_pressed = ui.input(|i| { let shortcut_pressed = ui.input(|i| {
@@ -609,9 +615,9 @@ fn render_post_view_media(
} }
} }
fn post_button(interactive: bool) -> impl egui::Widget { fn post_button<'a>(i18n: &'a mut Localization, interactive: bool) -> impl egui::Widget + 'a {
move |ui: &mut egui::Ui| { move |ui: &mut egui::Ui| {
let button = egui::Button::new(tr!("Post now", "Button label to post a note")); let button = egui::Button::new(tr!(i18n, "Post now", "Button label to post a note"));
if interactive { if interactive {
ui.add(button) ui.add(button)
} else { } else {
@@ -804,6 +810,7 @@ mod preview {
unknown_ids: app.unknown_ids, unknown_ids: app.unknown_ids,
current_account_has_wallet: false, current_account_has_wallet: false,
clipboard: app.clipboard, clipboard: app.clipboard,
i18n: app.i18n,
}; };
PostView::new( PostView::new(

View File

@@ -2,17 +2,26 @@ use core::f32;
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit}; use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
use enostr::ProfileState; use enostr::ProfileState;
use notedeck::{profile::unwrap_profile_url, tr, Images, NotedeckTextStyle}; use notedeck::{profile::unwrap_profile_url, tr, Images, Localization, NotedeckTextStyle};
use notedeck_ui::{profile::banner, ProfilePic}; use notedeck_ui::{profile::banner, ProfilePic};
pub struct EditProfileView<'a> { pub struct EditProfileView<'a> {
state: &'a mut ProfileState, state: &'a mut ProfileState,
img_cache: &'a mut Images, img_cache: &'a mut Images,
i18n: &'a mut Localization,
} }
impl<'a> EditProfileView<'a> { impl<'a> EditProfileView<'a> {
pub fn new(state: &'a mut ProfileState, img_cache: &'a mut Images) -> Self { pub fn new(
Self { state, img_cache } i18n: &'a mut Localization,
state: &'a mut ProfileState,
img_cache: &'a mut Images,
) -> Self {
Self {
i18n,
state,
img_cache,
}
} }
// return true to save // return true to save
@@ -34,8 +43,12 @@ impl<'a> EditProfileView<'a> {
if ui if ui
.add( .add(
button( button(
tr!("Save changes", "Button label to save profile changes") tr!(
.as_str(), self.i18n,
"Save changes",
"Button label to save profile changes"
)
.as_str(),
119.0, 119.0,
) )
.fill(notedeck_ui::colors::PINK), .fill(notedeck_ui::colors::PINK),
@@ -70,42 +83,52 @@ impl<'a> EditProfileView<'a> {
in_frame(ui, |ui| { in_frame(ui, |ui| {
ui.add(label( ui.add(label(
tr!("Display name", "Profile display name field label").as_str(), tr!(
self.i18n,
"Display name",
"Profile display name field label"
)
.as_str(),
)); ));
ui.add(singleline_textedit(self.state.str_mut("display_name"))); ui.add(singleline_textedit(self.state.str_mut("display_name")));
}); });
in_frame(ui, |ui| { in_frame(ui, |ui| {
ui.add(label( ui.add(label(
tr!("Username", "Profile username field label").as_str(), tr!(self.i18n, "Username", "Profile username field label").as_str(),
)); ));
ui.add(singleline_textedit(self.state.str_mut("name"))); ui.add(singleline_textedit(self.state.str_mut("name")));
}); });
in_frame(ui, |ui| { in_frame(ui, |ui| {
ui.add(label( ui.add(label(
tr!("Profile picture", "Profile picture URL field label").as_str(), tr!(
self.i18n,
"Profile picture",
"Profile picture URL field label"
)
.as_str(),
)); ));
ui.add(multiline_textedit(self.state.str_mut("picture"))); ui.add(multiline_textedit(self.state.str_mut("picture")));
}); });
in_frame(ui, |ui| { in_frame(ui, |ui| {
ui.add(label( ui.add(label(
tr!("Banner", "Profile banner URL field label").as_str(), tr!(self.i18n, "Banner", "Profile banner URL field label").as_str(),
)); ));
ui.add(multiline_textedit(self.state.str_mut("banner"))); ui.add(multiline_textedit(self.state.str_mut("banner")));
}); });
in_frame(ui, |ui| { in_frame(ui, |ui| {
ui.add(label( ui.add(label(
tr!("About", "Profile about/bio field label").as_str(), tr!(self.i18n, "About", "Profile about/bio field label").as_str(),
)); ));
ui.add(multiline_textedit(self.state.str_mut("about"))); ui.add(multiline_textedit(self.state.str_mut("about")));
}); });
in_frame(ui, |ui| { in_frame(ui, |ui| {
ui.add(label( ui.add(label(
tr!("Website", "Profile website field label").as_str(), tr!(self.i18n, "Website", "Profile website field label").as_str(),
)); ));
ui.add(singleline_textedit(self.state.str_mut("website"))); ui.add(singleline_textedit(self.state.str_mut("website")));
}); });
@@ -113,6 +136,7 @@ impl<'a> EditProfileView<'a> {
in_frame(ui, |ui| { in_frame(ui, |ui| {
ui.add(label( ui.add(label(
tr!( tr!(
self.i18n,
"Lightning network address (lud16)", "Lightning network address (lud16)",
"Bitcoin Lightning network address field label" "Bitcoin Lightning network address field label"
) )
@@ -124,6 +148,7 @@ impl<'a> EditProfileView<'a> {
in_frame(ui, |ui| { in_frame(ui, |ui| {
ui.add(label( ui.add(label(
tr!( tr!(
self.i18n,
"Nostr address (NIP-05 identity)", "Nostr address (NIP-05 identity)",
"NIP-05 identity field label" "NIP-05 identity field label"
) )
@@ -153,12 +178,14 @@ impl<'a> EditProfileView<'a> {
ui.visuals().noninteractive().fg_stroke.color, ui.visuals().noninteractive().fg_stroke.color,
RichText::new(if use_domain { RichText::new(if use_domain {
tr!( tr!(
self.i18n,
"\"{domain}\" will be used for identification", "\"{domain}\" will be used for identification",
"Domain identification message", "Domain identification message",
domain = suffix domain = suffix
) )
} else { } else {
tr!( tr!(
self.i18n,
"\"{username}\" at \"{domain}\" will be used for identification", "\"{username}\" at \"{domain}\" will be used for identification",
"Username and domain identification message", "Username and domain identification message",
username = prefix, username = prefix,

View File

@@ -4,7 +4,7 @@ pub use edit::EditProfileView;
use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke}; use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke};
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{ProfileRecord, Transaction}; use nostrdb::{ProfileRecord, Transaction};
use notedeck::tr; use notedeck::{tr, Localization};
use notedeck_ui::profile::follow_button; use notedeck_ui::profile::follow_button;
use tracing::error; use tracing::error;
@@ -91,8 +91,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
) )
.get_ptr(); .get_ptr();
profile_timeline.selected_view = profile_timeline.selected_view = tabs_ui(
tabs_ui(ui, profile_timeline.selected_view, &profile_timeline.views); ui,
self.note_context.i18n,
profile_timeline.selected_view,
&profile_timeline.views,
);
let reversed = false; let reversed = false;
// poll for new notes and insert them into our existing notes // poll for new notes and insert them into our existing notes
@@ -184,7 +188,10 @@ impl<'a, 'd> ProfileView<'a, 'd> {
match profile_type { match profile_type {
ProfileType::MyProfile => { ProfileType::MyProfile => {
if ui.add(edit_profile_button()).clicked() { if ui
.add(edit_profile_button(self.note_context.i18n))
.clicked()
{
action = Some(ProfileViewAction::EditProfile); action = Some(ProfileViewAction::EditProfile);
} }
} }
@@ -334,7 +341,7 @@ fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ {
} }
} }
fn edit_profile_button() -> impl egui::Widget + 'static { fn edit_profile_button<'a>(i18n: &'a mut Localization) -> impl egui::Widget + 'a {
|ui: &mut egui::Ui| -> egui::Response { |ui: &mut egui::Ui| -> egui::Response {
let (rect, resp) = ui.allocate_exact_size(vec2(124.0, 32.0), Sense::click()); let (rect, resp) = ui.allocate_exact_size(vec2(124.0, 32.0), Sense::click());
let painter = ui.painter_at(rect); let painter = ui.painter_at(rect);
@@ -363,7 +370,7 @@ fn edit_profile_button() -> impl egui::Widget + 'static {
let edit_icon_size = vec2(16.0, 16.0); let edit_icon_size = vec2(16.0, 16.0);
let galley = painter.layout( let galley = painter.layout(
tr!("Edit Profile", "Button label to edit user profile"), tr!(i18n, "Edit Profile", "Button label to edit user profile"),
NotedeckTextStyle::Button.get_font_id(ui.ctx()), NotedeckTextStyle::Button.get_font_id(ui.ctx()),
ui.visuals().text_color(), ui.visuals().text_color(),
rect.width(), rect.width(),

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use crate::ui::{Preview, PreviewConfig}; use crate::ui::{Preview, PreviewConfig};
use egui::{Align, Button, CornerRadius, Frame, Id, Layout, Margin, Rgba, RichText, Ui, Vec2}; use egui::{Align, Button, CornerRadius, Frame, Id, Layout, Margin, Rgba, RichText, Ui, Vec2};
use enostr::{RelayPool, RelayStatus}; use enostr::{RelayPool, RelayStatus};
use notedeck::{tr, NotedeckTextStyle, RelayAction}; use notedeck::{tr, Localization, NotedeckTextStyle, RelayAction};
use notedeck_ui::app_images; use notedeck_ui::app_images;
use notedeck_ui::{colors::PINK, padding}; use notedeck_ui::{colors::PINK, padding};
use tracing::debug; use tracing::debug;
@@ -13,6 +13,7 @@ use super::widgets::styled_button;
pub struct RelayView<'a> { pub struct RelayView<'a> {
pool: &'a RelayPool, pool: &'a RelayPool,
id_string_map: &'a mut HashMap<Id, String>, id_string_map: &'a mut HashMap<Id, String>,
i18n: &'a mut Localization,
} }
impl RelayView<'_> { impl RelayView<'_> {
@@ -26,7 +27,7 @@ impl RelayView<'_> {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| { ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
ui.label( ui.label(
RichText::new(tr!("Relays", "Label for relay list section")) RichText::new(tr!(self.i18n, "Relays", "Label for relay list section"))
.text_style(NotedeckTextStyle::Heading2.text_style()), .text_style(NotedeckTextStyle::Heading2.text_style()),
); );
}); });
@@ -53,10 +54,15 @@ impl RelayView<'_> {
} }
impl<'a> RelayView<'a> { impl<'a> RelayView<'a> {
pub fn new(pool: &'a RelayPool, id_string_map: &'a mut HashMap<Id, String>) -> Self { pub fn new(
pool: &'a RelayPool,
id_string_map: &'a mut HashMap<Id, String>,
i18n: &'a mut Localization,
) -> Self {
RelayView { RelayView {
pool, pool,
id_string_map, id_string_map,
i18n,
} }
} }
@@ -65,7 +71,7 @@ impl<'a> RelayView<'a> {
} }
/// Show the current relays and return a relay the user selected to delete /// Show the current relays and return a relay the user selected to delete
fn show_relays(&'a self, ui: &mut Ui) -> Option<String> { fn show_relays(&mut self, ui: &mut Ui) -> Option<String> {
let mut relay_to_remove = None; let mut relay_to_remove = None;
for (index, relay_info) in get_relay_infos(self.pool).iter().enumerate() { for (index, relay_info) in get_relay_infos(self.pool).iter().enumerate() {
ui.add_space(8.0); ui.add_space(8.0);
@@ -107,7 +113,7 @@ impl<'a> RelayView<'a> {
relay_to_remove = Some(relay_info.relay_url.to_string()); relay_to_remove = Some(relay_info.relay_url.to_string());
}; };
show_connection_status(ui, relay_info.status); show_connection_status(ui, self.i18n, relay_info.status);
}); });
}); });
}); });
@@ -123,7 +129,7 @@ impl<'a> RelayView<'a> {
match self.id_string_map.get(&id) { match self.id_string_map.get(&id) {
None => { None => {
ui.with_layout(Layout::top_down(Align::Min), |ui| { ui.with_layout(Layout::top_down(Align::Min), |ui| {
let relay_button = add_relay_button(); let relay_button = add_relay_button(self.i18n);
if ui.add(relay_button).clicked() { if ui.add(relay_button).clicked() {
debug!("add relay clicked"); debug!("add relay clicked");
self.id_string_map self.id_string_map
@@ -151,6 +157,7 @@ impl<'a> RelayView<'a> {
let text_edit = egui::TextEdit::singleline(text_buffer) let text_edit = egui::TextEdit::singleline(text_buffer)
.hint_text( .hint_text(
RichText::new(tr!( RichText::new(tr!(
self.i18n,
"Enter the relay here", "Enter the relay here",
"Placeholder for relay input field" "Placeholder for relay input field"
)) ))
@@ -163,7 +170,10 @@ impl<'a> RelayView<'a> {
ui.add(text_edit); ui.add(text_edit);
ui.add_space(8.0); ui.add_space(8.0);
if ui if ui
.add_sized(egui::vec2(50.0, 40.0), add_relay_button2(is_enabled)) .add_sized(
egui::vec2(50.0, 40.0),
add_relay_button2(self.i18n, is_enabled),
)
.clicked() .clicked()
{ {
self.id_string_map.remove(&id) // remove and return the value self.id_string_map.remove(&id) // remove and return the value
@@ -175,10 +185,10 @@ impl<'a> RelayView<'a> {
} }
} }
fn add_relay_button() -> Button<'static> { fn add_relay_button(i18n: &mut Localization) -> Button<'static> {
Button::image_and_text( Button::image_and_text(
app_images::add_relay_image().fit_to_exact_size(Vec2::new(48.0, 48.0)), app_images::add_relay_image().fit_to_exact_size(Vec2::new(48.0, 48.0)),
RichText::new(tr!("Add relay", "Button label to add a relay")) RichText::new(tr!(i18n, "Add relay", "Button label to add a relay"))
.size(16.0) .size(16.0)
// TODO: this color should not be hard coded. Find some way to add it to the visuals // TODO: this color should not be hard coded. Find some way to add it to the visuals
.color(PINK), .color(PINK),
@@ -186,9 +196,9 @@ fn add_relay_button() -> Button<'static> {
.frame(false) .frame(false)
} }
fn add_relay_button2(is_enabled: bool) -> impl egui::Widget + 'static { fn add_relay_button2<'a>(i18n: &'a mut Localization, is_enabled: bool) -> impl egui::Widget + 'a {
move |ui: &mut egui::Ui| -> egui::Response { move |ui: &mut egui::Ui| -> egui::Response {
let add_text = tr!("Add", "Button label to add a relay"); let add_text = tr!(i18n, "Add", "Button label to add a relay");
let button_widget = styled_button(add_text.as_str(), notedeck_ui::colors::PINK); let button_widget = styled_button(add_text.as_str(), notedeck_ui::colors::PINK);
ui.add_enabled(is_enabled, button_widget) ui.add_enabled(is_enabled, button_widget)
} }
@@ -219,7 +229,7 @@ fn relay_frame(ui: &mut Ui) -> Frame {
.stroke(ui.style().visuals.noninteractive().bg_stroke) .stroke(ui.style().visuals.noninteractive().bg_stroke)
} }
fn show_connection_status(ui: &mut Ui, status: RelayStatus) { fn show_connection_status(ui: &mut Ui, i18n: &mut Localization, status: RelayStatus) {
let fg_color = match status { let fg_color = match status {
RelayStatus::Connected => ui.visuals().selection.bg_fill, RelayStatus::Connected => ui.visuals().selection.bg_fill,
RelayStatus::Connecting => ui.visuals().warn_fg_color, RelayStatus::Connecting => ui.visuals().warn_fg_color,
@@ -228,9 +238,11 @@ fn show_connection_status(ui: &mut Ui, status: RelayStatus) {
let bg_color = egui::lerp(Rgba::from(fg_color)..=Rgba::BLACK, 0.8).into(); let bg_color = egui::lerp(Rgba::from(fg_color)..=Rgba::BLACK, 0.8).into();
let label_text = match status { let label_text = match status {
RelayStatus::Connected => tr!("Connected", "Status label for connected relay"), RelayStatus::Connected => tr!(i18n, "Connected", "Status label for connected relay"),
RelayStatus::Connecting => tr!("Connecting...", "Status label for connecting relay"), RelayStatus::Connecting => tr!(i18n, "Connecting...", "Status label for connecting relay"),
RelayStatus::Disconnected => tr!("Not Connected", "Status label for disconnected relay"), RelayStatus::Disconnected => {
tr!(i18n, "Not Connected", "Status label for disconnected relay")
}
}; };
let frame = Frame::new() let frame = Frame::new()
@@ -290,7 +302,7 @@ mod preview {
fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
self.pool.try_recv(); self.pool.try_recv();
let mut id_string_map = HashMap::new(); let mut id_string_map = HashMap::new();
RelayView::new(app.pool, &mut id_string_map).ui(ui); RelayView::new(app.pool, &mut id_string_map, app.i18n).ui(ui);
None None
} }
} }

View File

@@ -5,7 +5,7 @@ use state::TypingType;
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView}; use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
use egui_winit::clipboard::Clipboard; use egui_winit::clipboard::Clipboard;
use nostrdb::{Filter, Ndb, Transaction}; use nostrdb::{Filter, Ndb, Transaction};
use notedeck::{tr, tr_plural, NoteAction, NoteContext, NoteRef}; use notedeck::{tr, tr_plural, Localization, NoteAction, NoteContext, NoteRef};
use notedeck_ui::{ use notedeck_ui::{
context_menu::{input_context, PasteBehavior}, context_menu::{input_context, PasteBehavior},
icons::search_icon, icons::search_icon,
@@ -54,6 +54,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
ui.spacing_mut().item_spacing = egui::vec2(0.0, 12.0); ui.spacing_mut().item_spacing = egui::vec2(0.0, 12.0);
let search_resp = search_box( let search_resp = search_box(
self.note_context.i18n,
&mut self.query.string, &mut self.query.string,
self.query.focus_state.clone(), self.query.focus_state.clone(),
ui, ui,
@@ -120,6 +121,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
} }
SearchState::Searched => { SearchState::Searched => {
ui.label(tr_plural!( ui.label(tr_plural!(
self.note_context.i18n,
"Got {count} result for '{query}'", // one "Got {count} result for '{query}'", // one
"Got {count} results for '{query}'", // other "Got {count} results for '{query}'", // other
"Search results count", // comment "Search results count", // comment
@@ -130,6 +132,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
} }
SearchState::Typing(TypingType::AutoSearch) => { SearchState::Typing(TypingType::AutoSearch) => {
ui.label(tr!( ui.label(tr!(
self.note_context.i18n,
"Searching for '{query}'", "Searching for '{query}'",
"Search in progress message", "Search in progress message",
query = &self.query.string query = &self.query.string
@@ -247,6 +250,7 @@ impl SearchResponse {
} }
fn search_box( fn search_box(
i18n: &mut Localization,
input: &mut String, input: &mut String,
focus_state: FocusState, focus_state: FocusState,
ui: &mut egui::Ui, ui: &mut egui::Ui,
@@ -290,6 +294,7 @@ fn search_box(
TextEdit::singleline(input) TextEdit::singleline(input)
.hint_text( .hint_text(
RichText::new(tr!( RichText::new(tr!(
i18n,
"Search notes...", "Search notes...",
"Placeholder for search notes input field" "Placeholder for search notes input field"
)) ))

View File

@@ -12,7 +12,7 @@ use crate::{
route::Route, route::Route,
}; };
use notedeck::{tr, Accounts, UserAccount}; use notedeck::{tr, Accounts, Localization, UserAccount};
use notedeck_ui::{ use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
app_images, colors, View, app_images, colors, View,
@@ -26,6 +26,7 @@ static ICON_WIDTH: f32 = 40.0;
pub struct DesktopSidePanel<'a> { pub struct DesktopSidePanel<'a> {
selected_account: &'a UserAccount, selected_account: &'a UserAccount,
decks_cache: &'a DecksCache, decks_cache: &'a DecksCache,
i18n: &'a mut Localization,
} }
impl View for DesktopSidePanel<'_> { impl View for DesktopSidePanel<'_> {
@@ -58,10 +59,15 @@ impl SidePanelResponse {
} }
impl<'a> DesktopSidePanel<'a> { impl<'a> DesktopSidePanel<'a> {
pub fn new(selected_account: &'a UserAccount, decks_cache: &'a DecksCache) -> Self { pub fn new(
selected_account: &'a UserAccount,
decks_cache: &'a DecksCache,
i18n: &'a mut Localization,
) -> Self {
Self { Self {
selected_account, selected_account,
decks_cache, decks_cache,
i18n,
} }
} }
@@ -105,9 +111,13 @@ impl<'a> DesktopSidePanel<'a> {
ui.add_space(8.0); ui.add_space(8.0);
ui.add(egui::Label::new( ui.add(egui::Label::new(
RichText::new(tr!("DECKS", "Label for decks section in side panel")) RichText::new(tr!(
.size(11.0) self.i18n,
.color(ui.visuals().noninteractive().fg_stroke.color), "DECKS",
"Label for decks section in side panel"
))
.size(11.0)
.color(ui.visuals().noninteractive().fg_stroke.color),
)); ));
ui.add_space(8.0); ui.add_space(8.0);
let add_deck_resp = ui.add(add_deck_button()); let add_deck_resp = ui.add(add_deck_button());
@@ -175,8 +185,9 @@ impl<'a> DesktopSidePanel<'a> {
decks_cache: &mut DecksCache, decks_cache: &mut DecksCache,
accounts: &Accounts, accounts: &Accounts,
action: SidePanelAction, action: SidePanelAction,
i18n: &mut Localization,
) -> Option<SwitchingAction> { ) -> Option<SwitchingAction> {
let router = get_active_columns_mut(accounts, decks_cache).get_first_router(); let router = get_active_columns_mut(i18n, accounts, decks_cache).get_first_router();
let mut switching_response = None; let mut switching_response = None;
match action { match action {
/* /*
@@ -218,7 +229,7 @@ impl<'a> DesktopSidePanel<'a> {
{ {
router.go_back(); router.go_back();
} else { } else {
get_active_columns_mut(accounts, decks_cache).new_column_picker(); get_active_columns_mut(i18n, accounts, decks_cache).new_column_picker();
} }
} }
SidePanelAction::ComposeNote => { SidePanelAction::ComposeNote => {
@@ -263,7 +274,7 @@ impl<'a> DesktopSidePanel<'a> {
switching_response = Some(crate::nav::SwitchingAction::Decks( switching_response = Some(crate::nav::SwitchingAction::Decks(
DecksAction::Switch(index), DecksAction::Switch(index),
)); ));
if let Some(edit_deck) = get_decks_mut(accounts, decks_cache) if let Some(edit_deck) = get_decks_mut(i18n, accounts, decks_cache)
.decks_mut() .decks_mut()
.get_mut(index) .get_mut(index)
{ {

View File

@@ -1,5 +1,5 @@
use egui::{vec2, Button, Label, Layout, RichText}; use egui::{vec2, Button, Label, Layout, RichText};
use notedeck::{tr, NamedFontFamily, NotedeckTextStyle}; use notedeck::{tr, Localization, NamedFontFamily, NotedeckTextStyle};
use notedeck_ui::{colors::PINK, padding}; use notedeck_ui::{colors::PINK, padding};
use tracing::error; use tracing::error;
@@ -7,11 +7,12 @@ use crate::support::Support;
pub struct SupportView<'a> { pub struct SupportView<'a> {
support: &'a mut Support, support: &'a mut Support,
i18n: &'a mut Localization,
} }
impl<'a> SupportView<'a> { impl<'a> SupportView<'a> {
pub fn new(support: &'a mut Support) -> Self { pub fn new(support: &'a mut Support, i18n: &'a mut Localization) -> Self {
Self { support } Self { support, i18n }
} }
pub fn show(&mut self, ui: &mut egui::Ui) { pub fn show(&mut self, ui: &mut egui::Ui) {
@@ -22,14 +23,24 @@ impl<'a> SupportView<'a> {
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
); );
ui.add(Label::new( ui.add(Label::new(
RichText::new(tr!("Running into a bug?", "Heading for support section")).font(font), RichText::new(tr!(
self.i18n,
"Running into a bug?",
"Heading for support section"
))
.font(font),
)); ));
ui.label( ui.label(
RichText::new(tr!("Step 1", "Step 1 label in support instructions")) RichText::new(tr!(
.text_style(NotedeckTextStyle::Heading3.text_style()), self.i18n,
"Step 1",
"Step 1 label in support instructions"
))
.text_style(NotedeckTextStyle::Heading3.text_style()),
); );
padding(8.0, ui, |ui| { padding(8.0, ui, |ui| {
ui.label(tr!( ui.label(tr!(
self.i18n,
"Open your default email client to get help from the Damus team", "Open your default email client to get help from the Damus team",
"Instruction to open email client" "Instruction to open email client"
)); ));
@@ -37,7 +48,7 @@ impl<'a> SupportView<'a> {
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| { ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
let font_size = let font_size =
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body); notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
let button_resp = ui.add(open_email_button(font_size, size)); let button_resp = ui.add(open_email_button(self.i18n, font_size, size));
if button_resp.clicked() { if button_resp.clicked() {
if let Err(e) = open::that(self.support.get_mailto_url()) { if let Err(e) = open::that(self.support.get_mailto_url()) {
error!( error!(
@@ -55,19 +66,23 @@ impl<'a> SupportView<'a> {
if let Some(logs) = self.support.get_most_recent_log() { if let Some(logs) = self.support.get_most_recent_log() {
ui.label( ui.label(
RichText::new(tr!("Step 2", "Step 2 label in support instructions")) RichText::new(tr!(
.text_style(NotedeckTextStyle::Heading3.text_style()), self.i18n,
"Step 2",
"Step 2 label in support instructions"
))
.text_style(NotedeckTextStyle::Heading3.text_style()),
); );
let size = vec2(80.0, 40.0); let size = vec2(80.0, 40.0);
let copy_button = Button::new( let copy_button = Button::new(
RichText::new(tr!("Copy", "Button label to copy logs")).size( RichText::new(tr!(self.i18n, "Copy", "Button label to copy logs")).size(
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body), notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
), ),
) )
.fill(PINK) .fill(PINK)
.min_size(size); .min_size(size);
padding(8.0, ui, |ui| { padding(8.0, ui, |ui| {
ui.add(Label::new(RichText::new(tr!("Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.", "Instruction for copying logs"))).wrap()); ui.add(Label::new(RichText::new(tr!(self.i18n,"Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.", "Instruction for copying logs"))).wrap());
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| { ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
if ui.add(copy_button).clicked() { if ui.add(copy_button).clicked() {
ui.ctx().copy_text(logs.to_string()); ui.ctx().copy_text(logs.to_string());
@@ -86,9 +101,13 @@ impl<'a> SupportView<'a> {
} }
} }
fn open_email_button(font_size: f32, size: egui::Vec2) -> impl egui::Widget { fn open_email_button(
i18n: &mut Localization,
font_size: f32,
size: egui::Vec2,
) -> impl egui::Widget {
Button::new( Button::new(
RichText::new(tr!("Open Email", "Button label to open email client")).size(font_size), RichText::new(tr!(i18n, "Open Email", "Button label to open email client")).size(font_size),
) )
.fill(PINK) .fill(PINK)
.min_size(size) .min_size(size)

View File

@@ -8,7 +8,9 @@ use std::f32::consts::PI;
use tracing::{error, warn}; use tracing::{error, warn};
use crate::timeline::{TimelineCache, TimelineKind, TimelineTab, ViewFilter}; use crate::timeline::{TimelineCache, TimelineKind, TimelineTab, ViewFilter};
use notedeck::{note::root_note_id_from_selected_id, tr, NoteAction, NoteContext, ScrollInfo}; use notedeck::{
note::root_note_id_from_selected_id, tr, Localization, NoteAction, NoteContext, ScrollInfo,
};
use notedeck_ui::{ use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
NoteOptions, NoteView, NoteOptions, NoteView,
@@ -103,7 +105,12 @@ fn timeline_ui(
return None; return None;
}; };
timeline.selected_view = tabs_ui(ui, timeline.selected_view, &timeline.views); timeline.selected_view = tabs_ui(
ui,
note_context.i18n,
timeline.selected_view,
&timeline.views,
);
// need this for some reason?? // need this for some reason??
ui.add_space(3.0); ui.add_space(3.0);
@@ -263,7 +270,12 @@ fn goto_top_button(center: Pos2) -> impl egui::Widget {
} }
} }
pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usize { pub fn tabs_ui(
ui: &mut egui::Ui,
i18n: &mut Localization,
selected: usize,
views: &[TimelineTab],
) -> usize {
ui.spacing_mut().item_spacing.y = 0.0; ui.spacing_mut().item_spacing.y = 0.0;
let tab_res = egui_tabs::Tabs::new(views.len() as i32) let tab_res = egui_tabs::Tabs::new(views.len() as i32)
@@ -281,9 +293,13 @@ pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usi
let ind = state.index(); let ind = state.index();
let txt = match views[ind as usize].filter { let txt = match views[ind as usize].filter {
ViewFilter::Notes => tr!("Notes", "Label for notes-only filter"), ViewFilter::Notes => tr!(i18n, "Notes", "Label for notes-only filter"),
ViewFilter::NotesAndReplies => { ViewFilter::NotesAndReplies => {
tr!("Notes & Replies", "Label for notes and replies filter") tr!(
i18n,
"Notes & Replies",
"Label for notes and replies filter"
)
} }
}; };

View File

@@ -1,7 +1,7 @@
use egui::{vec2, CornerRadius, Layout}; use egui::{vec2, CornerRadius, Layout};
use notedeck::{ use notedeck::{
get_current_wallet, tr, Accounts, DefaultZapMsats, GlobalWallet, NotedeckTextStyle, get_current_wallet, tr, Accounts, DefaultZapMsats, GlobalWallet, Localization,
PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet, NotedeckTextStyle, PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet,
}; };
use crate::{nav::RouterAction, route::Route}; use crate::{nav::RouterAction, route::Route};
@@ -153,11 +153,12 @@ impl WalletAction {
pub struct WalletView<'a> { pub struct WalletView<'a> {
state: WalletState<'a>, state: WalletState<'a>,
i18n: &'a mut Localization,
} }
impl<'a> WalletView<'a> { impl<'a> WalletView<'a> {
pub fn new(state: WalletState<'a>) -> Self { pub fn new(state: WalletState<'a>, i18n: &'a mut Localization) -> Self {
Self { state } Self { state, i18n }
} }
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<WalletAction> { pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<WalletAction> {
@@ -173,11 +174,17 @@ impl<'a> WalletView<'a> {
wallet, wallet,
default_zap_state, default_zap_state,
can_create_local_wallet, can_create_local_wallet,
} => show_with_wallet(ui, wallet, default_zap_state, *can_create_local_wallet), } => show_with_wallet(
ui,
self.i18n,
wallet,
default_zap_state,
*can_create_local_wallet,
),
WalletState::NoWallet { WalletState::NoWallet {
state, state,
show_local_only, show_local_only,
} => show_no_wallet(ui, state, *show_local_only), } => show_no_wallet(ui, self.i18n, state, *show_local_only),
} }
} }
} }
@@ -196,6 +203,7 @@ fn try_create_wallet(state: &mut WalletUIState) -> Option<Wallet> {
fn show_no_wallet( fn show_no_wallet(
ui: &mut egui::Ui, ui: &mut egui::Ui,
i18n: &mut Localization,
state: &mut WalletUIState, state: &mut WalletUIState,
show_local_only: bool, show_local_only: bool,
) -> Option<WalletAction> { ) -> Option<WalletAction> {
@@ -203,6 +211,7 @@ fn show_no_wallet(
let text_edit = egui::TextEdit::singleline(&mut state.buf) let text_edit = egui::TextEdit::singleline(&mut state.buf)
.hint_text( .hint_text(
egui::RichText::new(tr!( egui::RichText::new(tr!(
i18n,
"Paste your NWC URI here...", "Paste your NWC URI here...",
"Placeholder text for NWC URI input" "Placeholder text for NWC URI input"
)) ))
@@ -222,10 +231,12 @@ fn show_no_wallet(
let error_str = match error_msg { let error_str = match error_msg {
WalletError::InvalidURI => tr!( WalletError::InvalidURI => tr!(
i18n,
"Invalid NWC URI", "Invalid NWC URI",
"Error message for invalid Nostr Wallet Connect URI" "Error message for invalid Nostr Wallet Connect URI"
), ),
WalletError::NoWallet => tr!( WalletError::NoWallet => tr!(
i18n,
"Add a wallet to continue", "Add a wallet to continue",
"Error message for missing wallet" "Error message for missing wallet"
), ),
@@ -239,6 +250,7 @@ fn show_no_wallet(
ui.checkbox( ui.checkbox(
&mut state.for_local_only, &mut state.for_local_only,
tr!( tr!(
i18n,
"Use this wallet for the current account only", "Use this wallet for the current account only",
"Checkbox label for using wallet only for current account" "Checkbox label for using wallet only for current account"
), ),
@@ -248,7 +260,7 @@ fn show_no_wallet(
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
ui.add(styled_button( ui.add(styled_button(
tr!("Add Wallet", "Button label to add a wallet").as_str(), tr!(i18n, "Add Wallet", "Button label to add a wallet").as_str(),
notedeck_ui::colors::PINK, notedeck_ui::colors::PINK,
)) ))
.clicked() .clicked()
@@ -259,6 +271,7 @@ fn show_no_wallet(
fn show_with_wallet( fn show_with_wallet(
ui: &mut egui::Ui, ui: &mut egui::Ui,
i18n: &mut Localization,
wallet: &mut Wallet, wallet: &mut Wallet,
default_zap_state: &mut DefaultZapState, default_zap_state: &mut DefaultZapState,
can_create_local_wallet: bool, can_create_local_wallet: bool,
@@ -279,12 +292,12 @@ fn show_with_wallet(
} }
}); });
let mut action = show_default_zap(ui, default_zap_state); let mut action = show_default_zap(ui, i18n, default_zap_state);
ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| 's: { ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| 's: {
if ui if ui
.add(styled_button( .add(styled_button(
tr!("Delete Wallet", "Button label to delete a wallet").as_str(), tr!(i18n, "Delete Wallet", "Button label to delete a wallet").as_str(),
ui.visuals().window_fill, ui.visuals().window_fill,
)) ))
.clicked() .clicked()
@@ -299,6 +312,7 @@ fn show_with_wallet(
.checkbox( .checkbox(
&mut false, &mut false,
tr!( tr!(
i18n,
"Add a different wallet that will only be used for this account", "Add a different wallet that will only be used for this account",
"Button label to add a different wallet" "Button label to add a different wallet"
), ),
@@ -323,13 +337,17 @@ fn show_balance(ui: &mut egui::Ui, msats: u64) -> egui::Response {
.inner .inner
} }
fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<WalletAction> { fn show_default_zap(
ui: &mut egui::Ui,
i18n: &mut Localization,
state: &mut DefaultZapState,
) -> Option<WalletAction> {
let mut action = None; let mut action = None;
ui.allocate_ui_with_layout( ui.allocate_ui_with_layout(
vec2(ui.available_width(), 50.0), vec2(ui.available_width(), 50.0),
egui::Layout::left_to_right(egui::Align::Center).with_main_wrap(true), egui::Layout::left_to_right(egui::Align::Center).with_main_wrap(true),
|ui| { |ui| {
ui.label(tr!("Default amount per zap: ", "Label for default zap amount input")); ui.label(tr!(i18n, "Default amount per zap: ", "Label for default zap amount input"));
match state { match state {
DefaultZapState::Pending(pending_default_zap_state) => { DefaultZapState::Pending(pending_default_zap_state) => {
let text = &mut pending_default_zap_state.amount_sats; let text = &mut pending_default_zap_state.amount_sats;
@@ -361,27 +379,27 @@ fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<Wa
ui.memory_mut(|m| m.request_focus(id)); ui.memory_mut(|m| m.request_focus(id));
ui.label(tr!("sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.")); ui.label(tr!(i18n, "sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings."));
if ui if ui
.add(styled_button(tr!("Save", "Button to save default zap amount").as_str(), ui.visuals().widgets.active.bg_fill)) .add(styled_button(tr!(i18n, "Save", "Button to save default zap amount").as_str(), ui.visuals().widgets.active.bg_fill))
.clicked() .clicked()
{ {
action = Some(WalletAction::SetDefaultZapSats(text.to_string())); action = Some(WalletAction::SetDefaultZapSats(text.to_string()));
} }
} }
DefaultZapState::Valid(msats) => { DefaultZapState::Valid(msats) => {
if let Some(wallet_action) = show_valid_msats(ui, **msats) { if let Some(wallet_action) = show_valid_msats(ui, i18n, **msats) {
action = Some(wallet_action); action = Some(wallet_action);
} }
ui.label(tr!("sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.")); ui.label(tr!(i18n, "sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings."));
} }
} }
if let DefaultZapState::Pending(pending) = state { if let DefaultZapState::Pending(pending) = state {
if let Some(error_message) = &pending.error_message { if let Some(error_message) = &pending.error_message {
let msg_str = match error_message { let msg_str = match error_message {
notedeck::DefaultZapError::InvalidUserInput => tr!("Invalid amount", "Error message for invalid zap amount"), notedeck::DefaultZapError::InvalidUserInput => tr!(i18n, "Invalid amount", "Error message for invalid zap amount"),
}; };
ui.colored_label(ui.visuals().warn_fg_color, msg_str); ui.colored_label(ui.visuals().warn_fg_color, msg_str);
@@ -393,7 +411,11 @@ fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<Wa
action action
} }
fn show_valid_msats(ui: &mut egui::Ui, msats: u64) -> Option<WalletAction> { fn show_valid_msats(
ui: &mut egui::Ui,
i18n: &mut Localization,
msats: u64,
) -> Option<WalletAction> {
let galley = { let galley = {
let painter = ui.painter(); let painter = ui.painter();
@@ -409,7 +431,11 @@ fn show_valid_msats(ui: &mut egui::Ui, msats: u64) -> Option<WalletAction> {
let resp = resp let resp = resp
.on_hover_cursor(egui::CursorIcon::PointingHand) .on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text_at_pointer(tr!("Click to edit", "Hover text for editable zap amount")); .on_hover_text_at_pointer(tr!(
i18n,
"Click to edit",
"Hover text for editable zap amount"
));
let painter = ui.painter_at(resp.rect); let painter = ui.painter_at(resp.rect);

View File

@@ -4,7 +4,7 @@ use crate::{
}; };
use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers};
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::{tr, Accounts, AppContext, Images, NoteAction, NoteContext}; use notedeck::{tr, Accounts, AppContext, Images, Localization, NoteAction, NoteContext};
use notedeck_ui::{app_images, icons::search_icon, jobs::JobsCache, NoteOptions, ProfilePic}; use notedeck_ui::{app_images, icons::search_icon, jobs::JobsCache, NoteOptions, ProfilePic};
/// DaveUi holds all of the data it needs to render itself /// DaveUi holds all of the data it needs to render itself
@@ -107,7 +107,7 @@ impl<'a> DaveUi<'a> {
.inner_margin(egui::Margin::same(8)) .inner_margin(egui::Margin::same(8))
.fill(ui.visuals().extreme_bg_color) .fill(ui.visuals().extreme_bg_color)
.corner_radius(12.0) .corner_radius(12.0)
.show(ui, |ui| self.inputbox(ui)) .show(ui, |ui| self.inputbox(app_ctx.i18n, ui))
.inner; .inner;
let note_action = egui::ScrollArea::vertical() let note_action = egui::ScrollArea::vertical()
@@ -134,11 +134,11 @@ impl<'a> DaveUi<'a> {
.or(DaveResponse { action }) .or(DaveResponse { action })
} }
fn error_chat(&self, err: &str, ui: &mut egui::Ui) { fn error_chat(&self, i18n: &mut Localization, err: &str, ui: &mut egui::Ui) {
if self.trial { if self.trial {
ui.add(egui::Label::new( ui.add(egui::Label::new(
egui::RichText::new( egui::RichText::new(
tr!("The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!", "Message shown when Dave trial period has ended"), tr!(i18n, "The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!", "Message shown when Dave trial period has ended"),
) )
.weak(), .weak(),
)); ));
@@ -160,7 +160,7 @@ impl<'a> DaveUi<'a> {
for message in self.chat { for message in self.chat {
let r = match message { let r = match message {
Message::Error(err) => { Message::Error(err) => {
self.error_chat(err, ui); self.error_chat(ctx.i18n, err, ui);
None None
} }
Message::User(msg) => { Message::User(msg) => {
@@ -220,6 +220,7 @@ impl<'a> DaveUi<'a> {
unknown_ids: ctx.unknown_ids, unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard, clipboard: ctx.clipboard,
current_account_has_wallet: false, current_account_has_wallet: false,
i18n: ctx.i18n,
}; };
let txn = Transaction::new(note_context.ndb).unwrap(); let txn = Transaction::new(note_context.ndb).unwrap();
@@ -303,13 +304,14 @@ impl<'a> DaveUi<'a> {
note_action note_action
} }
fn inputbox(&mut self, ui: &mut egui::Ui) -> DaveResponse { fn inputbox(&mut self, i18n: &mut Localization, ui: &mut egui::Ui) -> DaveResponse {
//ui.add_space(Self::chat_margin(ui.ctx()) as f32); //ui.add_space(Self::chat_margin(ui.ctx()) as f32);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.with_layout(Layout::right_to_left(Align::Max), |ui| { ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
let mut dave_response = DaveResponse::none(); let mut dave_response = DaveResponse::none();
if ui if ui
.add(egui::Button::new(tr!( .add(egui::Button::new(tr!(
i18n,
"Ask", "Ask",
"Button to send message to Dave AI assistant" "Button to send message to Dave AI assistant"
))) )))
@@ -330,6 +332,7 @@ impl<'a> DaveUi<'a> {
)) ))
.hint_text( .hint_text(
egui::RichText::new(tr!( egui::RichText::new(tr!(
i18n,
"Ask dave anything...", "Ask dave anything...",
"Placeholder text for Dave AI input field" "Placeholder text for Dave AI input field"
)) ))

View File

@@ -323,6 +323,7 @@ pub fn render_note_contents(
&supported_medias, &supported_medias,
carousel_id, carousel_id,
trusted_media, trusted_media,
note_context.i18n,
); );
ui.add_space(2.0); ui.add_space(2.0);
} }

View File

@@ -1,6 +1,6 @@
use egui::{Rect, Vec2}; use egui::{Rect, Vec2};
use nostrdb::NoteKey; use nostrdb::NoteKey;
use notedeck::{tr, BroadcastContext, NoteContextSelection}; use notedeck::{tr, BroadcastContext, Localization, NoteContextSelection};
pub struct NoteContextButton { pub struct NoteContextButton {
put_at: Option<Rect>, put_at: Option<Rect>,
@@ -105,24 +105,17 @@ impl NoteContextButton {
#[profiling::function] #[profiling::function]
pub fn menu( pub fn menu(
ui: &mut egui::Ui, ui: &mut egui::Ui,
i18n: &mut Localization,
button_response: egui::Response, button_response: egui::Response,
) -> Option<NoteContextSelection> { ) -> Option<NoteContextSelection> {
let mut context_selection: Option<NoteContextSelection> = None; let mut context_selection: Option<NoteContextSelection> = None;
// Debug: Check if global i18n is available
if let Some(i18n) = notedeck::i18n::get_global_i18n() {
if let Ok(locale) = i18n.get_current_locale() {
tracing::debug!("Current locale in context menu: {}", locale);
}
} else {
tracing::warn!("Global i18n context not available in context menu");
}
stationary_arbitrary_menu_button(ui, button_response, |ui| { stationary_arbitrary_menu_button(ui, button_response, |ui| {
ui.set_max_width(200.0); ui.set_max_width(200.0);
// Debug: Check what the tr! macro returns // Debug: Check what the tr! macro returns
let copy_text = tr!( let copy_text = tr!(
i18n,
"Copy Text", "Copy Text",
"Copy the text content of the note to clipboard" "Copy the text content of the note to clipboard"
); );
@@ -134,6 +127,7 @@ impl NoteContextButton {
} }
if ui if ui
.button(tr!( .button(tr!(
i18n,
"Copy Pubkey", "Copy Pubkey",
"Copy the author's public key to clipboard" "Copy the author's public key to clipboard"
)) ))
@@ -144,6 +138,7 @@ impl NoteContextButton {
} }
if ui if ui
.button(tr!( .button(tr!(
i18n,
"Copy Note ID", "Copy Note ID",
"Copy the unique note identifier to clipboard" "Copy the unique note identifier to clipboard"
)) ))
@@ -154,6 +149,7 @@ impl NoteContextButton {
} }
if ui if ui
.button(tr!( .button(tr!(
i18n,
"Copy Note JSON", "Copy Note JSON",
"Copy the raw note data in JSON format to clipboard" "Copy the raw note data in JSON format to clipboard"
)) ))
@@ -164,6 +160,7 @@ impl NoteContextButton {
} }
if ui if ui
.button(tr!( .button(tr!(
i18n,
"Broadcast", "Broadcast",
"Broadcast the note to all connected relays" "Broadcast the note to all connected relays"
)) ))
@@ -176,6 +173,7 @@ impl NoteContextButton {
} }
if ui if ui
.button(tr!( .button(tr!(
i18n,
"Broadcast Local", "Broadcast Local",
"Broadcast the note only to local network relays" "Broadcast the note only to local network relays"
)) ))

View File

@@ -6,8 +6,8 @@ use egui::{
}; };
use notedeck::{ use notedeck::{
fonts::get_font_size, note::MediaAction, show_one_error_message, supported_mime_hosted_at_url, fonts::get_font_size, note::MediaAction, show_one_error_message, supported_mime_hosted_at_url,
tr, GifState, GifStateMap, Images, JobPool, MediaCache, MediaCacheType, NotedeckTextStyle, tr, GifState, GifStateMap, Images, JobPool, Localization, MediaCache, MediaCacheType,
TexturedImage, TexturesCache, UrlMimes, NotedeckTextStyle, TexturedImage, TexturesCache, UrlMimes,
}; };
use crate::{ use crate::{
@@ -20,6 +20,7 @@ use crate::{
AnimationHelper, PulseAlpha, AnimationHelper, PulseAlpha,
}; };
#[allow(clippy::too_many_arguments)]
pub(crate) fn image_carousel( pub(crate) fn image_carousel(
ui: &mut egui::Ui, ui: &mut egui::Ui,
img_cache: &mut Images, img_cache: &mut Images,
@@ -28,6 +29,7 @@ pub(crate) fn image_carousel(
medias: &[RenderableMedia], medias: &[RenderableMedia],
carousel_id: egui::Id, carousel_id: egui::Id,
trusted_media: bool, trusted_media: bool,
i18n: &mut Localization,
) -> Option<MediaAction> { ) -> Option<MediaAction> {
// let's make sure everything is within our area // let's make sure everything is within our area
@@ -69,9 +71,14 @@ pub(crate) fn image_carousel(
blur_type.clone(), blur_type.clone(),
); );
if let Some(cur_action) = if let Some(cur_action) = render_media(
render_media(ui, &mut img_cache.gif_states, media_state, url, height) ui,
{ &mut img_cache.gif_states,
media_state,
url,
height,
i18n,
) {
// clicked the media, lets set the active index // clicked the media, lets set the active index
if let MediaUIAction::Clicked = cur_action { if let MediaUIAction::Clicked = cur_action {
set_show_popup(ui, popup_id(carousel_id), true); set_show_popup(ui, popup_id(carousel_id), true);
@@ -100,7 +107,14 @@ pub(crate) fn image_carousel(
let current_image_index = update_selected_image_index(ui, carousel_id, medias.len() as i32); let current_image_index = update_selected_image_index(ui, carousel_id, medias.len() as i32);
show_full_screen_media(ui, medias, current_image_index, img_cache, carousel_id); show_full_screen_media(
ui,
medias,
current_image_index,
img_cache,
carousel_id,
i18n,
);
} }
action action
} }
@@ -163,6 +177,7 @@ fn show_full_screen_media(
index: usize, index: usize,
img_cache: &mut Images, img_cache: &mut Images,
carousel_id: egui::Id, carousel_id: egui::Id,
i18n: &mut Localization,
) { ) {
Window::new("image_popup") Window::new("image_popup")
.title_bar(false) .title_bar(false)
@@ -201,6 +216,7 @@ fn show_full_screen_media(
cur_state.gifs, cur_state.gifs,
image_url, image_url,
carousel_id, carousel_id,
i18n,
); );
}) })
}); });
@@ -363,6 +379,7 @@ fn select_next_media(
next as usize next as usize
} }
#[allow(clippy::too_many_arguments)]
fn render_full_screen_media( fn render_full_screen_media(
ui: &mut egui::Ui, ui: &mut egui::Ui,
num_urls: usize, num_urls: usize,
@@ -371,6 +388,7 @@ fn render_full_screen_media(
gifs: &mut HashMap<String, GifState>, gifs: &mut HashMap<String, GifState>,
image_url: &str, image_url: &str,
carousel_id: egui::Id, carousel_id: egui::Id,
i18n: &mut Localization,
) { ) {
const TOP_BAR_HEIGHT: f32 = 30.0; const TOP_BAR_HEIGHT: f32 = 30.0;
const BOTTOM_BAR_HEIGHT: f32 = 60.0; const BOTTOM_BAR_HEIGHT: f32 = 60.0;
@@ -631,13 +649,17 @@ fn render_full_screen_media(
}); });
} }
copy_link(image_url, &response); copy_link(i18n, image_url, &response);
} }
fn copy_link(url: &str, img_resp: &Response) { fn copy_link(i18n: &mut Localization, url: &str, img_resp: &Response) {
img_resp.context_menu(|ui| { img_resp.context_menu(|ui| {
if ui if ui
.button(tr!("Copy Link", "Button to copy media link to clipboard")) .button(tr!(
i18n,
"Copy Link",
"Button to copy media link to clipboard"
))
.clicked() .clicked()
{ {
ui.ctx().copy_text(url.to_owned()); ui.ctx().copy_text(url.to_owned());
@@ -653,10 +675,11 @@ fn render_media(
render_state: MediaRenderState, render_state: MediaRenderState,
url: &str, url: &str,
height: f32, height: f32,
i18n: &mut Localization,
) -> Option<MediaUIAction> { ) -> Option<MediaUIAction> {
match render_state { match render_state {
MediaRenderState::ActualImage(image) => { MediaRenderState::ActualImage(image) => {
if render_success_media(ui, url, image, gifs, height).clicked() { if render_success_media(ui, url, image, gifs, height, i18n).clicked() {
Some(MediaUIAction::Clicked) Some(MediaUIAction::Clicked)
} else { } else {
None None
@@ -695,9 +718,9 @@ fn render_media(
let resp = match obfuscated_texture { let resp = match obfuscated_texture {
ObfuscatedTexture::Blur(texture_handle) => { ObfuscatedTexture::Blur(texture_handle) => {
let resp = ui.add(texture_to_image(texture_handle, height)); let resp = ui.add(texture_to_image(texture_handle, height));
render_blur_text(ui, url, resp.rect) render_blur_text(ui, i18n, url, resp.rect)
} }
ObfuscatedTexture::Default => render_default_blur(ui, height, url), ObfuscatedTexture::Default => render_default_blur(ui, i18n, height, url),
}; };
if resp if resp
@@ -712,7 +735,12 @@ fn render_media(
} }
} }
fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> egui::Response { fn render_blur_text(
ui: &mut egui::Ui,
i18n: &mut Localization,
url: &str,
render_rect: egui::Rect,
) -> egui::Response {
let helper = AnimationHelper::new_from_rect(ui, ("show_media", url), render_rect); let helper = AnimationHelper::new_from_rect(ui, ("show_media", url), render_rect);
let painter = ui.painter_at(helper.get_animation_rect()); let painter = ui.painter_at(helper.get_animation_rect());
@@ -726,6 +754,7 @@ fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> eg
); );
let info_galley = painter.layout( let info_galley = painter.layout(
tr!( tr!(
i18n,
"Media from someone you don't follow", "Media from someone you don't follow",
"Text shown on blurred media from unfollowed users" "Text shown on blurred media from unfollowed users"
) )
@@ -736,7 +765,7 @@ fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> eg
); );
let load_galley = painter.layout_no_wrap( let load_galley = painter.layout_no_wrap(
tr!("Tap to Load", "Button text to load blurred media").to_owned(), tr!(i18n, "Tap to Load", "Button text to load blurred media"),
animation_fontid, animation_fontid,
egui::Color32::BLACK, egui::Color32::BLACK,
// ui.visuals().widgets.inactive.bg_fill, // ui.visuals().widgets.inactive.bg_fill,
@@ -792,9 +821,14 @@ fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> eg
helper.take_animation_response() helper.take_animation_response()
} }
fn render_default_blur(ui: &mut egui::Ui, height: f32, url: &str) -> egui::Response { fn render_default_blur(
ui: &mut egui::Ui,
i18n: &mut Localization,
height: f32,
url: &str,
) -> egui::Response {
let rect = render_default_blur_bg(ui, height, url, false); let rect = render_default_blur_bg(ui, height, url, false);
render_blur_text(ui, url, rect) render_blur_text(ui, i18n, url, rect)
} }
fn render_default_blur_bg(ui: &mut egui::Ui, height: f32, url: &str, shimmer: bool) -> egui::Rect { fn render_default_blur_bg(ui: &mut egui::Ui, height: f32, url: &str, shimmer: bool) -> egui::Rect {
@@ -883,12 +917,13 @@ fn render_success_media(
tex: &mut TexturedImage, tex: &mut TexturedImage,
gifs: &mut GifStateMap, gifs: &mut GifStateMap,
height: f32, height: f32,
i18n: &mut Localization,
) -> Response { ) -> Response {
let texture = handle_repaint(ui, retrieve_latest_texture(url, gifs, tex)); let texture = handle_repaint(ui, retrieve_latest_texture(url, gifs, tex));
let img = texture_to_image(texture, height); let img = texture_to_image(texture, height);
let img_resp = ui.add(Button::image(img).frame(false)); let img_resp = ui.add(Button::image(img).frame(false));
copy_link(url, &img_resp); copy_link(i18n, url, &img_resp);
img_resp img_resp
} }

View File

@@ -17,6 +17,7 @@ use notedeck::note::MediaAction;
use notedeck::note::ZapTargetAmount; use notedeck::note::ZapTargetAmount;
use notedeck::ui::is_narrow; use notedeck::ui::is_narrow;
use notedeck::Images; use notedeck::Images;
use notedeck::Localization;
pub use options::NoteOptions; pub use options::NoteOptions;
pub use reply_description::reply_desc; pub use reply_description::reply_desc;
@@ -27,8 +28,8 @@ use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
use notedeck::{ use notedeck::{
name::get_display_name, name::get_display_name,
note::{NoteAction, NoteContext, ZapAction}, note::{NoteAction, NoteContext, ZapAction},
tr, AnyZapState, CachedNote, ContextSelection, NoteCache, NoteZapTarget, NoteZapTargetOwned, tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, NotedeckTextStyle,
NotedeckTextStyle, ZapTarget, Zaps, ZapTarget, Zaps,
}; };
pub struct NoteView<'a, 'd> { pub struct NoteView<'a, 'd> {
@@ -194,7 +195,6 @@ impl<'a, 'd> NoteView<'a, 'd> {
} }
fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response { 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"); let txn = self.note.txn().expect("todo: implement non-db notes");
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
@@ -206,23 +206,22 @@ impl<'a, 'd> NoteView<'a, 'd> {
//ui.horizontal(|ui| { //ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0; 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)); let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0));
ui.allocate_rect(rect, Sense::hover()); ui.allocate_rect(rect, Sense::hover());
ui.put(rect, |ui: &mut egui::Ui| { ui.put(rect, |ui: &mut egui::Ui| {
render_reltime(ui, cached_note, false).response render_reltime(ui, self.note_context.i18n, self.note.created_at(), false).response
}); });
let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0)); let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0));
ui.allocate_rect(rect, Sense::hover()); ui.allocate_rect(rect, Sense::hover());
ui.put(rect, |ui: &mut egui::Ui| { ui.put(rect, |ui: &mut egui::Ui| {
ui.add( ui.add(
Username::new(profile.as_ref().ok(), self.note.pubkey()) Username::new(
.abbreviated(6) self.note_context.i18n,
.pk_colored(true), profile.as_ref().ok(),
self.note.pubkey(),
)
.abbreviated(6)
.pk_colored(true),
) )
}); });
@@ -308,9 +307,13 @@ impl<'a, 'd> NoteView<'a, 'd> {
let color = ui.style().visuals.noninteractive().fg_stroke.color; let color = ui.style().visuals.noninteractive().fg_stroke.color;
ui.add_space(4.0); ui.add_space(4.0);
ui.label( ui.label(
RichText::new(tr!("Reposted", "Label for reposted notes")) RichText::new(tr!(
.color(color) self.note_context.i18n,
.text_style(style.text_style()), "Reposted",
"Label for reposted notes"
))
.color(color)
.text_style(style.text_style()),
); );
}); });
NoteView::new(self.note_context, &note_to_repost, self.flags, self.jobs).show(ui) NoteView::new(self.note_context, &note_to_repost, self.flags, self.jobs).show(ui)
@@ -348,20 +351,17 @@ impl<'a, 'd> NoteView<'a, 'd> {
#[profiling::function] #[profiling::function]
fn note_header( fn note_header(
ui: &mut egui::Ui, ui: &mut egui::Ui,
note_cache: &mut NoteCache, i18n: &mut Localization,
note: &Note, note: &Note,
profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>, profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
show_unread_indicator: bool, show_unread_indicator: bool,
) { ) {
let note_key = note.key().unwrap();
let horiz_resp = ui let horiz_resp = ui
.horizontal(|ui| { .horizontal(|ui| {
ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 }; ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 };
ui.add(Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20)); ui.add(Username::new(i18n, profile.as_ref().ok(), note.pubkey()).abbreviated(20));
let cached_note = note_cache.cached_note_or_insert_mut(note_key, note); render_reltime(ui, i18n, note.created_at(), true);
render_reltime(ui, cached_note, true);
}) })
.response; .response;
@@ -405,7 +405,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
ui.horizontal_centered(|ui| { ui.horizontal_centered(|ui| {
NoteView::note_header( NoteView::note_header(
ui, ui,
self.note_context.note_cache, self.note_context.i18n,
self.note, self.note,
profile, profile,
self.show_unread_indicator, self.show_unread_indicator,
@@ -460,10 +460,16 @@ impl<'a, 'd> NoteView<'a, 'd> {
cur_acc: cur_acc.keypair(), cur_acc: cur_acc.keypair(),
}) })
}; };
note_action = note_action = render_note_actionbar(
render_note_actionbar(ui, zapper, self.note.id(), self.note.pubkey(), note_key) ui,
.inner zapper,
.or(note_action); self.note.id(),
self.note.pubkey(),
note_key,
self.note_context.i18n,
)
.inner
.or(note_action);
} }
NoteUiResponse { NoteUiResponse {
@@ -489,7 +495,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
NoteView::note_header( NoteView::note_header(
ui, ui,
self.note_context.note_cache, self.note_context.i18n,
self.note, self.note,
profile, profile,
self.show_unread_indicator, self.show_unread_indicator,
@@ -542,6 +548,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
self.note.id(), self.note.id(),
self.note.pubkey(), self.note.pubkey(),
note_key, note_key,
self.note_context.i18n,
) )
.inner .inner
.or(note_action); .or(note_action);
@@ -588,7 +595,8 @@ impl<'a, 'd> NoteView<'a, 'd> {
}; };
let resp = ui.add(NoteContextButton::new(note_key).place_at(context_pos)); let resp = ui.add(NoteContextButton::new(note_key).place_at(context_pos));
if let Some(action) = NoteContextButton::menu(ui, resp.clone()) { if let Some(action) = NoteContextButton::menu(ui, self.note_context.i18n, resp.clone())
{
note_action = Some(NoteAction::Context(ContextSelection { note_key, action })); note_action = Some(NoteAction::Context(ContextSelection { note_key, action }));
} }
} }
@@ -765,11 +773,13 @@ fn render_note_actionbar(
note_id: &[u8; 32], note_id: &[u8; 32],
note_pubkey: &[u8; 32], note_pubkey: &[u8; 32],
note_key: NoteKey, note_key: NoteKey,
i18n: &mut Localization,
) -> egui::InnerResponse<Option<NoteAction>> { ) -> egui::InnerResponse<Option<NoteAction>> {
ui.horizontal(|ui| 's: { ui.horizontal(|ui| 's: {
let reply_resp = reply_button(ui, note_key).on_hover_cursor(egui::CursorIcon::PointingHand); let reply_resp =
reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
let quote_resp = let quote_resp =
quote_repost_button(ui, note_key).on_hover_cursor(egui::CursorIcon::PointingHand); quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
let to_noteid = |id: &[u8; 32]| NoteId::new(*id); let to_noteid = |id: &[u8; 32]| NoteId::new(*id);
if reply_resp.clicked() { if reply_resp.clicked() {
@@ -804,7 +814,7 @@ fn render_note_actionbar(
cur_acc.secret_key.as_ref()?; cur_acc.secret_key.as_ref()?;
match zap_state { match zap_state {
Ok(any_zap_state) => ui.add(zap_button(any_zap_state, note_id)), Ok(any_zap_state) => ui.add(zap_button(i18n, any_zap_state, note_id)),
Err(err) => { Err(err) => {
let (rect, _) = let (rect, _) =
ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click()); ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
@@ -832,7 +842,8 @@ fn render_note_actionbar(
#[profiling::function] #[profiling::function]
fn render_reltime( fn render_reltime(
ui: &mut egui::Ui, ui: &mut egui::Ui,
note_cache: &mut CachedNote, i18n: &mut Localization,
created_at: u64,
before: bool, before: bool,
) -> egui::InnerResponse<()> { ) -> egui::InnerResponse<()> {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -840,7 +851,7 @@ fn render_reltime(
secondary_label(ui, ""); secondary_label(ui, "");
} }
secondary_label(ui, note_cache.reltime_str_mut()); secondary_label(ui, notedeck::time_ago_since(i18n, created_at));
if !before { if !before {
secondary_label(ui, ""); secondary_label(ui, "");
@@ -848,7 +859,7 @@ fn render_reltime(
}) })
} }
fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { fn reply_button(ui: &mut egui::Ui, i18n: &mut Localization, note_key: NoteKey) -> egui::Response {
let img = if ui.style().visuals.dark_mode { let img = if ui.style().visuals.dark_mode {
app_images::reply_dark_image() app_images::reply_dark_image()
} else { } else {
@@ -862,9 +873,11 @@ fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
let expand_size = 5.0; // from hover_expand_small let expand_size = 5.0; // from hover_expand_small
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
let put_resp = ui let put_resp = ui.put(rect, img.max_width(size)).on_hover_text(tr!(
.put(rect, img.max_width(size)) i18n,
.on_hover_text(tr!("Reply to this note", "Hover text for reply button")); "Reply to this note",
"Hover text for reply button"
));
resp.union(put_resp) resp.union(put_resp)
} }
@@ -877,7 +890,11 @@ fn repost_icon(dark_mode: bool) -> egui::Image<'static> {
} }
} }
fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { fn quote_repost_button(
ui: &mut egui::Ui,
i18n: &mut Localization,
note_key: NoteKey,
) -> egui::Response {
let size = 14.0; let size = 14.0;
let expand_size = 5.0; let expand_size = 5.0;
let anim_speed = 0.05; let anim_speed = 0.05;
@@ -889,12 +906,20 @@ fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
let put_resp = ui let put_resp = ui
.put(rect, repost_icon(ui.visuals().dark_mode).max_width(size)) .put(rect, repost_icon(ui.visuals().dark_mode).max_width(size))
.on_hover_text(tr!("Repost this note", "Hover text for repost button")); .on_hover_text(tr!(
i18n,
"Repost this note",
"Hover text for repost button"
));
resp.union(put_resp) resp.union(put_resp)
} }
fn zap_button(state: AnyZapState, noteid: &[u8; 32]) -> impl egui::Widget + use<'_> { fn zap_button<'a>(
i18n: &'a mut Localization,
state: AnyZapState,
noteid: &'a [u8; 32],
) -> impl egui::Widget + use<'a> {
move |ui: &mut egui::Ui| -> egui::Response { move |ui: &mut egui::Ui| -> egui::Response {
let (rect, size, resp) = crate::anim::hover_expand_small(ui, ui.id().with("zap")); let (rect, size, resp) = crate::anim::hover_expand_small(ui, ui.id().with("zap"));
@@ -927,9 +952,11 @@ fn zap_button(state: AnyZapState, noteid: &[u8; 32]) -> impl egui::Widget + use<
let expand_size = 5.0; // from hover_expand_small let expand_size = 5.0; // from hover_expand_small
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
let put_resp = ui let put_resp = ui.put(rect, img).on_hover_text(tr!(
.put(rect, img) i18n,
.on_hover_text(tr!("Zap this note", "Hover text for zap button")); "Zap this note",
"Hover text for zap button"
));
resp.union(put_resp) resp.union(put_resp)
} }

View File

@@ -130,9 +130,13 @@ fn render_text_segments(
if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) { if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) {
let r = ui.add( let r = ui.add(
Label::new( Label::new(
RichText::new(tr!("note", "Link text for note references")) RichText::new(tr!(
.size(size) note_context.i18n,
.color(link_color), "note",
"Link text for note references"
))
.size(size)
.color(link_color),
) )
.sense(Sense::click()) .sense(Sense::click())
.selectable(selectable), .selectable(selectable),
@@ -157,9 +161,13 @@ fn render_text_segments(
if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) { if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) {
let r = ui.add( let r = ui.add(
Label::new( Label::new(
RichText::new(tr!("thread", "Link text for thread references")) RichText::new(tr!(
.size(size) note_context.i18n,
.color(link_color), "thread",
"Link text for thread references"
))
.size(size)
.color(link_color),
) )
.sense(Sense::click()) .sense(Sense::click())
.selectable(selectable), .selectable(selectable),
@@ -206,6 +214,7 @@ pub fn reply_desc(
} else { } else {
// Handle case where reply note is not found // Handle case where reply note is not found
let template = tr!( let template = tr!(
note_context.i18n,
"replying to a note", "replying to a note",
"Fallback text when reply note is not found" "Fallback text when reply note is not found"
); );
@@ -225,6 +234,7 @@ pub fn reply_desc(
let segments = if note_reply.is_reply_to_root() { let segments = if note_reply.is_reply_to_root() {
// Template: "replying to {user}'s {thread}" // Template: "replying to {user}'s {thread}"
let template = tr!( let template = tr!(
note_context.i18n,
"replying to {user}'s {thread}", "replying to {user}'s {thread}",
"Template for replying to root thread", "Template for replying to root thread",
user = "{user}", user = "{user}",
@@ -243,6 +253,7 @@ pub fn reply_desc(
if root_note.pubkey() == reply_note.pubkey() { if root_note.pubkey() == reply_note.pubkey() {
// Template: "replying to {user}'s {note}" // Template: "replying to {user}'s {note}"
let template = tr!( let template = tr!(
note_context.i18n,
"replying to {user}'s {note}", "replying to {user}'s {note}",
"Template for replying to user's note", "Template for replying to user's note",
user = "{user}", user = "{user}",
@@ -254,6 +265,7 @@ pub fn reply_desc(
// Template: "replying to {reply_user}'s {note} in {thread_user}'s {thread}" // Template: "replying to {reply_user}'s {note} in {thread_user}'s {thread}"
// This would need more sophisticated placeholder handling // This would need more sophisticated placeholder handling
let template = tr!( let template = tr!(
note_context.i18n,
"replying to {user}'s {note} in {thread_user}'s {thread}", "replying to {user}'s {note} in {thread_user}'s {thread}",
"Template for replying to note in different user's thread", "Template for replying to note in different user's thread",
user = "{user}", user = "{user}",
@@ -273,6 +285,7 @@ pub fn reply_desc(
} else { } else {
// Template: "replying to {user} in someone's thread" // Template: "replying to {user} in someone's thread"
let template = tr!( let template = tr!(
note_context.i18n,
"replying to {user} in someone's thread", "replying to {user} in someone's thread",
"Template for replying to user in unknown thread", "Template for replying to user in unknown thread",
user = "{user}" user = "{user}"
@@ -283,6 +296,7 @@ pub fn reply_desc(
} else { } else {
// Fallback // Fallback
let template = tr!( let template = tr!(
note_context.i18n,
"replying to {user}", "replying to {user}",
"Fallback template for replying to user", "Fallback template for replying to user",
user = "{user}" user = "{user}"

View File

@@ -3,7 +3,9 @@ use egui::{Frame, Label, RichText};
use egui_extras::Size; use egui_extras::Size;
use nostrdb::ProfileRecord; use nostrdb::ProfileRecord;
use notedeck::{name::get_display_name, profile::get_profile_url, tr, Images, NotedeckTextStyle}; use notedeck::{
name::get_display_name, profile::get_profile_url, tr, Images, Localization, NotedeckTextStyle,
};
use super::{about_section_widget, banner, display_name_widget}; use super::{about_section_widget, banner, display_name_widget};
@@ -68,6 +70,7 @@ impl egui::Widget for ProfilePreview<'_, '_> {
pub struct SimpleProfilePreview<'a, 'cache> { pub struct SimpleProfilePreview<'a, 'cache> {
profile: Option<&'a ProfileRecord<'a>>, profile: Option<&'a ProfileRecord<'a>>,
pub i18n: &'cache mut Localization,
cache: &'cache mut Images, cache: &'cache mut Images,
is_nsec: bool, is_nsec: bool,
} }
@@ -76,12 +79,14 @@ impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
pub fn new( pub fn new(
profile: Option<&'a ProfileRecord<'a>>, profile: Option<&'a ProfileRecord<'a>>,
cache: &'cache mut Images, cache: &'cache mut Images,
i18n: &'cache mut Localization,
is_nsec: bool, is_nsec: bool,
) -> Self { ) -> Self {
SimpleProfilePreview { SimpleProfilePreview {
profile, profile,
cache, cache,
is_nsec, is_nsec,
i18n,
} }
} }
} }
@@ -96,12 +101,16 @@ impl egui::Widget for SimpleProfilePreview<'_, '_> {
if !self.is_nsec { if !self.is_nsec {
ui.add( ui.add(
Label::new( Label::new(
RichText::new(tr!("Read only", "Label for read-only profile mode")) RichText::new(tr!(
.size(notedeck::fonts::get_font_size( self.i18n,
ui.ctx(), "Read only",
&NotedeckTextStyle::Tiny, "Label for read-only profile mode"
)) ))
.color(ui.visuals().warn_fg_color), .size(notedeck::fonts::get_font_size(
ui.ctx(),
&NotedeckTextStyle::Tiny,
))
.color(ui.visuals().warn_fg_color),
) )
.selectable(false), .selectable(false),
); );

View File

@@ -1,8 +1,9 @@
use egui::{Color32, RichText, Widget}; use egui::{Color32, RichText, Widget};
use nostrdb::ProfileRecord; use nostrdb::ProfileRecord;
use notedeck::{fonts::NamedFontFamily, tr}; use notedeck::{fonts::NamedFontFamily, tr, Localization};
pub struct Username<'a> { pub struct Username<'a> {
i18n: &'a mut Localization,
profile: Option<&'a ProfileRecord<'a>>, profile: Option<&'a ProfileRecord<'a>>,
pk: &'a [u8; 32], pk: &'a [u8; 32],
pk_colored: bool, pk_colored: bool,
@@ -20,10 +21,15 @@ impl<'a> Username<'a> {
self self
} }
pub fn new(profile: Option<&'a ProfileRecord>, pk: &'a [u8; 32]) -> Self { pub fn new(
i18n: &'a mut Localization,
profile: Option<&'a ProfileRecord>,
pk: &'a [u8; 32],
) -> Self {
let pk_colored = false; let pk_colored = false;
let abbrev: usize = 1000; let abbrev: usize = 1000;
Username { Username {
i18n,
profile, profile,
pk, pk,
pk_colored, pk_colored,
@@ -53,6 +59,7 @@ impl Widget for Username<'_> {
} }
} else { } else {
let mut txt = RichText::new(tr!( let mut txt = RichText::new(tr!(
self.i18n,
"nostrich", "nostrich",
"Default username when profile is not available" "Default username when profile is not available"
)) ))