mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-17 08:44:20 +01:00
Add relay-specific column with hashtag filtering
This commit adds the ability to create columns that display content from a specific relay, with optional hashtag filtering. Features: - New TimelineKind::Relay variant storing relay URL and optional hashtags - Relay-specific subscription support in RelayPool (subscribe_to method) - UI for configuring relay URL and hashtag filters - Filter generation for relay-specific queries - Column header with relay icon - Serialization/deserialization support for deck persistence Implementation details: - Extended RelayPool with subscribe_to() for relay-specific subscriptions - Added TimelineSub::try_add_remote_with_relay() to handle targeted subscriptions - Timeline cache automatically routes relay columns to specific relays - UI validates relay URLs before creating columns - Hashtag filtering is optional and space-separated Usage: Users can now add a "Relay" column from the column picker, enter a relay URL (e.g., wss://relay.example.com), and optionally filter by hashtags.
This commit is contained in:
@@ -219,6 +219,26 @@ impl RelayPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subscribe to a specific relay by URL
|
||||||
|
pub fn subscribe_to(&mut self, relay_url: &str, subid: String, filter: Vec<Filter>) {
|
||||||
|
for relay in &mut self.relays {
|
||||||
|
if relay.url() == relay_url {
|
||||||
|
if let Some(debug) = &mut self.debug {
|
||||||
|
debug.send_cmd(
|
||||||
|
relay.url().to_owned(),
|
||||||
|
&ClientMessage::req(subid.clone(), filter.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = relay.send(&ClientMessage::req(subid, filter)) {
|
||||||
|
error!("error subscribing to {}: {err}", relay.url());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error!("relay {} not found in pool", relay_url);
|
||||||
|
}
|
||||||
|
|
||||||
/// Keep relay connectiongs alive by pinging relays that haven't been
|
/// Keep relay connectiongs alive by pinging relays that haven't been
|
||||||
/// pinged in awhile. Adjust ping rate with [`ping_rate`].
|
/// pinged in awhile. Adjust ping rate with [`ping_rate`].
|
||||||
pub fn keepalive_ping(&mut self, wakeup: impl Fn() + Send + Sync + Clone + 'static) {
|
pub fn keepalive_ping(&mut self, wakeup: impl Fn() + Send + Sync + Clone + 'static) {
|
||||||
|
|||||||
@@ -763,6 +763,7 @@ fn should_show_compose_button(decks: &DecksCache, accounts: &Accounts) -> bool {
|
|||||||
TimelineKind::Universe => true,
|
TimelineKind::Universe => true,
|
||||||
TimelineKind::Generic(_) => true,
|
TimelineKind::Generic(_) => true,
|
||||||
TimelineKind::Hashtag(_) => true,
|
TimelineKind::Hashtag(_) => true,
|
||||||
|
TimelineKind::Relay(_, _) => true,
|
||||||
|
|
||||||
// no!
|
// no!
|
||||||
TimelineKind::Search(_) => false,
|
TimelineKind::Search(_) => false,
|
||||||
|
|||||||
@@ -384,11 +384,24 @@ impl TimelineSub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_add_remote(&mut self, pool: &mut RelayPool, filter: &HybridFilter) {
|
pub fn try_add_remote(&mut self, pool: &mut RelayPool, filter: &HybridFilter) {
|
||||||
|
self.try_add_remote_with_relay(pool, filter, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_add_remote_with_relay(
|
||||||
|
&mut self,
|
||||||
|
pool: &mut RelayPool,
|
||||||
|
filter: &HybridFilter,
|
||||||
|
relay_url: Option<&str>,
|
||||||
|
) {
|
||||||
let before = self.state.clone();
|
let before = self.state.clone();
|
||||||
match &mut self.state {
|
match &mut self.state {
|
||||||
SubState::NoSub { dependers } => {
|
SubState::NoSub { dependers } => {
|
||||||
let subid = subscriptions::new_sub_id();
|
let subid = subscriptions::new_sub_id();
|
||||||
|
if let Some(relay) = relay_url {
|
||||||
|
pool.subscribe_to(relay, subid.clone(), filter.remote().to_vec());
|
||||||
|
} else {
|
||||||
pool.subscribe(subid.clone(), filter.remote().to_vec());
|
pool.subscribe(subid.clone(), filter.remote().to_vec());
|
||||||
|
}
|
||||||
self.filter = Some(filter.to_owned());
|
self.filter = Some(filter.to_owned());
|
||||||
self.state = SubState::RemoteOnly {
|
self.state = SubState::RemoteOnly {
|
||||||
remote: subid,
|
remote: subid,
|
||||||
@@ -397,7 +410,11 @@ impl TimelineSub {
|
|||||||
}
|
}
|
||||||
SubState::LocalOnly { local, dependers } => {
|
SubState::LocalOnly { local, dependers } => {
|
||||||
let subid = subscriptions::new_sub_id();
|
let subid = subscriptions::new_sub_id();
|
||||||
|
if let Some(relay) = relay_url {
|
||||||
|
pool.subscribe_to(relay, subid.clone(), filter.remote().to_vec());
|
||||||
|
} else {
|
||||||
pool.subscribe(subid.clone(), filter.remote().to_vec());
|
pool.subscribe(subid.clone(), filter.remote().to_vec());
|
||||||
|
}
|
||||||
self.filter = Some(filter.to_owned());
|
self.filter = Some(filter.to_owned());
|
||||||
self.state = SubState::Unified {
|
self.state = SubState::Unified {
|
||||||
unified: UnifiedSubscription {
|
unified: UnifiedSubscription {
|
||||||
|
|||||||
@@ -336,6 +336,11 @@ impl Route {
|
|||||||
"Add Hashtag Column",
|
"Add Hashtag Column",
|
||||||
"Column title for adding hashtag column"
|
"Column title for adding hashtag column"
|
||||||
)),
|
)),
|
||||||
|
AddColumnRoute::Relay => ColumnTitle::formatted(tr!(
|
||||||
|
i18n,
|
||||||
|
"Add Relay Column",
|
||||||
|
"Column title for adding relay column"
|
||||||
|
)),
|
||||||
AddColumnRoute::UndecidedIndividual => ColumnTitle::formatted(tr!(
|
AddColumnRoute::UndecidedIndividual => ColumnTitle::formatted(tr!(
|
||||||
i18n,
|
i18n,
|
||||||
"Subscribe to someone's notes",
|
"Subscribe to someone's notes",
|
||||||
|
|||||||
@@ -220,7 +220,16 @@ impl TimelineCache {
|
|||||||
if let Some(filter) = timeline.filter.get_any_ready() {
|
if let Some(filter) = timeline.filter.get_any_ready() {
|
||||||
debug!("got open with *new* subscription for {:?}", &timeline.kind);
|
debug!("got open with *new* subscription for {:?}", &timeline.kind);
|
||||||
timeline.subscription.try_add_local(ndb, filter);
|
timeline.subscription.try_add_local(ndb, filter);
|
||||||
|
|
||||||
|
// Check if this is a relay-specific timeline
|
||||||
|
match &timeline.kind {
|
||||||
|
TimelineKind::Relay(relay_url, _) => {
|
||||||
|
timeline.subscription.try_add_remote_with_relay(pool, filter, Some(relay_url));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
timeline.subscription.try_add_remote(pool, filter);
|
timeline.subscription.try_add_remote(pool, filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// This should never happen reasoning, self.notes would have
|
// This should never happen reasoning, self.notes would have
|
||||||
// failed above if the filter wasn't ready
|
// failed above if the filter wasn't ready
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ pub enum TimelineKind {
|
|||||||
Generic(u64),
|
Generic(u64),
|
||||||
|
|
||||||
Hashtag(Vec<String>),
|
Hashtag(Vec<String>),
|
||||||
|
|
||||||
|
/// Relay-specific timeline with optional hashtag filtering
|
||||||
|
/// Format: Relay(relay_url, optional_hashtags)
|
||||||
|
Relay(String, Option<Vec<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFS_TOKEN_DEPRECATED: &str = "notifs";
|
const NOTIFS_TOKEN_DEPRECATED: &str = "notifs";
|
||||||
@@ -315,6 +319,7 @@ impl TimelineKind {
|
|||||||
TimelineKind::Universe => None,
|
TimelineKind::Universe => None,
|
||||||
TimelineKind::Generic(_) => None,
|
TimelineKind::Generic(_) => None,
|
||||||
TimelineKind::Hashtag(_ht) => None,
|
TimelineKind::Hashtag(_ht) => None,
|
||||||
|
TimelineKind::Relay(_, _) => None,
|
||||||
TimelineKind::Search(query) => query.author(),
|
TimelineKind::Search(query) => query.author(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,6 +335,7 @@ impl TimelineKind {
|
|||||||
TimelineKind::Universe => true,
|
TimelineKind::Universe => true,
|
||||||
TimelineKind::Generic(_) => true,
|
TimelineKind::Generic(_) => true,
|
||||||
TimelineKind::Hashtag(_ht) => true,
|
TimelineKind::Hashtag(_ht) => true,
|
||||||
|
TimelineKind::Relay(_, _) => true,
|
||||||
TimelineKind::Search(_q) => true,
|
TimelineKind::Search(_q) => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,6 +369,15 @@ impl TimelineKind {
|
|||||||
writer.write_token("hashtag");
|
writer.write_token("hashtag");
|
||||||
writer.write_token(&ht.join(" "));
|
writer.write_token(&ht.join(" "));
|
||||||
}
|
}
|
||||||
|
TimelineKind::Relay(relay_url, hashtags) => {
|
||||||
|
writer.write_token("relay");
|
||||||
|
writer.write_token(relay_url);
|
||||||
|
if let Some(ht) = hashtags {
|
||||||
|
writer.write_token(&ht.join(" "));
|
||||||
|
} else {
|
||||||
|
writer.write_token("");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,6 +443,23 @@ impl TimelineKind {
|
|||||||
let search_query = SearchQuery::parse_from_tokens(p)?;
|
let search_query = SearchQuery::parse_from_tokens(p)?;
|
||||||
Ok(TimelineKind::Search(search_query))
|
Ok(TimelineKind::Search(search_query))
|
||||||
},
|
},
|
||||||
|
|p| {
|
||||||
|
p.parse_token("relay")?;
|
||||||
|
let relay_url = p.pull_token()?.to_string();
|
||||||
|
let hashtags_str = p.pull_token()?;
|
||||||
|
let hashtags = if hashtags_str.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
hashtags_str
|
||||||
|
.split_whitespace()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_lowercase().to_string())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Ok(TimelineKind::Relay(relay_url, hashtags))
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -492,6 +524,31 @@ impl TimelineKind {
|
|||||||
FilterState::ready(filters)
|
FilterState::ready(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TimelineKind::Relay(_relay_url, hashtags) => {
|
||||||
|
let filters = if let Some(hashtags) = hashtags {
|
||||||
|
// Filter by hashtags if provided
|
||||||
|
hashtags
|
||||||
|
.iter()
|
||||||
|
.filter(|tag| !tag.is_empty())
|
||||||
|
.map(|tag| {
|
||||||
|
Filter::new()
|
||||||
|
.kinds([1])
|
||||||
|
.limit(filter::default_limit())
|
||||||
|
.tags([tag.to_lowercase().as_str()], 't')
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
// Otherwise show all notes from the relay
|
||||||
|
vec![Filter::new()
|
||||||
|
.kinds([1])
|
||||||
|
.limit(filter::default_limit())
|
||||||
|
.build()]
|
||||||
|
};
|
||||||
|
|
||||||
|
FilterState::ready(filters)
|
||||||
|
}
|
||||||
|
|
||||||
TimelineKind::Algo(algo_timeline) => match algo_timeline {
|
TimelineKind::Algo(algo_timeline) => match algo_timeline {
|
||||||
AlgoTimeline::LastPerPubkey(list_k) => match list_k {
|
AlgoTimeline::LastPerPubkey(list_k) => match list_k {
|
||||||
ListKind::Contact(pubkey) => last_per_pubkey_filter_state(ndb, pubkey),
|
ListKind::Contact(pubkey) => last_per_pubkey_filter_state(ndb, pubkey),
|
||||||
@@ -583,6 +640,33 @@ impl TimelineKind {
|
|||||||
|
|
||||||
TimelineKind::Hashtag(hashtag) => Some(Timeline::hashtag(hashtag)),
|
TimelineKind::Hashtag(hashtag) => Some(Timeline::hashtag(hashtag)),
|
||||||
|
|
||||||
|
TimelineKind::Relay(relay_url, hashtags) => {
|
||||||
|
let filters = if let Some(ref hashtags) = hashtags {
|
||||||
|
hashtags
|
||||||
|
.iter()
|
||||||
|
.filter(|tag| !tag.is_empty())
|
||||||
|
.map(|tag| {
|
||||||
|
Filter::new()
|
||||||
|
.kinds([1])
|
||||||
|
.limit(filter::default_limit())
|
||||||
|
.tags([tag.as_str()], 't')
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
vec![Filter::new()
|
||||||
|
.kinds([1])
|
||||||
|
.limit(filter::default_limit())
|
||||||
|
.build()]
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Timeline::new(
|
||||||
|
TimelineKind::Relay(relay_url, hashtags),
|
||||||
|
FilterState::ready(filters),
|
||||||
|
TimelineTab::only_notes_and_replies(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
TimelineKind::List(ListKind::Contact(pk)) => Some(Timeline::new(
|
TimelineKind::List(ListKind::Contact(pk)) => Some(Timeline::new(
|
||||||
TimelineKind::contact_list(pk),
|
TimelineKind::contact_list(pk),
|
||||||
contact_filter_state(txn, ndb, &pk),
|
contact_filter_state(txn, ndb, &pk),
|
||||||
@@ -619,6 +703,13 @@ impl TimelineKind {
|
|||||||
ColumnTitle::formatted(tr!(i18n, "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()),
|
||||||
|
TimelineKind::Relay(relay_url, hashtags) => {
|
||||||
|
if let Some(hashtags) = hashtags {
|
||||||
|
ColumnTitle::formatted(format!("{} ({})", relay_url, hashtags.join(" ")))
|
||||||
|
} else {
|
||||||
|
ColumnTitle::formatted(relay_url.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub fn render_timeline_route(
|
|||||||
| TimelineKind::Notifications(_)
|
| TimelineKind::Notifications(_)
|
||||||
| TimelineKind::Universe
|
| TimelineKind::Universe
|
||||||
| TimelineKind::Hashtag(_)
|
| TimelineKind::Hashtag(_)
|
||||||
|
| TimelineKind::Relay(_, _)
|
||||||
| TimelineKind::Generic(_) => {
|
| TimelineKind::Generic(_) => {
|
||||||
let resp =
|
let resp =
|
||||||
ui::TimelineView::new(kind, timeline_cache, note_context, note_options, jobs, col)
|
ui::TimelineView::new(kind, timeline_cache, note_context, note_options, jobs, col)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use egui::{
|
|||||||
pos2, vec2, Align, Color32, FontId, Id, Image, Margin, Pos2, Rect, RichText, ScrollArea,
|
pos2, vec2, Align, Color32, FontId, Id, Image, Margin, Pos2, Rect, RichText, ScrollArea,
|
||||||
Separator, Ui, Vec2, Widget,
|
Separator, Ui, Vec2, Widget,
|
||||||
};
|
};
|
||||||
use enostr::Pubkey;
|
use enostr::{Pubkey, RelayPool};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ pub enum AddColumnResponse {
|
|||||||
UndecidedNotification,
|
UndecidedNotification,
|
||||||
ExternalNotification,
|
ExternalNotification,
|
||||||
Hashtag,
|
Hashtag,
|
||||||
|
Relay,
|
||||||
Algo(AlgoOption),
|
Algo(AlgoOption),
|
||||||
UndecidedIndividual,
|
UndecidedIndividual,
|
||||||
ExternalIndividual,
|
ExternalIndividual,
|
||||||
@@ -59,6 +60,7 @@ enum AddColumnOption {
|
|||||||
Notification(PubkeySource),
|
Notification(PubkeySource),
|
||||||
Contacts(PubkeySource),
|
Contacts(PubkeySource),
|
||||||
UndecidedHashtag,
|
UndecidedHashtag,
|
||||||
|
UndecidedRelay,
|
||||||
UndecidedIndividual,
|
UndecidedIndividual,
|
||||||
ExternalIndividual,
|
ExternalIndividual,
|
||||||
Individual(PubkeySource),
|
Individual(PubkeySource),
|
||||||
@@ -77,6 +79,7 @@ pub enum AddColumnRoute {
|
|||||||
UndecidedNotification,
|
UndecidedNotification,
|
||||||
ExternalNotification,
|
ExternalNotification,
|
||||||
Hashtag,
|
Hashtag,
|
||||||
|
Relay,
|
||||||
Algo(AddAlgoRoute),
|
Algo(AddAlgoRoute),
|
||||||
UndecidedIndividual,
|
UndecidedIndividual,
|
||||||
ExternalIndividual,
|
ExternalIndividual,
|
||||||
@@ -105,6 +108,7 @@ impl AddColumnRoute {
|
|||||||
Self::UndecidedIndividual => &["column", "individual_selection"],
|
Self::UndecidedIndividual => &["column", "individual_selection"],
|
||||||
Self::ExternalIndividual => &["column", "external_individual_selection"],
|
Self::ExternalIndividual => &["column", "external_individual_selection"],
|
||||||
Self::Hashtag => &["column", "hashtag"],
|
Self::Hashtag => &["column", "hashtag"],
|
||||||
|
Self::Relay => &["column", "relay"],
|
||||||
Self::Algo(AddAlgoRoute::Base) => &["column", "algo_selection"],
|
Self::Algo(AddAlgoRoute::Base) => &["column", "algo_selection"],
|
||||||
Self::Algo(AddAlgoRoute::LastPerPubkey) => {
|
Self::Algo(AddAlgoRoute::LastPerPubkey) => {
|
||||||
&["column", "algo_selection", "last_per_pubkey"]
|
&["column", "algo_selection", "last_per_pubkey"]
|
||||||
@@ -132,6 +136,7 @@ impl TokenSerializable for AddColumnRoute {
|
|||||||
|p| parse_column_route(p, AddColumnRoute::UndecidedIndividual),
|
|p| parse_column_route(p, AddColumnRoute::UndecidedIndividual),
|
||||||
|p| parse_column_route(p, AddColumnRoute::ExternalIndividual),
|
|p| parse_column_route(p, AddColumnRoute::ExternalIndividual),
|
||||||
|p| parse_column_route(p, AddColumnRoute::Hashtag),
|
|p| parse_column_route(p, AddColumnRoute::Hashtag),
|
||||||
|
|p| parse_column_route(p, AddColumnRoute::Relay),
|
||||||
|p| parse_column_route(p, AddColumnRoute::Algo(AddAlgoRoute::Base)),
|
|p| parse_column_route(p, AddColumnRoute::Algo(AddAlgoRoute::Base)),
|
||||||
|p| parse_column_route(p, AddColumnRoute::Algo(AddAlgoRoute::LastPerPubkey)),
|
|p| parse_column_route(p, AddColumnRoute::Algo(AddAlgoRoute::LastPerPubkey)),
|
||||||
],
|
],
|
||||||
@@ -153,6 +158,7 @@ impl AddColumnOption {
|
|||||||
),
|
),
|
||||||
AddColumnOption::ExternalNotification => AddColumnResponse::ExternalNotification,
|
AddColumnOption::ExternalNotification => AddColumnResponse::ExternalNotification,
|
||||||
AddColumnOption::UndecidedHashtag => AddColumnResponse::Hashtag,
|
AddColumnOption::UndecidedHashtag => AddColumnResponse::Hashtag,
|
||||||
|
AddColumnOption::UndecidedRelay => AddColumnResponse::Relay,
|
||||||
AddColumnOption::UndecidedIndividual => AddColumnResponse::UndecidedIndividual,
|
AddColumnOption::UndecidedIndividual => AddColumnResponse::UndecidedIndividual,
|
||||||
AddColumnOption::ExternalIndividual => AddColumnResponse::ExternalIndividual,
|
AddColumnOption::ExternalIndividual => AddColumnResponse::ExternalIndividual,
|
||||||
AddColumnOption::Individual(pubkey_source) => AddColumnResponse::Timeline(
|
AddColumnOption::Individual(pubkey_source) => AddColumnResponse::Timeline(
|
||||||
@@ -515,6 +521,16 @@ impl<'a> AddColumnView<'a> {
|
|||||||
icon: app_images::hashtag_image(),
|
icon: app_images::hashtag_image(),
|
||||||
option: AddColumnOption::UndecidedHashtag,
|
option: AddColumnOption::UndecidedHashtag,
|
||||||
});
|
});
|
||||||
|
vec.push(ColumnOptionData {
|
||||||
|
title: tr!(self.i18n, "Relay", "Title for relay column"),
|
||||||
|
description: tr!(
|
||||||
|
self.i18n,
|
||||||
|
"View content from a specific relay",
|
||||||
|
"Description for relay column"
|
||||||
|
),
|
||||||
|
icon: app_images::add_relay_image(),
|
||||||
|
option: AddColumnOption::UndecidedRelay,
|
||||||
|
});
|
||||||
vec.push(ColumnOptionData {
|
vec.push(ColumnOptionData {
|
||||||
title: tr!(self.i18n, "Individual", "Title for individual user column"),
|
title: tr!(self.i18n, "Individual", "Title for individual user column"),
|
||||||
description: tr!(
|
description: tr!(
|
||||||
@@ -684,6 +700,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, ctx.i18n, &mut app.view_state.id_string_map),
|
AddColumnRoute::Hashtag => hashtag_ui(ui, ctx.i18n, &mut app.view_state.id_string_map),
|
||||||
|
AddColumnRoute::Relay => relay_ui(ui, ctx.i18n, &mut app.view_state.id_string_map, ctx.pool),
|
||||||
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),
|
||||||
};
|
};
|
||||||
@@ -790,6 +807,12 @@ pub fn render_add_column_routes(
|
|||||||
.router_mut()
|
.router_mut()
|
||||||
.route_to(crate::route::Route::AddColumn(AddColumnRoute::Hashtag));
|
.route_to(crate::route::Route::AddColumn(AddColumnRoute::Hashtag));
|
||||||
}
|
}
|
||||||
|
AddColumnResponse::Relay => {
|
||||||
|
app.columns_mut(ctx.i18n, ctx.accounts)
|
||||||
|
.column_mut(col)
|
||||||
|
.router_mut()
|
||||||
|
.route_to(crate::route::Route::AddColumn(AddColumnRoute::Relay));
|
||||||
|
}
|
||||||
AddColumnResponse::UndecidedIndividual => {
|
AddColumnResponse::UndecidedIndividual => {
|
||||||
app.columns_mut(ctx.i18n, ctx.accounts)
|
app.columns_mut(ctx.i18n, ctx.accounts)
|
||||||
.column_mut(col)
|
.column_mut(col)
|
||||||
@@ -869,6 +892,102 @@ fn sanitize_hashtag(raw_hashtag: &str) -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn relay_ui(
|
||||||
|
ui: &mut Ui,
|
||||||
|
i18n: &mut Localization,
|
||||||
|
id_string_map: &mut HashMap<Id, String>,
|
||||||
|
pool: &RelayPool,
|
||||||
|
) -> Option<AddColumnResponse> {
|
||||||
|
padding(16.0, ui, |ui| {
|
||||||
|
let relay_id = ui.id().with("relay_url");
|
||||||
|
let hashtag_id = ui.id().with("relay_hashtags");
|
||||||
|
|
||||||
|
// Relay URL input
|
||||||
|
ui.label(tr!(i18n, "Relay URL", "Label for relay URL input"));
|
||||||
|
let relay_buffer = id_string_map.entry(relay_id).or_default();
|
||||||
|
let relay_edit = egui::TextEdit::singleline(relay_buffer)
|
||||||
|
.hint_text(
|
||||||
|
RichText::new(tr!(
|
||||||
|
i18n,
|
||||||
|
"wss://relay.example.com",
|
||||||
|
"Placeholder for relay URL input field"
|
||||||
|
))
|
||||||
|
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||||
|
)
|
||||||
|
.vertical_align(Align::Center)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.min_size(Vec2::new(0.0, 40.0))
|
||||||
|
.margin(Margin::same(12));
|
||||||
|
ui.add(relay_edit);
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Hashtag input (optional)
|
||||||
|
ui.label(tr!(i18n, "Hashtags (optional)", "Label for hashtag filter input"));
|
||||||
|
let hashtag_buffer = id_string_map.entry(hashtag_id).or_default();
|
||||||
|
let hashtag_edit = egui::TextEdit::singleline(hashtag_buffer)
|
||||||
|
.hint_text(
|
||||||
|
RichText::new(tr!(
|
||||||
|
i18n,
|
||||||
|
"Enter hashtags to filter (space-separated)",
|
||||||
|
"Placeholder for hashtag filter input field"
|
||||||
|
))
|
||||||
|
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||||
|
)
|
||||||
|
.vertical_align(Align::Center)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.min_size(Vec2::new(0.0, 40.0))
|
||||||
|
.margin(Margin::same(12));
|
||||||
|
ui.add(hashtag_edit);
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
let mut handle_user_input = false;
|
||||||
|
if ui.input(|i| i.key_released(egui::Key::Enter))
|
||||||
|
|| ui
|
||||||
|
.add_sized(egui::vec2(50.0, 40.0), add_column_button(i18n))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
handle_user_input = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone values before validation to avoid borrow issues
|
||||||
|
let relay_value = id_string_map.get(&relay_id).map(|s| s.clone()).unwrap_or_default();
|
||||||
|
let hashtag_value = id_string_map.get(&hashtag_id).map(|s| s.clone()).unwrap_or_default();
|
||||||
|
|
||||||
|
if handle_user_input && !relay_value.is_empty() {
|
||||||
|
// Validate relay URL
|
||||||
|
if !pool.is_valid_url(&relay_value) {
|
||||||
|
// Show error message - for now just don't process
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hashtags = if hashtag_value.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
hashtag_value
|
||||||
|
.split_whitespace()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| sanitize_hashtag(s).to_lowercase().to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = AddColumnResponse::Timeline(TimelineKind::Relay(
|
||||||
|
relay_value,
|
||||||
|
hashtags,
|
||||||
|
));
|
||||||
|
id_string_map.remove(&relay_id);
|
||||||
|
id_string_map.remove(&hashtag_id);
|
||||||
|
Some(resp)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.inner
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -460,6 +460,10 @@ impl<'a> NavTitle<'a> {
|
|||||||
app_images::hashtag_image().fit_to_exact_size(egui::vec2(pfp_size, pfp_size)),
|
app_images::hashtag_image().fit_to_exact_size(egui::vec2(pfp_size, pfp_size)),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
TimelineKind::Relay(_, _) => Some(ui.add(
|
||||||
|
app_images::add_relay_image().fit_to_exact_size(egui::vec2(pfp_size, pfp_size)),
|
||||||
|
)),
|
||||||
|
|
||||||
TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
|
TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
|
||||||
|
|
||||||
TimelineKind::Search(_sq) => {
|
TimelineKind::Search(_sq) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user