mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-17 08:44:20 +01:00
Fix critical issues from code review
Removed: - Channel switcher feature (Cmd+K) - removed entire feature as requested Fixed channel dialog: - Auto-focus on name field now works correctly - Escape key now closes dialog - Enter key submits form (when not in hashtags field) - Made hashtags optional (only channel name required) Fixed error handling: - Replaced .expect() and .unwrap() with proper error handling - Transaction creation failures now log errors instead of panicking - System time errors handled gracefully - Added safety comments for fallback channel access Improved internationalization: - format_timestamp() now uses tr!() macro for all strings - Supports localization for "Just now", "ago", "Yesterday", etc. Refactored code: - Extracted inline action handling to process_chat_action() function - Cleaner separation of concerns - More testable and maintainable Thread panel: - ThreadView handles missing threads gracefully (no extra validation needed) Build status: - Compiles successfully with only unused field warnings - All functionality tested
This commit is contained in:
@@ -52,7 +52,6 @@ pub struct Damus {
|
||||
pub channels_cache: crate::channels::ChannelsCache,
|
||||
pub relay_config: crate::relay_config::RelayConfig,
|
||||
pub channel_dialog: ui::ChannelDialog,
|
||||
pub channel_switcher: ui::ChannelSwitcher,
|
||||
pub thread_panel: ui::ThreadPanel,
|
||||
pub view_state: ViewState,
|
||||
pub drafts: Drafts,
|
||||
@@ -229,22 +228,14 @@ fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Con
|
||||
damus.thread_panel.close();
|
||||
} else if damus.channel_dialog.is_open {
|
||||
damus.channel_dialog.close();
|
||||
} else if damus.channel_switcher.is_open {
|
||||
damus.channel_switcher.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Cmd+N / Ctrl+N to open new channel dialog
|
||||
let cmd_n = (i.modifiers.command || i.modifiers.ctrl) && i.key_pressed(egui::Key::N);
|
||||
if cmd_n && !damus.channel_dialog.is_open && !damus.channel_switcher.is_open && !damus.thread_panel.is_open {
|
||||
if cmd_n && !damus.channel_dialog.is_open && !damus.thread_panel.is_open {
|
||||
damus.channel_dialog.open();
|
||||
}
|
||||
|
||||
// Cmd+K / Ctrl+K to open channel switcher
|
||||
let cmd_k = (i.modifiers.command || i.modifiers.ctrl) && i.key_pressed(egui::Key::K);
|
||||
if cmd_k && !damus.channel_dialog.is_open && !damus.channel_switcher.is_open && !damus.thread_panel.is_open {
|
||||
damus.channel_switcher.open();
|
||||
}
|
||||
});
|
||||
|
||||
if damus.columns(app_ctx.accounts).columns().is_empty() {
|
||||
@@ -446,7 +437,13 @@ fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::
|
||||
storage::save_channels_cache(app_ctx.path, &damus.channels_cache);
|
||||
|
||||
// Subscribe to the new channel
|
||||
let txn = nostrdb::Transaction::new(app_ctx.ndb).unwrap();
|
||||
let txn = match nostrdb::Transaction::new(app_ctx.ndb) {
|
||||
Ok(txn) => txn,
|
||||
Err(e) => {
|
||||
error!("Failed to create transaction for channel subscription: {}", e);
|
||||
return app_resp;
|
||||
}
|
||||
};
|
||||
let channel_count = damus.channels_cache.active_channels(app_ctx.accounts).num_channels();
|
||||
if let Some(channel) = damus.channels_cache.active_channels_mut(app_ctx.i18n, app_ctx.accounts).get_channel_mut(channel_count - 1) {
|
||||
if !channel.subscribed {
|
||||
@@ -476,30 +473,6 @@ fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::
|
||||
}
|
||||
}
|
||||
|
||||
// Show channel switcher (Cmd+K)
|
||||
if let Some(switcher_action) = damus.channel_switcher.show(
|
||||
ui.ctx(),
|
||||
app_ctx.i18n,
|
||||
&damus.channels_cache,
|
||||
app_ctx.accounts,
|
||||
) {
|
||||
match switcher_action {
|
||||
ui::ChannelSwitcherAction::SelectChannel(idx) => {
|
||||
// Select the channel
|
||||
damus
|
||||
.channels_cache
|
||||
.active_channels_mut(app_ctx.i18n, app_ctx.accounts)
|
||||
.select_channel(idx);
|
||||
|
||||
// Save channel state
|
||||
storage::save_channels_cache(app_ctx.path, &damus.channels_cache);
|
||||
}
|
||||
ui::ChannelSwitcherAction::Close => {
|
||||
// Switcher was closed, nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show thread panel if open
|
||||
if damus.thread_panel.is_open {
|
||||
let mut note_context = notedeck::NoteContext {
|
||||
@@ -730,7 +703,6 @@ impl Damus {
|
||||
channels_cache,
|
||||
relay_config,
|
||||
channel_dialog: ui::ChannelDialog::default(),
|
||||
channel_switcher: ui::ChannelSwitcher::default(),
|
||||
thread_panel: ui::ThreadPanel::default(),
|
||||
unrecognized_args,
|
||||
jobs,
|
||||
@@ -788,7 +760,6 @@ impl Damus {
|
||||
channels_cache,
|
||||
relay_config,
|
||||
channel_dialog: ui::ChannelDialog::default(),
|
||||
channel_switcher: ui::ChannelSwitcher::default(),
|
||||
thread_panel: ui::ThreadPanel::default(),
|
||||
unrecognized_args: BTreeSet::default(),
|
||||
jobs: JobsCache::default(),
|
||||
@@ -1029,6 +1000,64 @@ fn render_damus_desktop(
|
||||
}
|
||||
}
|
||||
|
||||
/// Process chat view actions like Reply, React, Repost
|
||||
fn process_chat_action(
|
||||
action: NoteAction,
|
||||
app: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
) {
|
||||
match action {
|
||||
NoteAction::Note { note_id, .. } => {
|
||||
// Open thread panel for viewing threads
|
||||
app.thread_panel.open(*note_id.bytes());
|
||||
}
|
||||
NoteAction::Reply(note_id) => {
|
||||
// Open thread panel for replying
|
||||
app.thread_panel.open(*note_id.bytes());
|
||||
}
|
||||
NoteAction::React(react_action) => {
|
||||
// Handle reaction (like) - send to relays
|
||||
if let Some(filled) = ctx.accounts.selected_filled() {
|
||||
let txn = match Transaction::new(ctx.ndb) {
|
||||
Ok(txn) => txn,
|
||||
Err(e) => {
|
||||
error!("Failed to create transaction for reaction: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Send reaction event using existing infrastructure
|
||||
if let Err(err) = crate::actionbar::send_reaction_event(
|
||||
ctx.ndb,
|
||||
&txn,
|
||||
ctx.pool,
|
||||
filled,
|
||||
&react_action,
|
||||
) {
|
||||
error!("Failed to send reaction: {err}");
|
||||
} else {
|
||||
// Mark reaction as sent in UI
|
||||
ui.ctx().data_mut(|d| {
|
||||
use notedeck::note::reaction_sent_id;
|
||||
d.insert_temp(
|
||||
reaction_sent_id(&filled.pubkey, react_action.note_id.bytes()),
|
||||
true,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
NoteAction::Repost(note_id) => {
|
||||
// For now, open thread panel - could add repost dialog later
|
||||
app.thread_panel.open(*note_id.bytes());
|
||||
}
|
||||
_ => {
|
||||
// Other actions not yet supported in chat view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn timelines_view(
|
||||
ui: &mut egui::Ui,
|
||||
sizes: Size,
|
||||
@@ -1115,9 +1144,13 @@ fn timelines_view(
|
||||
});
|
||||
|
||||
// Check if a channel is selected
|
||||
let selected_channel = app.channels_cache.active_channels(ctx.accounts).selected_channel();
|
||||
// Clone the timeline_kind to avoid borrowing app across the closure
|
||||
let selected_timeline_kind = app.channels_cache
|
||||
.active_channels(ctx.accounts)
|
||||
.selected_channel()
|
||||
.map(|c| c.timeline_kind.clone());
|
||||
|
||||
if let Some(channel) = selected_channel {
|
||||
if let Some(timeline_kind) = selected_timeline_kind {
|
||||
// Render ChatView for the selected channel
|
||||
strip.cell(|ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
@@ -1140,7 +1173,7 @@ fn timelines_view(
|
||||
|
||||
// Create a ChatView for the selected channel
|
||||
let mut chat_view = crate::ui::ChatView::new(
|
||||
&channel.timeline_kind,
|
||||
&timeline_kind,
|
||||
&mut app.timeline_cache,
|
||||
&mut note_context,
|
||||
notedeck_ui::NoteOptions::default(),
|
||||
@@ -1152,49 +1185,7 @@ fn timelines_view(
|
||||
|
||||
// Handle chat view actions
|
||||
if let Some(Some(action)) = chat_response.output {
|
||||
match action {
|
||||
NoteAction::Note { note_id, .. } => {
|
||||
// Open thread panel for viewing threads
|
||||
app.thread_panel.open(*note_id.bytes());
|
||||
}
|
||||
NoteAction::Reply(note_id) => {
|
||||
// Open thread panel for replying
|
||||
app.thread_panel.open(*note_id.bytes());
|
||||
}
|
||||
NoteAction::React(react_action) => {
|
||||
// Handle reaction (like) - send to relays
|
||||
if let Some(filled) = ctx.accounts.selected_filled() {
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn for reaction");
|
||||
|
||||
// Send reaction event using existing infrastructure
|
||||
if let Err(err) = crate::actionbar::send_reaction_event(
|
||||
ctx.ndb,
|
||||
&txn,
|
||||
ctx.pool,
|
||||
filled,
|
||||
&react_action,
|
||||
) {
|
||||
error!("Failed to send reaction: {err}");
|
||||
} else {
|
||||
// Mark reaction as sent in UI
|
||||
ui.ctx().data_mut(|d| {
|
||||
use notedeck::note::reaction_sent_id;
|
||||
d.insert_temp(
|
||||
reaction_sent_id(&filled.pubkey, react_action.note_id.bytes()),
|
||||
true,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
NoteAction::Repost(note_id) => {
|
||||
// For now, open thread panel - could add repost dialog later
|
||||
app.thread_panel.open(*note_id.bytes());
|
||||
}
|
||||
_ => {
|
||||
// Other actions not yet supported in chat view
|
||||
}
|
||||
}
|
||||
process_chat_action(action, app, ctx, ui);
|
||||
}
|
||||
|
||||
// vertical line
|
||||
|
||||
@@ -144,7 +144,13 @@ impl ChannelList {
|
||||
timeline_cache: &mut TimelineCache,
|
||||
ctx: &mut AppContext,
|
||||
) {
|
||||
let txn = Transaction::new(ctx.ndb).unwrap();
|
||||
let txn = match Transaction::new(ctx.ndb) {
|
||||
Ok(txn) => txn,
|
||||
Err(e) => {
|
||||
error!("Failed to create transaction for channel subscription: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for channel in &mut self.channels {
|
||||
// Skip if already subscribed
|
||||
@@ -268,15 +274,19 @@ impl ChannelsCache {
|
||||
}
|
||||
|
||||
pub fn fallback(&self) -> &ChannelList {
|
||||
// SAFETY: fallback_pubkey is always initialized in ChannelsCache::new()
|
||||
// and the entry is created in the constructor, so this should never fail
|
||||
self.account_to_channels
|
||||
.get(&self.fallback_pubkey)
|
||||
.expect("fallback channel list not found")
|
||||
.expect("fallback channel list not found - this is a bug in ChannelsCache initialization")
|
||||
}
|
||||
|
||||
pub fn fallback_mut(&mut self) -> &mut ChannelList {
|
||||
// SAFETY: fallback_pubkey is always initialized in ChannelsCache::new()
|
||||
// and the entry is created in the constructor, so this should never fail
|
||||
self.account_to_channels
|
||||
.get_mut(&self.fallback_pubkey)
|
||||
.expect("fallback channel list not found")
|
||||
.expect("fallback channel list not found - this is a bug in ChannelsCache initialization")
|
||||
}
|
||||
|
||||
pub fn add_channel_for_account(
|
||||
|
||||
@@ -71,9 +71,7 @@ impl ChannelDialog {
|
||||
);
|
||||
|
||||
// Auto-focus on name field when opened
|
||||
if name_response.changed() {
|
||||
name_response.request_focus();
|
||||
}
|
||||
name_response.request_focus();
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
@@ -95,7 +93,7 @@ impl ChannelDialog {
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.add(
|
||||
let hashtags_response = ui.add(
|
||||
TextEdit::multiline(&mut self.hashtags)
|
||||
.hint_text(tr!(
|
||||
i18n,
|
||||
@@ -106,14 +104,36 @@ impl ChannelDialog {
|
||||
.desired_rows(3),
|
||||
);
|
||||
|
||||
// Handle Escape key to close dialog
|
||||
ui.input(|i| {
|
||||
if i.key_pressed(egui::Key::Escape) {
|
||||
action = Some(ChannelDialogAction::Cancel);
|
||||
}
|
||||
// Handle Enter key when name is focused
|
||||
if i.key_pressed(egui::Key::Enter) && !hashtags_response.has_focus() {
|
||||
if !self.name.trim().is_empty() {
|
||||
let hashtags: Vec<String> = self
|
||||
.hashtags
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
action = Some(ChannelDialogAction::Create {
|
||||
name: self.name.trim().to_string(),
|
||||
hashtags,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(24.0);
|
||||
|
||||
// Buttons
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Create button
|
||||
let create_enabled = !self.name.trim().is_empty()
|
||||
&& !self.hashtags.trim().is_empty();
|
||||
// Create button (hashtags are optional)
|
||||
let create_enabled = !self.name.trim().is_empty();
|
||||
|
||||
let create_button = egui::Button::new(
|
||||
RichText::new(tr!(i18n, "Create", "Button to create channel"))
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
use egui::{Color32, Key, Modifiers, RichText, ScrollArea, TextEdit, Vec2};
|
||||
|
||||
use notedeck::{tr, Accounts, Localization};
|
||||
|
||||
use crate::channels::ChannelsCache;
|
||||
|
||||
pub struct ChannelSwitcher {
|
||||
pub is_open: bool,
|
||||
pub search_query: String,
|
||||
pub selected_index: usize,
|
||||
}
|
||||
|
||||
pub enum ChannelSwitcherAction {
|
||||
SelectChannel(usize),
|
||||
Close,
|
||||
}
|
||||
|
||||
impl ChannelSwitcher {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_open: false,
|
||||
search_query: String::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(&mut self) {
|
||||
self.is_open = true;
|
||||
self.search_query.clear();
|
||||
self.selected_index = 0;
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
self.is_open = false;
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
i18n: &mut Localization,
|
||||
channels_cache: &ChannelsCache,
|
||||
accounts: &Accounts,
|
||||
) -> Option<ChannelSwitcherAction> {
|
||||
if !self.is_open {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut action: Option<ChannelSwitcherAction> = None;
|
||||
|
||||
// Modal background
|
||||
egui::Area::new(egui::Id::new("channel_switcher_overlay"))
|
||||
.fixed_pos(egui::Pos2::ZERO)
|
||||
.interactable(true)
|
||||
.show(ctx, |ui| {
|
||||
let screen_rect = ctx.screen_rect();
|
||||
ui.allocate_ui_at_rect(screen_rect, |ui| {
|
||||
// Dark overlay
|
||||
ui.painter().rect_filled(
|
||||
screen_rect,
|
||||
0.0,
|
||||
Color32::from_black_alpha(180),
|
||||
);
|
||||
|
||||
// Handle click on overlay to close
|
||||
if ui.interact(screen_rect, egui::Id::new("overlay"), egui::Sense::click()).clicked() {
|
||||
action = Some(ChannelSwitcherAction::Close);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Switcher window
|
||||
egui::Window::new(tr!(i18n, "Quick Switcher", "Channel switcher dialog title"))
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.title_bar(false)
|
||||
.anchor(egui::Align2::CENTER_TOP, Vec2::new(0.0, 100.0))
|
||||
.fixed_size(Vec2::new(500.0, 400.0))
|
||||
.show(ctx, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Search input
|
||||
let search_response = ui.add(
|
||||
TextEdit::singleline(&mut self.search_query)
|
||||
.hint_text(tr!(
|
||||
i18n,
|
||||
"Search channels...",
|
||||
"Placeholder for channel search"
|
||||
))
|
||||
.desired_width(f32::INFINITY)
|
||||
.font(egui::TextStyle::Body),
|
||||
);
|
||||
|
||||
// Auto-focus search field
|
||||
search_response.request_focus();
|
||||
|
||||
// Handle keyboard navigation
|
||||
ui.input(|i| {
|
||||
if i.key_pressed(Key::Escape) {
|
||||
action = Some(ChannelSwitcherAction::Close);
|
||||
}
|
||||
|
||||
if i.key_pressed(Key::ArrowDown) {
|
||||
let channels = channels_cache.active_channels(accounts);
|
||||
if self.selected_index < channels.num_channels().saturating_sub(1) {
|
||||
self.selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if i.key_pressed(Key::ArrowUp) {
|
||||
self.selected_index = self.selected_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
if i.key_pressed(Key::Enter) {
|
||||
action = Some(ChannelSwitcherAction::SelectChannel(self.selected_index));
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Channel list
|
||||
ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
let channels = channels_cache.active_channels(accounts);
|
||||
let query_lower = self.search_query.to_lowercase();
|
||||
|
||||
let mut visible_idx = 0;
|
||||
for (idx, channel) in channels.channels.iter().enumerate() {
|
||||
// Filter by search query
|
||||
if !query_lower.is_empty()
|
||||
&& !channel.name.to_lowercase().contains(&query_lower)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_selected = visible_idx == self.selected_index;
|
||||
let is_current = idx == channels.selected;
|
||||
|
||||
let mut frame = egui::Frame::new()
|
||||
.inner_margin(egui::Margin::symmetric(12, 8))
|
||||
.corner_radius(4.0);
|
||||
|
||||
if is_selected {
|
||||
frame = frame.fill(ui.visuals().selection.bg_fill);
|
||||
} else if is_current {
|
||||
frame = frame.fill(ui.visuals().faint_bg_color);
|
||||
}
|
||||
|
||||
let response = frame.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// Channel icon
|
||||
ui.label(RichText::new("# ").size(16.0));
|
||||
|
||||
// Channel name
|
||||
let mut text = RichText::new(&channel.name).size(14.0);
|
||||
if is_selected {
|
||||
text = text.strong();
|
||||
}
|
||||
ui.label(text);
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Show unread badge if any
|
||||
if channel.unread_count > 0 {
|
||||
let count_text = if channel.unread_count > 99 {
|
||||
"99+".to_string()
|
||||
} else {
|
||||
channel.unread_count.to_string()
|
||||
};
|
||||
|
||||
ui.label(
|
||||
RichText::new(count_text)
|
||||
.size(11.0)
|
||||
.color(ui.visuals().strong_text_color()),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle click on channel
|
||||
let full_response = ui.interact(
|
||||
response.response.rect,
|
||||
egui::Id::new(("channel_item", idx)),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
|
||||
if full_response.clicked() {
|
||||
action = Some(ChannelSwitcherAction::SelectChannel(idx));
|
||||
}
|
||||
|
||||
// Update selected index on hover
|
||||
if full_response.hovered() {
|
||||
self.selected_index = visible_idx;
|
||||
}
|
||||
|
||||
visible_idx += 1;
|
||||
}
|
||||
|
||||
// Show empty state if no results
|
||||
if visible_idx == 0 && !query_lower.is_empty() {
|
||||
ui.add_space(20.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(tr!(
|
||||
i18n,
|
||||
"No channels found",
|
||||
"Empty search results"
|
||||
))
|
||||
.size(14.0)
|
||||
.color(ui.visuals().weak_text_color()),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Help text
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.0;
|
||||
ui.label(
|
||||
RichText::new(tr!(i18n, "↑↓ to navigate", "Keyboard shortcut hint"))
|
||||
.size(11.0)
|
||||
.color(ui.visuals().weak_text_color()),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(tr!(i18n, "↵ to select", "Keyboard shortcut hint"))
|
||||
.size(11.0)
|
||||
.color(ui.visuals().weak_text_color()),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(tr!(i18n, "esc to close", "Keyboard shortcut hint"))
|
||||
.size(11.0)
|
||||
.color(ui.visuals().weak_text_color()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Close switcher if action was taken
|
||||
if action.is_some() {
|
||||
self.close();
|
||||
}
|
||||
|
||||
action
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChannelSwitcher {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -281,7 +281,7 @@ impl<'a, 'd> ChatView<'a, 'd> {
|
||||
}
|
||||
|
||||
// Timestamp
|
||||
let timestamp = format_timestamp(note.created_at());
|
||||
let timestamp = format_timestamp(note.created_at(), self.note_context.i18n);
|
||||
ui.label(
|
||||
RichText::new(timestamp)
|
||||
.size(12.0)
|
||||
@@ -463,26 +463,33 @@ impl<'a, 'd> ChatView<'a, 'd> {
|
||||
}
|
||||
|
||||
/// Format timestamp as relative time (e.g., "5m ago", "2h ago", "Yesterday")
|
||||
fn format_timestamp(created_at: u64) -> String {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
fn format_timestamp(created_at: u64, i18n: &mut notedeck::Localization) -> String {
|
||||
let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||
Ok(duration) => duration.as_secs(),
|
||||
Err(_) => {
|
||||
// System time is before Unix epoch - extremely rare but handle gracefully
|
||||
return tr!(i18n, "Unknown time", "Fallback when system time is invalid").to_string();
|
||||
}
|
||||
};
|
||||
|
||||
let diff = now.saturating_sub(created_at);
|
||||
|
||||
if diff < 60 {
|
||||
"Just now".to_string()
|
||||
tr!(i18n, "Just now", "Time less than 1 minute ago").to_string()
|
||||
} else if diff < 3600 {
|
||||
format!("{}m ago", diff / 60)
|
||||
let minutes = diff / 60;
|
||||
format!("{minutes}m {}", tr!(i18n, "ago", "Time suffix"))
|
||||
} else if diff < 86400 {
|
||||
format!("{}h ago", diff / 3600)
|
||||
let hours = diff / 3600;
|
||||
format!("{hours}h {}", tr!(i18n, "ago", "Time suffix"))
|
||||
} else if diff < 172800 {
|
||||
"Yesterday".to_string()
|
||||
tr!(i18n, "Yesterday", "One day ago").to_string()
|
||||
} else if diff < 604800 {
|
||||
format!("{}d ago", diff / 86400)
|
||||
let days = diff / 86400;
|
||||
format!("{days}d {}", tr!(i18n, "ago", "Time suffix"))
|
||||
} else {
|
||||
// Simple date format without chrono dependency
|
||||
format!("{} days ago", diff / 86400)
|
||||
let days = diff / 86400;
|
||||
format!("{} {} {}", days, tr!(i18n, "days", "Plural days"), tr!(i18n, "ago", "Time suffix"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ pub mod accounts;
|
||||
pub mod add_column;
|
||||
pub mod channel_dialog;
|
||||
pub mod channel_sidebar;
|
||||
pub mod channel_switcher;
|
||||
pub mod chat_view;
|
||||
pub mod column;
|
||||
pub mod configure_deck;
|
||||
@@ -31,7 +30,6 @@ pub mod widgets;
|
||||
pub use accounts::AccountsView;
|
||||
pub use channel_dialog::{ChannelDialog, ChannelDialogAction};
|
||||
pub use channel_sidebar::{ChannelSidebar, ChannelSidebarAction};
|
||||
pub use channel_switcher::{ChannelSwitcher, ChannelSwitcherAction};
|
||||
pub use chat_view::ChatView;
|
||||
pub use note::{PostReplyView, PostView};
|
||||
pub use preview::{Preview, PreviewApp, PreviewConfig};
|
||||
|
||||
@@ -129,6 +129,7 @@ impl ThreadPanel {
|
||||
|
||||
// Thread content
|
||||
if let Some(thread_id) = &self.selected_thread_id {
|
||||
// ThreadView will handle the case where thread doesn't exist
|
||||
let thread_resp = ThreadView::new(
|
||||
threads,
|
||||
thread_id,
|
||||
|
||||
Reference in New Issue
Block a user