mirror of
https://github.com/aljazceru/notedeck.git
synced 2026-02-23 17:34:25 +01:00
Complete Phase 1 & 2: Full Slack-like interface integration
This commit integrates all components to create a functional Slack-like interface: **Phase 1: API Fixes** - Fixed ChatView compilation errors (APIs match current codebase) - Proper BodyResponse, ProfilePic, and timeline access patterns **Phase 2: Integration** - ✅ ChannelSidebar integrated into main layout (240px sidebar) - ✅ Channel subscription logic implemented and working - ✅ RelayConfig applied to RelayPool on startup - ✅ Save triggers for channel modifications - Layout: [ChannelSidebar | DesktopSidePanel | Columns] **Key Features Now Working:** 1. Channel sidebar displays and is clickable 2. Channels subscribe to hashtag filters on app start 3. Relay configuration properly applied 4. Channel selection persists to disk 5. ChatView component compiles (ready for Phase 3 wiring) **Changes:** - app.rs: Added ChannelSidebar rendering, action handling, relay application - channels.rs: Added `subscribed` field, proper subscribe_all() logic - chat_view.rs: Fixed all API mismatches (compiles cleanly) **Next Steps (Phase 3):** - Wire ChatView to display selected channel's timeline - Add channel creation/edit dialog - Build thread side panel for Slack-style threading All code compiles successfully with zero errors!
This commit is contained in:
@@ -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<nav::SwitchingAction> = None;
|
||||
let mut channel_sidebar_action: Option<ChannelSidebarAction> = 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<AppAction> = None;
|
||||
|
||||
for response in responses {
|
||||
|
||||
@@ -21,6 +21,7 @@ pub struct Channel {
|
||||
pub timeline_kind: TimelineKind,
|
||||
pub router: Router<Route>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Option<NoteAction>> {
|
||||
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<Vec<u8>> = 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(
|
||||
|
||||
Reference in New Issue
Block a user