Add channel creation dialog

Implement a channel creation dialog that allows users to:
- Enter a channel name
- Specify comma-separated hashtags to track
- Create and subscribe to new channels

When a channel is created:
1. It's added to the active channels list
2. Saved to disk for persistence
3. Automatically subscribed to start fetching messages

The dialog uses egui::Window for a modal-like experience and
includes validation to ensure both name and hashtags are provided.
This commit is contained in:
Claude
2025-11-12 19:07:16 +00:00
parent 352293b64b
commit 829cca9864
3 changed files with 225 additions and 1 deletions

View File

@@ -50,6 +50,7 @@ pub struct Damus {
pub decks_cache: DecksCache,
pub channels_cache: crate::channels::ChannelsCache,
pub relay_config: crate::relay_config::RelayConfig,
pub channel_dialog: ui::ChannelDialog,
pub view_state: ViewState,
pub drafts: Drafts,
pub timeline_cache: TimelineCache,
@@ -399,6 +400,53 @@ fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
// Show channel creation dialog
if let Some(dialog_action) = damus.channel_dialog.show(ui.ctx(), app_ctx.i18n) {
match dialog_action {
ui::ChannelDialogAction::Create { name, hashtags } => {
// Create new channel
let channel = crate::channels::Channel::new(name, hashtags);
// Add to active channels
damus
.channels_cache
.active_channels_mut(app_ctx.i18n, app_ctx.accounts)
.add_channel(channel);
// Save channels cache
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 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 {
if let Some(result) = damus.timeline_cache.open(
&mut damus.subscriptions,
app_ctx.ndb,
app_ctx.note_cache,
&txn,
app_ctx.pool,
&channel.timeline_kind,
) {
result.process(
app_ctx.ndb,
app_ctx.note_cache,
&txn,
&mut damus.timeline_cache,
app_ctx.unknown_ids,
);
channel.subscribed = true;
}
}
}
}
ui::ChannelDialogAction::Cancel => {
// Dialog was canceled, nothing to do
}
}
}
// We use this for keeping timestamps and things up to date
//ui.ctx().request_repaint_after(Duration::from_secs(5));
@@ -587,6 +635,7 @@ impl Damus {
decks_cache,
channels_cache,
relay_config,
channel_dialog: ui::ChannelDialog::default(),
unrecognized_args,
jobs,
threads,
@@ -642,6 +691,7 @@ impl Damus {
decks_cache,
channels_cache,
relay_config,
channel_dialog: ui::ChannelDialog::default(),
unrecognized_args: BTreeSet::default(),
jobs: JobsCache::default(),
threads: Threads::default(),
@@ -1061,7 +1111,7 @@ fn timelines_view(
storage::save_channels_cache(ctx.path, &app.channels_cache);
}
ChannelSidebarAction::AddChannel => {
// TODO: Show add channel dialog
app.channel_dialog.open();
}
}
}

View File

@@ -0,0 +1,172 @@
use egui::{RichText, TextEdit, Vec2};
use notedeck::{tr, Localization};
pub struct ChannelDialog {
pub name: String,
pub hashtags: String,
pub is_open: bool,
}
pub enum ChannelDialogAction {
Create { name: String, hashtags: Vec<String> },
Cancel,
}
impl ChannelDialog {
pub fn new() -> Self {
Self {
name: String::new(),
hashtags: String::new(),
is_open: false,
}
}
pub fn open(&mut self) {
self.is_open = true;
self.name.clear();
self.hashtags.clear();
}
pub fn close(&mut self) {
self.is_open = false;
}
pub fn show(
&mut self,
ctx: &egui::Context,
i18n: &mut Localization,
) -> Option<ChannelDialogAction> {
if !self.is_open {
return None;
}
let mut action: Option<ChannelDialogAction> = None;
egui::Window::new(tr!(i18n, "Create Channel", "Dialog title for creating a new channel"))
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, Vec2::ZERO)
.fixed_size(Vec2::new(400.0, 300.0))
.show(ctx, |ui| {
ui.vertical(|ui| {
ui.add_space(16.0);
// Channel name input
ui.label(
RichText::new(tr!(i18n, "Channel Name", "Label for channel name input"))
.size(14.0)
.strong(),
);
ui.add_space(8.0);
let name_response = ui.add(
TextEdit::singleline(&mut self.name)
.hint_text(tr!(
i18n,
"e.g., General, Bitcoin, News...",
"Placeholder for channel name"
))
.desired_width(f32::INFINITY),
);
// Auto-focus on name field when opened
if name_response.changed() {
name_response.request_focus();
}
ui.add_space(16.0);
// Hashtags input
ui.label(
RichText::new(tr!(i18n, "Hashtags", "Label for hashtags input"))
.size(14.0)
.strong(),
);
ui.add_space(4.0);
ui.label(
RichText::new(tr!(
i18n,
"Comma-separated hashtags to track",
"Help text for hashtags input"
))
.size(12.0)
.color(ui.visuals().weak_text_color()),
);
ui.add_space(8.0);
ui.add(
TextEdit::multiline(&mut self.hashtags)
.hint_text(tr!(
i18n,
"e.g., bitcoin, nostr, news",
"Placeholder for hashtags"
))
.desired_width(f32::INFINITY)
.desired_rows(3),
);
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();
let create_button = egui::Button::new(
RichText::new(tr!(i18n, "Create", "Button to create channel"))
.size(14.0),
)
.min_size(Vec2::new(80.0, 32.0));
let create_response = ui.add_enabled(create_enabled, create_button);
if create_response.clicked() {
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(8.0);
// Cancel button
let cancel_button = egui::Button::new(
RichText::new(tr!(i18n, "Cancel", "Button to cancel"))
.size(14.0)
.color(ui.visuals().weak_text_color()),
)
.frame(false)
.min_size(Vec2::new(80.0, 32.0));
if ui.add(cancel_button).clicked() {
action = Some(ChannelDialogAction::Cancel);
}
});
});
});
});
// Close dialog if action was taken
if action.is_some() {
self.close();
}
action
}
}
impl Default for ChannelDialog {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,6 +1,7 @@
pub mod account_login_view;
pub mod accounts;
pub mod add_column;
pub mod channel_dialog;
pub mod channel_sidebar;
pub mod chat_view;
pub mod column;
@@ -26,6 +27,7 @@ pub mod wallet;
pub mod widgets;
pub use accounts::AccountsView;
pub use channel_dialog::{ChannelDialog, ChannelDialogAction};
pub use channel_sidebar::{ChannelSidebar, ChannelSidebarAction};
pub use chat_view::ChatView;
pub use note::{PostReplyView, PostView};