diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index 3277058..2434e48 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -12,7 +12,12 @@ use crate::{ support::Support, timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind}, toolbar::unseen_notification, - ui::{self, toolbar::toolbar, DesktopSidePanel, SidePanelAction}, + ui::{ + self, + channel_sidebar::CHANNEL_SIDEBAR_WIDTH, + toolbar::toolbar, + ChannelSidebar, ChannelSidebarAction, DesktopSidePanel, SidePanelAction, + }, view_state::ViewState, Result, }; @@ -524,7 +529,7 @@ impl Damus { let threads = Threads::default(); // Load or create channels cache - let channels_cache = if let Some(channels_cache) = crate::storage::load_channels_cache( + let mut channels_cache = if let Some(channels_cache) = crate::storage::load_channels_cache( app_context.path, app_context.i18n, ) { @@ -535,6 +540,19 @@ impl Damus { crate::channels::ChannelsCache::default_channels_cache(app_context.i18n) }; + // Subscribe to all active channels to fetch data from relays + { + let active_channels = channels_cache.active_channels_mut( + app_context.i18n, + app_context.accounts, + ); + active_channels.subscribe_all( + &mut subscriptions, + &mut timeline_cache, + app_context, + ); + } + // Load or create relay config let relay_config = if let Some(relay_config) = crate::storage::load_relay_config( app_context.path, @@ -546,6 +564,16 @@ impl Damus { crate::relay_config::RelayConfig::default() }; + // Apply relay config to pool - connect to configured relays + for relay_url in relay_config.get_relays() { + let wakeup = || {}; // Wakeup closure for relay events + if let Err(e) = app_context.pool.add_url(relay_url.clone(), wakeup) { + error!("Failed to add relay {}: {}", relay_url, e); + } else { + info!("Added relay from config: {}", relay_url); + } + } + Self { subscriptions, timeline_cache, @@ -861,15 +889,38 @@ fn timelines_view( ) -> AppResponse { let num_cols = get_active_columns(ctx.accounts, &app.decks_cache).num_columns(); let mut side_panel_action: Option = None; + let mut channel_sidebar_action: Option = None; let mut responses = Vec::with_capacity(num_cols); let mut can_take_drag_from = Vec::new(); StripBuilder::new(ui) + .size(Size::exact(CHANNEL_SIDEBAR_WIDTH)) .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH)) .sizes(sizes, num_cols) .clip(true) .horizontal(|mut strip| { + // Channel Sidebar + strip.cell(|ui| { + let rect = ui.available_rect_before_wrap(); + let mut channel_sidebar = + ChannelSidebar::new(&app.channels_cache, ctx.accounts, ctx.i18n); + + if let Some(response) = channel_sidebar.show(ui) { + if response.response.clicked() { + channel_sidebar_action = Some(response.action); + } + } + + // vertical sidebar line + ui.painter().vline( + rect.right(), + rect.y_range(), + ui.visuals().widgets.noninteractive.bg_stroke, + ); + }); + + // Desktop Side Panel strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); let side_panel = DesktopSidePanel::new( @@ -952,6 +1003,22 @@ fn timelines_view( ); } + // process channel sidebar action + if let Some(action) = channel_sidebar_action { + match action { + ChannelSidebarAction::SelectChannel(idx) => { + app.channels_cache + .active_channels_mut(ctx.i18n, ctx.accounts) + .select_channel(idx); + // Save channel state after selection changes + storage::save_channels_cache(ctx.path, &app.channels_cache); + } + ChannelSidebarAction::AddChannel => { + // TODO: Show add channel dialog + } + } + } + let mut app_action: Option = None; for response in responses { diff --git a/crates/notedeck_columns/src/channels.rs b/crates/notedeck_columns/src/channels.rs index 83fa86b..11171dc 100644 --- a/crates/notedeck_columns/src/channels.rs +++ b/crates/notedeck_columns/src/channels.rs @@ -21,6 +21,7 @@ pub struct Channel { pub timeline_kind: TimelineKind, pub router: Router, pub unread_count: usize, + pub subscribed: bool, } impl Channel { @@ -36,6 +37,7 @@ impl Channel { timeline_kind, router, unread_count: 0, + subscribed: false, } } @@ -50,6 +52,7 @@ impl Channel { timeline_kind, router, unread_count: 0, + subscribed: false, } } @@ -143,8 +146,13 @@ impl ChannelList { ) { let txn = Transaction::new(ctx.ndb).unwrap(); - for channel in &self.channels { - if let Some(_result) = timeline_cache.open( + for channel in &mut self.channels { + // Skip if already subscribed + if channel.subscribed { + continue; + } + + if let Some(result) = timeline_cache.open( subs, ctx.ndb, ctx.note_cache, @@ -152,7 +160,18 @@ impl ChannelList { ctx.pool, &channel.timeline_kind, ) { - // Process results if needed + // Process the result to handle unknown IDs and new notes + result.process( + ctx.ndb, + ctx.note_cache, + &txn, + timeline_cache, + ctx.unknown_ids, + ); + + // Mark channel as subscribed + channel.subscribed = true; + info!("Subscribed to channel: {}", channel.name); } } } @@ -164,9 +183,13 @@ impl ChannelList { ndb: &mut nostrdb::Ndb, pool: &mut enostr::RelayPool, ) { - for channel in &self.channels { + for channel in &mut self.channels { if let Err(err) = timeline_cache.pop(&channel.timeline_kind, ndb, pool) { error!("Failed to unsubscribe from channel timeline: {err}"); + } else { + // Mark channel as unsubscribed + channel.subscribed = false; + info!("Unsubscribed from channel: {}", channel.name); } } } diff --git a/crates/notedeck_columns/src/ui/chat_view.rs b/crates/notedeck_columns/src/ui/chat_view.rs index 002c78a..3050f3c 100644 --- a/crates/notedeck_columns/src/ui/chat_view.rs +++ b/crates/notedeck_columns/src/ui/chat_view.rs @@ -1,12 +1,12 @@ use egui::{ - vec2, Align, Color32, CursorIcon, Layout, Margin, Pos2, Rect, RichText, ScrollArea, Sense, - Stroke, TextStyle, Vec2, + vec2, Align, Color32, CursorIcon, Layout, Margin, RichText, ScrollArea, Sense, + Stroke, }; -use nostrdb::{Note, NoteKey, ProfileRecord, Transaction}; +use nostrdb::{Note, Transaction}; use notedeck::fonts::get_font_size; use notedeck::name::get_display_name; -use notedeck::{tr, JobsCache, Localization, Muted, NoteAction, NoteContext, NotedeckTextStyle}; -use notedeck_ui::{ProfilePic, ProfilePreview}; +use notedeck::{tr, JobsCache, NoteAction, NoteContext, NotedeckTextStyle}; +use notedeck_ui::ProfilePic; use tracing::warn; use crate::nav::BodyResponse; @@ -48,11 +48,10 @@ impl<'a, 'd> ChatView<'a, 'd> { } pub fn ui(&mut self, ui: &mut egui::Ui) -> BodyResponse> { - let timeline = if let Some(tl) = self.timeline_cache.get_mut(self.timeline_id) { - tl - } else { - return BodyResponse::new(None); - }; + // Check that timeline exists + if self.timeline_cache.get(self.timeline_id).is_none() { + return BodyResponse::none(); + } let scroll_id = egui::Id::new(("chat_scroll", self.timeline_id, self.col)); @@ -65,7 +64,16 @@ impl<'a, 'd> ChatView<'a, 'd> { .auto_shrink([false, false]) .show(ui, |ui| { ui.with_layout(Layout::top_down(Align::Min), |ui| { - let notes = timeline.notes(self.note_context.ndb); + let units_len = { + let timeline = if let Some(tl) = self.timeline_cache.get(self.timeline_id) { + tl + } else { + warn!("Timeline missing in chat view"); + return; + }; + timeline.current_view().units.len() + }; + let txn = if let Ok(txn) = Transaction::new(self.note_context.ndb) { txn } else { @@ -73,7 +81,7 @@ impl<'a, 'd> ChatView<'a, 'd> { return; }; - if notes.is_empty() { + if units_len == 0 { // Empty state ui.add_space(50.0); ui.vertical_centered(|ui| { @@ -102,8 +110,33 @@ impl<'a, 'd> ChatView<'a, 'd> { let mut last_author: Option> = None; let mut last_timestamp: u64 = 0; - for note_key in notes { - let note = if let Ok(note) = self.note_context.ndb.get_note_by_key(&txn, *note_key) { + for i in 0..units_len { + let note_key = { + let timeline = if let Some(tl) = self.timeline_cache.get(self.timeline_id) { + tl + } else { + continue; + }; + + let unit = if let Some(u) = timeline.current_view().units.get(i) { + u + } else { + continue; + }; + + // Extract the note key from the unit + match unit { + crate::timeline::NoteUnit::Single(note_ref) => note_ref.key, + crate::timeline::NoteUnit::Composite(composite) => { + match composite { + crate::timeline::CompositeUnit::Reaction(r) => r.note_reacted_to.key, + crate::timeline::CompositeUnit::Repost(r) => r.note_reposted.key, + } + } + } + }; + + let note = if let Ok(note) = self.note_context.ndb.get_note_by_key(&txn, note_key) { note } else { continue; @@ -141,7 +174,7 @@ impl<'a, 'd> ChatView<'a, 'd> { }); }); - BodyResponse::new(note_action) + BodyResponse::output(Some(note_action)) } fn render_message( @@ -168,13 +201,15 @@ impl<'a, 'd> ChatView<'a, 'd> { .ok(); let resp = ui.add( - ProfilePic::new(self.jobs, note.pubkey()) - .size(AVATAR_SIZE) - .profile(profile.as_ref().map(|p| p.record().profile())), + &mut ProfilePic::from_profile_or_default( + self.note_context.img_cache, + profile.as_ref() + ) + .size(AVATAR_SIZE) ); if resp.clicked() { - note_action = Some(NoteAction::Profile(note.pubkey().bytes_vec())); + note_action = Some(NoteAction::Profile(enostr::Pubkey::new(*note.pubkey()))); } } else { // Just spacing for grouped messages @@ -185,13 +220,8 @@ impl<'a, 'd> ChatView<'a, 'd> { // Message content column ui.with_layout(Layout::top_down(Align::Min), |ui| { - let max_rect = ui.max_rect(); - let content_max_rect = Rect::from_min_size( - max_rect.min, - vec2(max_bubble_width, max_rect.height()), - ); - - ui.set_max_rect(content_max_rect); + // Constrain the content width for message bubbles + ui.set_max_width(max_bubble_width); if show_header { // Message header: name + timestamp @@ -221,7 +251,7 @@ impl<'a, 'd> ChatView<'a, 'd> { .get_profile_by_pubkey(txn, note.pubkey()) .ok(); - let display_name = get_display_name(profile.as_ref().map(|p| p.record())); + let display_name = get_display_name(profile.as_ref()); let name_response = ui.add( egui::Label::new(