Merge branch 'multifeed'

This commit is contained in:
Mike Dilger 2023-01-03 18:05:41 +13:00
commit 32128e1bea
20 changed files with 829 additions and 458 deletions

1
Cargo.lock generated
View File

@ -1697,6 +1697,7 @@ dependencies = [
"lazy_static", "lazy_static",
"linkify", "linkify",
"nostr-types", "nostr-types",
"parking_lot",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest",
"rusqlite", "rusqlite",

View File

@ -24,6 +24,7 @@ image = "0.24"
lazy_static = "1.4" lazy_static = "1.4"
linkify = "0.9" linkify = "0.9"
nostr-types = { git = "https://github.com/mikedilger/nostr-types" } nostr-types = { git = "https://github.com/mikedilger/nostr-types" }
parking_lot = "0.12"
rand = "0.8" rand = "0.8"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
rusqlite = { version = "0.28", features = ["bundled", "chrono", "serde_json"] } rusqlite = { version = "0.28", features = ["bundled", "chrono", "serde_json"] }

View File

@ -1,55 +1,141 @@
use crate::comms::BusMessage;
use crate::globals::GLOBALS; use crate::globals::GLOBALS;
use nostr_types::PublicKeyHex;
use nostr_types::{Event, EventKind, Id}; use nostr_types::{Event, EventKind, Id};
use parking_lot::RwLock;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub enum FeedKind {
General,
Thread(Id),
Person(PublicKeyHex),
}
pub struct Feed { pub struct Feed {
feed: Vec<Id>, current_feed_kind: RwLock<FeedKind>,
general_feed: RwLock<Vec<Id>>,
// We only recompute the feed at specified intervals // We only recompute the feed at specified intervals
interval_ms: u32, interval_ms: RwLock<u32>,
last_computed: Instant, last_computed: RwLock<Instant>,
// We track these to update subscriptions on them // We track these to update subscriptions on them
my_event_ids: Vec<Id>, my_event_ids: RwLock<Vec<Id>>,
followed_event_ids: Vec<Id>, followed_event_ids: RwLock<Vec<Id>>,
} }
impl Feed { impl Feed {
pub fn new() -> Feed { pub fn new() -> Feed {
Feed { Feed {
feed: Vec::new(), current_feed_kind: RwLock::new(FeedKind::General),
interval_ms: 1000, // Every second, until we load from settings general_feed: RwLock::new(Vec::new()),
last_computed: Instant::now(), interval_ms: RwLock::new(1000), // Every second, until we load from settings
my_event_ids: Vec::new(), last_computed: RwLock::new(Instant::now()),
followed_event_ids: Vec::new(), my_event_ids: RwLock::new(Vec::new()),
followed_event_ids: RwLock::new(Vec::new()),
} }
} }
pub fn get(&mut self) -> Vec<Id> { pub fn set_feed_to_general(&self) {
// We are always subscribed to the general feed. Don't resubscribe here
// because it won't have changed, but the relays will shower you with
// all those events again.
*self.current_feed_kind.write() = FeedKind::General;
}
pub fn set_feed_to_thread(&self, id: Id) {
let _ = GLOBALS.to_minions.send(BusMessage {
target: "all".to_string(),
kind: "subscribe_thread_feed".to_string(),
json_payload: serde_json::to_string(&id).unwrap(),
});
*self.current_feed_kind.write() = FeedKind::Thread(id);
}
pub fn set_feed_to_person(&self, pubkey: PublicKeyHex) {
let _ = GLOBALS.to_minions.send(BusMessage {
target: "all".to_string(),
kind: "subscribe_person_feed".to_string(),
json_payload: serde_json::to_string(&pubkey).unwrap(),
});
*self.current_feed_kind.write() = FeedKind::Person(pubkey);
}
pub fn get_feed_kind(&self) -> FeedKind {
self.current_feed_kind.read().to_owned()
}
pub fn get_general(&self) -> Vec<Id> {
let now = Instant::now(); let now = Instant::now();
if self.last_computed + Duration::from_millis(self.interval_ms as u64) < now { if *self.last_computed.read() + Duration::from_millis(*self.interval_ms.read() as u64) < now
{
self.recompute(); self.recompute();
self.last_computed = now; *self.last_computed.write() = now;
} }
self.feed.clone() self.general_feed.read().clone()
}
pub fn get_thread_parent(&self, id: Id) -> Id {
let mut event = match GLOBALS.events.blocking_read().get(&id).cloned() {
None => return id,
Some(e) => e,
};
// Try for root
if let Some((root, _)) = event.replies_to_root() {
if GLOBALS.events.blocking_read().contains_key(&root) {
return root;
}
}
// Climb parents as high as we can
while let Some((parent, _)) = event.replies_to() {
if let Some(e) = GLOBALS.events.blocking_read().get(&parent) {
event = e.to_owned();
} else {
break;
}
}
// The highest event id we have
event.id
}
pub fn get_person_feed(&self, person: PublicKeyHex) -> Vec<Id> {
let mut events: Vec<Event> = GLOBALS
.events
.blocking_read()
.iter()
.map(|(_, e)| e)
.filter(|e| e.kind == EventKind::TextNote)
.filter(|e| e.pubkey.as_hex_string() == person.0)
.filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id))
.map(|e| e.to_owned())
.collect();
events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at));
events.iter().map(|e| e.id).collect()
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn get_my_event_ids(&self) -> Vec<Id> { pub fn get_my_event_ids(&self) -> Vec<Id> {
// we assume the main get() happens fast enough to recompute for us. // we assume the main get() happens fast enough to recompute for us.
self.my_event_ids.clone() self.my_event_ids.read().clone()
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn get_followed_event_ids(&self) -> Vec<Id> { pub fn get_followed_event_ids(&self) -> Vec<Id> {
// we assume the main get() happens fast enough to recompute for us. // we assume the main get() happens fast enough to recompute for us.
self.followed_event_ids.clone() self.followed_event_ids.read().clone()
} }
fn recompute(&mut self) { fn recompute(&self) {
let settings = GLOBALS.settings.blocking_read().clone(); let settings = GLOBALS.settings.blocking_read().clone();
self.interval_ms = settings.feed_recompute_interval_ms; *self.interval_ms.write() = settings.feed_recompute_interval_ms;
let events: Vec<Event> = GLOBALS let events: Vec<Event> = GLOBALS
.events .events
@ -60,22 +146,26 @@ impl Feed {
.map(|e| e.to_owned()) .map(|e| e.to_owned())
.collect(); .collect();
let mut pubkeys = GLOBALS.people.blocking_read().get_followed_pubkeys();
if let Some(pubkey) = GLOBALS.signer.blocking_read().public_key() {
pubkeys.push(pubkey.into()); // add the user
}
// My event ids // My event ids
if let Some(pubkey) = GLOBALS.signer.blocking_read().public_key() { if let Some(pubkey) = GLOBALS.signer.blocking_read().public_key() {
self.my_event_ids = events *self.my_event_ids.write() = events
.iter() .iter()
.filter_map(|e| if e.pubkey == pubkey { Some(e.id) } else { None }) .filter_map(|e| if e.pubkey == pubkey { Some(e.id) } else { None })
.collect(); .collect();
} else { } else {
self.my_event_ids = vec![]; *self.my_event_ids.write() = vec![];
} }
// Followed event ids // Followed event ids
let followed_pubkeys = GLOBALS.people.blocking_read().get_followed_pubkeys(); *self.followed_event_ids.write() = events
self.followed_event_ids = events
.iter() .iter()
.filter_map(|e| { .filter_map(|e| {
if followed_pubkeys.contains(&e.pubkey.into()) { if pubkeys.contains(&e.pubkey.into()) {
Some(e.id) Some(e.id)
} else { } else {
None None
@ -86,29 +176,14 @@ impl Feed {
// Filter further for the feed // Filter further for the feed
let mut events: Vec<Event> = events let mut events: Vec<Event> = events
.iter() .iter()
.filter(|e| pubkeys.contains(&e.pubkey.into())) // something we follow
.filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id)) .filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id))
.filter(|e| {
if settings.view_threaded {
e.replies_to().is_none()
} else {
true
}
})
.cloned() .cloned()
.collect(); .collect();
if settings.view_threaded { // In time order
events.sort_unstable_by(|a, b| { events.sort_by(|a, b| b.created_at.cmp(&a.created_at));
let a_last = GLOBALS.last_reply.blocking_read().get(&a.id).cloned();
let b_last = GLOBALS.last_reply.blocking_read().get(&b.id).cloned();
let a_time = a_last.unwrap_or(a.created_at);
let b_time = b_last.unwrap_or(b.created_at);
b_time.cmp(&a_time)
});
} else {
events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at));
}
self.feed = events.iter().map(|e| e.id).collect(); *self.general_feed.write() = events.iter().map(|e| e.id).collect();
} }
} }

View File

@ -7,7 +7,7 @@ use crate::people::People;
use crate::relationship::Relationship; use crate::relationship::Relationship;
use crate::settings::Settings; use crate::settings::Settings;
use crate::signer::Signer; use crate::signer::Signer;
use nostr_types::{Event, Id, IdHex, PublicKeyHex, Unixtime, Url}; use nostr_types::{Event, Id, IdHex, PublicKeyHex, Url};
use rusqlite::Connection; use rusqlite::Connection;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
@ -40,10 +40,6 @@ pub struct Globals {
/// All relationships between events /// All relationships between events
pub relationships: RwLock<HashMap<Id, Vec<(Id, Relationship)>>>, pub relationships: RwLock<HashMap<Id, Vec<(Id, Relationship)>>>,
/// The date of the latest reply. Only reply relationships count, not reactions,
/// deletions, or quotes
pub last_reply: RwLock<HashMap<Id, Unixtime>>,
/// Desired events, referred to by others, with possible URLs where we can /// Desired events, referred to by others, with possible URLs where we can
/// get them. We may already have these, but if not we should ask for them. /// get them. We may already have these, but if not we should ask for them.
pub desired_events: RwLock<HashMap<Id, Vec<Url>>>, pub desired_events: RwLock<HashMap<Id, Vec<Url>>>,
@ -71,7 +67,7 @@ pub struct Globals {
pub event_is_new: RwLock<Vec<Id>>, pub event_is_new: RwLock<Vec<Id>>,
/// Feed /// Feed
pub feed: Mutex<Feed>, pub feed: Feed,
/// Fetcher /// Fetcher
pub fetcher: Fetcher, pub fetcher: Fetcher,
@ -97,7 +93,6 @@ lazy_static! {
events: RwLock::new(HashMap::new()), events: RwLock::new(HashMap::new()),
incoming_events: RwLock::new(Vec::new()), incoming_events: RwLock::new(Vec::new()),
relationships: RwLock::new(HashMap::new()), relationships: RwLock::new(HashMap::new()),
last_reply: RwLock::new(HashMap::new()),
desired_events: RwLock::new(HashMap::new()), desired_events: RwLock::new(HashMap::new()),
people: RwLock::new(People::new()), people: RwLock::new(People::new()),
relays: RwLock::new(HashMap::new()), relays: RwLock::new(HashMap::new()),
@ -106,7 +101,7 @@ lazy_static! {
signer: RwLock::new(Signer::default()), signer: RwLock::new(Signer::default()),
dismissed: RwLock::new(Vec::new()), dismissed: RwLock::new(Vec::new()),
event_is_new: RwLock::new(Vec::new()), event_is_new: RwLock::new(Vec::new()),
feed: Mutex::new(Feed::new()), feed: Feed::new(),
fetcher: Fetcher::new(), fetcher: Fetcher::new(),
failed_avatars: RwLock::new(HashSet::new()), failed_avatars: RwLock::new(HashSet::new()),
} }
@ -227,18 +222,6 @@ impl Globals {
.or_insert_with(|| vec![r]); .or_insert_with(|| vec![r]);
} }
pub async fn update_last_reply(id: Id, time: Unixtime) {
let mut last_reply = GLOBALS.last_reply.write().await;
last_reply
.entry(id)
.and_modify(|lasttime| {
if time > *lasttime {
*lasttime = time;
}
})
.or_insert_with(|| time);
}
pub fn get_replies_sync(id: Id) -> Vec<Id> { pub fn get_replies_sync(id: Id) -> Vec<Id> {
let mut output: Vec<Id> = Vec::new(); let mut output: Vec<Id> = Vec::new();
if let Some(vec) = GLOBALS.relationships.blocking_read().get(&id) { if let Some(vec) = GLOBALS.relationships.blocking_read().get(&id) {

View File

@ -1,18 +1,33 @@
use super::Minion; use super::Minion;
use crate::{BusMessage, Error}; use crate::{BusMessage, Error};
use futures::SinkExt; use futures::SinkExt;
use nostr_types::{ClientMessage, Event, IdHex, PublicKeyHex}; use nostr_types::{ClientMessage, Event, Id, IdHex, PublicKeyHex};
use tungstenite::protocol::Message as WsMessage; use tungstenite::protocol::Message as WsMessage;
impl Minion { impl Minion {
pub(super) async fn handle_bus_message( pub(super) async fn handle_bus_message(
&mut self, &mut self,
bus_message: BusMessage, bus_message: BusMessage,
) -> Result<(), Error> { ) -> Result<bool, Error> {
match &*bus_message.kind { match &*bus_message.kind {
"set_followed_people" => { "shutdown" => {
let v: Vec<PublicKeyHex> = serde_json::from_str(&bus_message.json_payload)?; tracing::info!("{}: Websocket listener shutting down", &self.url);
self.upsert_following(v).await?; return Ok(false);
}
//"set_followed_people" => {
// let v: Vec<PublicKeyHex> = serde_json::from_str(&bus_message.json_payload)?;
// self.upsert_following(v).await?;
//}
"subscribe_general_feed" => {
self.subscribe_general_feed().await?;
}
"subscribe_person_feed" => {
let pubkeyhex: PublicKeyHex = serde_json::from_str(&bus_message.json_payload)?;
self.subscribe_person_feed(pubkeyhex).await?;
}
"subscribe_thread_feed" => {
let id: Id = serde_json::from_str(&bus_message.json_payload)?;
self.subscribe_thread_feed(id).await?;
} }
"fetch_events" => { "fetch_events" => {
let v: Vec<IdHex> = serde_json::from_str(&bus_message.json_payload)?; let v: Vec<IdHex> = serde_json::from_str(&bus_message.json_payload)?;
@ -38,6 +53,6 @@ impl Minion {
); );
} }
} }
Ok(()) Ok(true)
} }
} }

View File

@ -29,8 +29,13 @@ impl Minion {
.subscriptions .subscriptions
.get_handle_by_id(&subid.0) .get_handle_by_id(&subid.0)
.unwrap_or_else(|| "_".to_owned()); .unwrap_or_else(|| "_".to_owned());
tracing::trace!("{}: {}: NEW EVENT", &self.url, handle); tracing::debug!("{}: {}: NEW EVENT", &self.url, handle);
// Try processing everything immediately
crate::process::process_new_event(&event, true, Some(self.url.clone()))
.await?;
/*
if event.kind == EventKind::TextNote { if event.kind == EventKind::TextNote {
// Just store text notes in incoming // Just store text notes in incoming
GLOBALS GLOBALS
@ -43,6 +48,8 @@ impl Minion {
crate::process::process_new_event(&event, true, Some(self.url.clone())) crate::process::process_new_event(&event, true, Some(self.url.clone()))
.await?; .await?;
} }
*/
} }
} }
RelayMessage::Notice(msg) => { RelayMessage::Notice(msg) => {

View File

@ -10,8 +10,9 @@ use futures::{SinkExt, StreamExt};
use futures_util::stream::{SplitSink, SplitStream}; use futures_util::stream::{SplitSink, SplitStream};
use http::Uri; use http::Uri;
use nostr_types::{ use nostr_types::{
EventKind, Filter, IdHex, PublicKeyHex, RelayInformationDocument, Unixtime, Url, EventKind, Filter, Id, IdHex, PublicKeyHex, RelayInformationDocument, Unixtime, Url,
}; };
use std::time::Duration;
use subscription::Subscriptions; use subscription::Subscriptions;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::select; use tokio::select;
@ -219,13 +220,8 @@ impl Minion {
Err(e) => return Err(e.into()) Err(e) => return Err(e.into())
}; };
#[allow(clippy::collapsible_if)] #[allow(clippy::collapsible_if)]
if bus_message.target == self.url.inner() { if bus_message.target == self.url.inner() || bus_message.target == "all" {
self.handle_bus_message(bus_message).await?; keepgoing = self.handle_bus_message(bus_message).await?;
} else if &*bus_message.target == "all" {
if &*bus_message.kind == "shutdown" {
tracing::info!("{}: Websocket listener shutting down", &self.url);
keepgoing = false;
}
} }
}, },
} }
@ -243,7 +239,180 @@ impl Minion {
Ok(()) Ok(())
} }
async fn subscribe_general_feed(&mut self) -> Result<(), Error> {
// NOTE if the general feed is already subscribed we shoudn't do anything
// but we may need to update the subscription
let mut filters: Vec<Filter> = Vec::new();
let feed_chunk = GLOBALS.settings.read().await.feed_chunk;
let followed_pubkeys = GLOBALS.people.read().await.get_followed_pubkeys();
if let Some(pubkey) = GLOBALS.signer.read().await.public_key() {
// feed related by me
filters.push(Filter {
authors: vec![pubkey.into()],
kinds: vec![
EventKind::TextNote,
EventKind::Reaction,
EventKind::EventDeletion,
],
since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)),
..Default::default()
});
// Any mentions of me
filters.push(Filter {
p: vec![pubkey.into()],
since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)),
..Default::default()
});
// my metadata
// FIXME TBD
/*
filters.push(Filter {
authors: vec![pubkey],
kinds: vec![EventKind::Metadata, EventKind::RecommendRelay, EventKind::ContactList, EventKind::RelaysList],
since: // last we last checked
.. Default::default()
});
*/
}
if !followed_pubkeys.is_empty() {
// feed related by people followed
filters.push(Filter {
authors: followed_pubkeys.clone(),
kinds: vec![
EventKind::TextNote,
EventKind::Reaction,
EventKind::EventDeletion,
],
since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)),
..Default::default()
});
// metadata by people followed
// FIXME TBD
/*
filters.push(Filter {
authors: pubkeys.clone(),
kinds: vec![EventKind::Metadata, EventKind::RecommendRelay, EventKind::ContactList, EventKind::RelaysList],
since: // last we last checked
.. Default::default()
});
*/
}
// reactions to posts by me
// FIXME TBD
// reactions to posts by people followed
// FIXME TBD
// NO REPLIES OR ANCESTORS
if filters.is_empty() {
self.unsubscribe("general_feed").await?;
} else {
self.subscribe(filters, "general_feed").await?;
}
Ok(())
}
async fn subscribe_person_feed(&mut self, pubkey: PublicKeyHex) -> Result<(), Error> {
// NOTE we do not unsubscribe to the general feed
let mut filters: Vec<Filter> = Vec::new();
let feed_chunk = GLOBALS.settings.read().await.feed_chunk;
// feed related by person
filters.push(Filter {
authors: vec![pubkey],
kinds: vec![
EventKind::TextNote,
EventKind::Reaction,
EventKind::EventDeletion,
],
since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)),
..Default::default()
});
// persons metadata
// FIXME TBD
/*
filters.push(Filter {
authors: vec![pubkey],
kinds: vec![EventKind::Metadata, EventKind::RecommendRelay, EventKind::ContactList, EventKind::RelaysList],
since: // last we last checked
.. Default::default()
});
*/
// reactions to post by person
// FIXME TBD
// NO REPLIES OR ANCESTORS
if filters.is_empty() {
self.unsubscribe("person_feed").await?;
} else {
self.subscribe(filters, "person_feed").await?;
}
Ok(())
}
async fn subscribe_thread_feed(&mut self, id: Id) -> Result<(), Error> {
// NOTE we do not unsubscribe to the general feed
let mut filters: Vec<Filter> = Vec::new();
let feed_chunk = GLOBALS.settings.read().await.feed_chunk;
// This post and ancestors
let mut ids: Vec<IdHex> = vec![id.into()];
// FIXME - We could have this precalculated like GLOBALS.relationships
// in reverse. It would be potentially more complete having
// iteratively climbed the chain.
if let Some(event) = GLOBALS.events.read().await.get(&id) {
for (id, url) in &event.replies_to_ancestors() {
if let Some(url) = url {
if url == &self.url {
ids.push((*id).into());
}
} else {
ids.push((*id).into());
}
}
}
filters.push(Filter {
ids: ids.clone(),
..Default::default()
});
// Replies and reactions to this post and ancestors
filters.push(Filter {
e: ids,
since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)),
..Default::default()
});
// Metadata for people in those events
// TBD
if filters.is_empty() {
self.unsubscribe("thread_feed").await?;
} else {
self.subscribe(filters, "thread_feed").await?;
}
Ok(())
}
// Create or replace the following subscription // Create or replace the following subscription
/*
async fn upsert_following(&mut self, pubkeys: Vec<PublicKeyHex>) -> Result<(), Error> { async fn upsert_following(&mut self, pubkeys: Vec<PublicKeyHex>) -> Result<(), Error> {
let websocket_sink = self.sink.as_mut().unwrap(); let websocket_sink = self.sink.as_mut().unwrap();
@ -354,6 +523,7 @@ impl Minion {
Ok(()) Ok(())
} }
*/
async fn get_events(&mut self, ids: Vec<IdHex>) -> Result<(), Error> { async fn get_events(&mut self, ids: Vec<IdHex>) -> Result<(), Error> {
if ids.is_empty() { if ids.is_empty() {

View File

@ -220,10 +220,10 @@ impl Overlord {
// Fire off a minion to handle this relay // Fire off a minion to handle this relay
self.start_minion(best_relay.relay.url.clone()).await?; self.start_minion(best_relay.relay.url.clone()).await?;
// Tell it to follow the chosen people // Subscribe to the general feed
let _ = self.to_minions.send(BusMessage { let _ = self.to_minions.send(BusMessage {
target: best_relay.relay.url.clone(), target: best_relay.relay.url.clone(),
kind: "set_followed_people".to_string(), kind: "subscribe_general_feed".to_string(),
json_payload: serde_json::to_string(&best_relay.pubkeys).unwrap(), json_payload: serde_json::to_string(&best_relay.pubkeys).unwrap(),
}); });

View File

@ -141,18 +141,6 @@ pub async fn process_new_event(
// Insert into relationships // Insert into relationships
Globals::add_relationship(id, event.id, Relationship::Reply).await; Globals::add_relationship(id, event.id, Relationship::Reply).await;
// Update last_reply
let mut id = id;
Globals::update_last_reply(id, event.created_at).await;
while let Some(ev) = GLOBALS.events.read().await.get(&id).cloned() {
if let Some((pid, _)) = ev.replies_to() {
id = pid;
Globals::update_last_reply(id, event.created_at).await;
} else {
break;
}
}
} }
// We desire all ancestors // We desire all ancestors

View File

@ -7,7 +7,6 @@ pub const DEFAULT_FEED_CHUNK: u64 = 43200; // 12 hours
pub const DEFAULT_OVERLAP: u64 = 600; // 10 minutes pub const DEFAULT_OVERLAP: u64 = 600; // 10 minutes
pub const DEFAULT_VIEW_POSTS_REFERRED_TO: bool = true; pub const DEFAULT_VIEW_POSTS_REFERRED_TO: bool = true;
pub const DEFAULT_VIEW_POSTS_REFERRING_TO: bool = false; pub const DEFAULT_VIEW_POSTS_REFERRING_TO: bool = false;
pub const DEFAULT_VIEW_THREADED: bool = true;
pub const DEFAULT_NUM_RELAYS_PER_PERSON: u8 = 4; pub const DEFAULT_NUM_RELAYS_PER_PERSON: u8 = 4;
pub const DEFAULT_MAX_RELAYS: u8 = 15; pub const DEFAULT_MAX_RELAYS: u8 = 15;
pub const DEFAULT_MAX_FPS: u32 = 30; pub const DEFAULT_MAX_FPS: u32 = 30;
@ -21,7 +20,6 @@ pub struct Settings {
pub overlap: u64, pub overlap: u64,
pub view_posts_referred_to: bool, pub view_posts_referred_to: bool,
pub view_posts_referring_to: bool, pub view_posts_referring_to: bool,
pub view_threaded: bool,
pub num_relays_per_person: u8, pub num_relays_per_person: u8,
pub max_relays: u8, pub max_relays: u8,
pub public_key: Option<PublicKey>, pub public_key: Option<PublicKey>,
@ -39,7 +37,6 @@ impl Default for Settings {
overlap: DEFAULT_OVERLAP, overlap: DEFAULT_OVERLAP,
view_posts_referred_to: DEFAULT_VIEW_POSTS_REFERRED_TO, view_posts_referred_to: DEFAULT_VIEW_POSTS_REFERRED_TO,
view_posts_referring_to: DEFAULT_VIEW_POSTS_REFERRING_TO, view_posts_referring_to: DEFAULT_VIEW_POSTS_REFERRING_TO,
view_threaded: DEFAULT_VIEW_THREADED,
num_relays_per_person: DEFAULT_NUM_RELAYS_PER_PERSON, num_relays_per_person: DEFAULT_NUM_RELAYS_PER_PERSON,
max_relays: DEFAULT_MAX_RELAYS, max_relays: DEFAULT_MAX_RELAYS,
public_key: None, public_key: None,
@ -76,7 +73,6 @@ impl Settings {
"view_posts_referring_to" => { "view_posts_referring_to" => {
settings.view_posts_referring_to = numstr_to_bool(row.1) settings.view_posts_referring_to = numstr_to_bool(row.1)
} }
"view_threaded" => settings.view_threaded = numstr_to_bool(row.1),
"num_relays_per_person" => { "num_relays_per_person" => {
settings.num_relays_per_person = settings.num_relays_per_person =
row.1.parse::<u8>().unwrap_or(DEFAULT_NUM_RELAYS_PER_PERSON) row.1.parse::<u8>().unwrap_or(DEFAULT_NUM_RELAYS_PER_PERSON)
@ -130,7 +126,6 @@ impl Settings {
('overlap', ?),\ ('overlap', ?),\
('view_posts_referred_to', ?),\ ('view_posts_referred_to', ?),\
('view_posts_referring_to', ?),\ ('view_posts_referring_to', ?),\
('view_threaded', ?),\
('num_relays_per_person', ?),\ ('num_relays_per_person', ?),\
('max_relays', ?),\ ('max_relays', ?),\
('max_fps', ?),\ ('max_fps', ?),\
@ -143,7 +138,6 @@ impl Settings {
self.overlap, self.overlap,
bool_to_numstr(self.view_posts_referred_to), bool_to_numstr(self.view_posts_referred_to),
bool_to_numstr(self.view_posts_referring_to), bool_to_numstr(self.view_posts_referring_to),
bool_to_numstr(self.view_threaded),
self.num_relays_per_person, self.num_relays_per_person,
self.max_relays, self.max_relays,
self.max_fps, self.max_fps,

View File

@ -1,11 +1,12 @@
use super::{GossipUi, Page}; use super::{GossipUi, Page};
use crate::comms::BusMessage; use crate::comms::BusMessage;
use crate::feed::FeedKind;
use crate::globals::{Globals, GLOBALS}; use crate::globals::{Globals, GLOBALS};
use crate::ui::widgets::{CopyButton, ReplyButton}; use crate::ui::widgets::{CopyButton, ReplyButton};
use eframe::egui; use eframe::egui;
use egui::{ use egui::{
Align, Color32, Context, Frame, Image, Label, Layout, RichText, ScrollArea, Sense, TextEdit, Align, Color32, Context, Frame, Image, Layout, RichText, ScrollArea, SelectableLabel, Sense,
Ui, Vec2, TextEdit, Ui, Vec2,
}; };
use linkify::{LinkFinder, LinkKind}; use linkify::{LinkFinder, LinkKind};
use nostr_types::{EventKind, Id, PublicKeyHex}; use nostr_types::{EventKind, Id, PublicKeyHex};
@ -18,7 +19,36 @@ struct FeedPostParams {
} }
pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) { pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) {
let feed = GLOBALS.feed.blocking_lock().get(); let mut feed_kind = GLOBALS.feed.get_feed_kind();
app.page = match feed_kind {
FeedKind::General => Page::FeedGeneral,
FeedKind::Thread(_) => Page::FeedThread,
FeedKind::Person(_) => Page::FeedPerson,
};
ui.horizontal(|ui| {
if ui
.add(SelectableLabel::new(
app.page == Page::FeedGeneral,
"Following",
))
.clicked()
{
app.page = Page::FeedGeneral;
GLOBALS.feed.set_feed_to_general();
feed_kind = FeedKind::General;
}
ui.separator();
if matches!(feed_kind, FeedKind::Thread(..)) {
ui.selectable_value(&mut app.page, Page::FeedThread, "Thread");
ui.separator();
}
if matches!(feed_kind, FeedKind::Person(..)) {
ui.selectable_value(&mut app.page, Page::FeedPerson, "Person");
ui.separator();
}
});
ui.separator();
Globals::trim_desired_events_sync(); Globals::trim_desired_events_sync();
let desired_count: isize = match GLOBALS.desired_events.try_read() { let desired_count: isize = match GLOBALS.desired_events.try_read() {
@ -57,12 +87,6 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
}); });
} }
if ui.button("close all").clicked() {
app.hides = feed.clone();
}
if ui.button("open all").clicked() {
app.hides.clear();
}
ui.label(&format!( ui.label(&format!(
"RIF={}", "RIF={}",
GLOBALS GLOBALS
@ -148,8 +172,30 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
ui.separator(); ui.separator();
let threaded = GLOBALS.settings.blocking_read().view_threaded; match feed_kind {
FeedKind::General => {
let feed = GLOBALS.feed.get_general();
render_a_feed(app, ctx, frame, ui, feed, false);
}
FeedKind::Thread(id) => {
let parent = GLOBALS.feed.get_thread_parent(id);
render_a_feed(app, ctx, frame, ui, vec![parent], true);
}
FeedKind::Person(pubkeyhex) => {
let feed = GLOBALS.feed.get_person_feed(pubkeyhex);
render_a_feed(app, ctx, frame, ui, feed, false);
}
}
}
fn render_a_feed(
app: &mut GossipUi,
ctx: &Context,
frame: &mut eframe::Frame,
ui: &mut Ui,
feed: Vec<Id>,
threaded: bool,
) {
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
let bgcolor = if ctx.style().visuals.dark_mode { let bgcolor = if ctx.style().visuals.dark_mode {
Color32::BLACK Color32::BLACK
@ -215,7 +261,7 @@ fn render_post_maybe_fake(
ui.add_space(height); ui.add_space(height);
// Yes, and we need to fake render threads to get their approx height too. // Yes, and we need to fake render threads to get their approx height too.
if threaded && !as_reply_to && !app.hides.contains(&id) { if threaded && !as_reply_to {
let replies = Globals::get_replies_sync(event.id); let replies = Globals::get_replies_sync(event.id);
for reply_id in replies { for reply_id in replies {
render_post_maybe_fake( render_post_maybe_fake(
@ -329,17 +375,6 @@ fn render_post_actual(
ui.horizontal(|ui| { ui.horizontal(|ui| {
// Indents first (if threaded) // Indents first (if threaded)
if threaded { if threaded {
#[allow(clippy::collapsible_else_if)]
if app.hides.contains(&id) {
if ui.add(Label::new("").sense(Sense::click())).clicked() {
app.hides.retain(|e| *e != id)
}
} else {
if ui.add(Label::new("").sense(Sense::click())).clicked() {
app.hides.push(id);
}
}
let space = 16.0 * (10.0 - (100.0 / (indent as f32 + 10.0))); let space = 16.0 * (10.0 - (100.0 / (indent as f32 + 10.0)));
ui.add_space(space); ui.add_space(space);
if indent > 0 { if indent > 0 {
@ -383,6 +418,10 @@ fn render_post_actual(
ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { ui.with_layout(Layout::right_to_left(Align::TOP), |ui| {
ui.menu_button(RichText::new("").size(28.0), |ui| { ui.menu_button(RichText::new("").size(28.0), |ui| {
if ui.button("View Thread").clicked() {
GLOBALS.feed.set_feed_to_thread(event.id);
app.page = Page::FeedThread;
}
if ui.button("Copy ID").clicked() { if ui.button("Copy ID").clicked() {
ui.output().copied_text = event.id.as_hex_string(); ui.output().copied_text = event.id.as_hex_string();
} }
@ -401,6 +440,11 @@ fn render_post_actual(
} }
}); });
if ui.button("").clicked() {
GLOBALS.feed.set_feed_to_thread(event.id);
app.page = Page::FeedThread;
}
ui.label( ui.label(
RichText::new(crate::date_ago::date_ago(event.created_at)) RichText::new(crate::date_ago::date_ago(event.created_at))
.italics() .italics()
@ -452,7 +496,7 @@ fn render_post_actual(
ui.separator(); ui.separator();
if threaded && !as_reply_to && !app.hides.contains(&id) { if threaded && !as_reply_to {
let replies = Globals::get_replies_sync(event.id); let replies = Globals::get_replies_sync(event.id);
for reply_id in replies { for reply_id in replies {
render_post_maybe_fake( render_post_maybe_fake(

71
src/ui/help/about.rs Normal file
View File

@ -0,0 +1,71 @@
use super::GossipUi;
use eframe::egui;
use egui::{Align, Context, Layout, RichText, TextStyle, Ui};
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.with_layout(Layout::top_down(Align::Center), |ui| {
ui.add_space(30.0);
ui.image(&app.icon, app.icon.size_vec2());
ui.add_space(15.0);
ui.label(
RichText::new(&app.about.name).strong()
);
ui.add_space(15.0);
ui.label(
RichText::new(&app.about.version)
.text_style(TextStyle::Body)
);
ui.add_space(15.0);
ui.label(
RichText::new(&app.about.description)
.text_style(TextStyle::Body)
);
ui.add_space(35.0);
ui.label(
RichText::new(format!("nostr is a protocol and specification for storing and retrieving social media events onto servers called relays. Many users store their events onto multiple relays for reliability, censorship resistance, and to spread their reach. If you didn't store an event on a particular relay, don't expect anyone to find it there because relays normally don't share events with each other.
Users are defined by their keypair, and are known by the public key of that pair. All events they generate are signed by their private key, and verifiable by their public key.
We are storing data on your system in this file: {}. This data is only used locally by this client - the nostr protocol does not use clients as a store of data for other people. We are storing your settings, your private and public key, information about relays, and a cache of events. We cache events in your feed so that we don't have to ask relays for them again, which means less network traffic and faster startup times.
", app.about.storage_path))
.text_style(TextStyle::Body)
);
ui.add_space(22.0);
ui.hyperlink_to("Learn More about Nostr", "https://github.com/nostr-protocol/nostr");
ui.add_space(30.0);
ui.hyperlink_to("Source Code", &app.about.homepage);
ui.label(
RichText::new("by")
.text_style(TextStyle::Small)
);
ui.label(
RichText::new(&app.about.authors)
.text_style(TextStyle::Small)
);
ui.add_space(15.0);
ui.label(
RichText::new("This program comes with absolutely no warranty.")
.text_style(TextStyle::Small)
);
ui.label(
RichText::new("See the MIT License for details.")
.text_style(TextStyle::Small)
);
});
}

View File

@ -1,16 +1,20 @@
use super::{GossipUi, Page}; use super::{GossipUi, Page};
use eframe::egui; use eframe::egui;
use egui::{Align, Context, Layout, RichText, ScrollArea, TextStyle, TopBottomPanel, Ui}; use egui::{Context, ScrollArea, Ui};
mod about;
mod stats;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
TopBottomPanel::top("help_menu").show(ctx, |ui| { ui.horizontal(|ui| {
ui.horizontal(|ui| { ui.selectable_value(&mut app.page, Page::HelpHelp, "Help");
ui.selectable_value(&mut app.page, Page::HelpHelp, "Help"); ui.separator();
ui.separator(); ui.selectable_value(&mut app.page, Page::HelpStats, "Stats");
ui.selectable_value(&mut app.page, Page::HelpAbout, "About"); ui.separator();
ui.separator(); ui.selectable_value(&mut app.page, Page::HelpAbout, "About");
}); ui.separator();
}); });
ui.separator();
if app.page == Page::HelpHelp { if app.page == Page::HelpHelp {
ui.add_space(24.0); ui.add_space(24.0);
@ -127,71 +131,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.add_space(10.0); ui.add_space(10.0);
}); });
} else if app.page == Page::HelpStats {
stats::update(app, ctx, _frame, ui);
} else if app.page == Page::HelpAbout { } else if app.page == Page::HelpAbout {
ui.with_layout(Layout::top_down(Align::Center), |ui| { about::update(app, ctx, _frame, ui);
ui.add_space(30.0);
ui.image(&app.icon, app.icon.size_vec2());
ui.add_space(15.0);
ui.label(
RichText::new(&app.about.name).strong()
);
ui.add_space(15.0);
ui.label(
RichText::new(&app.about.version)
.text_style(TextStyle::Body)
);
ui.add_space(15.0);
ui.label(
RichText::new(&app.about.description)
.text_style(TextStyle::Body)
);
ui.add_space(35.0);
ui.label(
RichText::new(format!("nostr is a protocol and specification for storing and retrieving social media events onto servers called relays. Many users store their events onto multiple relays for reliability, censorship resistance, and to spread their reach. If you didn't store an event on a particular relay, don't expect anyone to find it there because relays normally don't share events with each other.
Users are defined by their keypair, and are known by the public key of that pair. All events they generate are signed by their private key, and verifiable by their public key.
We are storing data on your system in this file: {}. This data is only used locally by this client - the nostr protocol does not use clients as a store of data for other people. We are storing your settings, your private and public key, information about relays, and a cache of events. We cache events in your feed so that we don't have to ask relays for them again, which means less network traffic and faster startup times.
", app.about.storage_path))
.text_style(TextStyle::Body)
);
ui.add_space(22.0);
ui.hyperlink_to("Learn More about Nostr", "https://github.com/nostr-protocol/nostr");
ui.add_space(30.0);
ui.hyperlink_to("Source Code", &app.about.homepage);
ui.label(
RichText::new("by")
.text_style(TextStyle::Small)
);
ui.label(
RichText::new(&app.about.authors)
.text_style(TextStyle::Small)
);
ui.add_space(15.0);
ui.label(
RichText::new("This program comes with absolutely no warranty.")
.text_style(TextStyle::Small)
);
ui.label(
RichText::new("See the MIT License for details.")
.text_style(TextStyle::Small)
);
});
} }
} }

View File

@ -3,7 +3,6 @@ mod help;
mod people; mod people;
mod relays; mod relays;
mod settings; mod settings;
mod stats;
mod style; mod style;
mod widgets; mod widgets;
mod you; mod you;
@ -16,8 +15,8 @@ use crate::settings::Settings;
use crate::ui::widgets::CopyButton; use crate::ui::widgets::CopyButton;
use eframe::{egui, IconData, Theme}; use eframe::{egui, IconData, Theme};
use egui::{ use egui::{
ColorImage, Context, ImageData, Label, RichText, Sense, TextStyle, TextureHandle, ColorImage, Context, ImageData, Label, RichText, SelectableLabel, Sense, TextStyle,
TextureOptions, Ui, TextureHandle, TextureOptions, Ui,
}; };
use nostr_types::{Id, PublicKey, PublicKeyHex}; use nostr_types::{Id, PublicKey, PublicKeyHex};
use std::collections::HashMap; use std::collections::HashMap;
@ -55,15 +54,17 @@ pub fn run() -> Result<(), Error> {
#[derive(PartialEq)] #[derive(PartialEq)]
enum Page { enum Page {
Feed, FeedGeneral,
PeopleFollow, FeedThread,
FeedPerson,
PeopleList, PeopleList,
PeopleFollow,
Person, Person,
You, You,
Relays, Relays,
Settings, Settings,
Stats,
HelpHelp, HelpHelp,
HelpStats,
HelpAbout, HelpAbout,
} }
@ -84,7 +85,6 @@ struct GossipUi {
import_bech32: String, import_bech32: String,
import_hex: String, import_hex: String,
replying_to: Option<Id>, replying_to: Option<Id>,
hides: Vec<Id>,
person_view_pubkey: Option<PublicKeyHex>, person_view_pubkey: Option<PublicKeyHex>,
avatars: HashMap<PublicKeyHex, TextureHandle>, avatars: HashMap<PublicKeyHex, TextureHandle>,
new_relay_url: String, new_relay_url: String,
@ -140,7 +140,7 @@ impl GossipUi {
GossipUi { GossipUi {
next_frame: Instant::now(), next_frame: Instant::now(),
page: Page::Feed, page: Page::FeedGeneral,
status: status:
"Welcome to Gossip. Status messages will appear here. Click them to dismiss them." "Welcome to Gossip. Status messages will appear here. Click them to dismiss them."
.to_owned(), .to_owned(),
@ -157,7 +157,6 @@ impl GossipUi {
import_bech32: "".to_owned(), import_bech32: "".to_owned(),
import_hex: "".to_owned(), import_hex: "".to_owned(),
replying_to: None, replying_to: None,
hides: Vec::new(),
person_view_pubkey: None, person_view_pubkey: None,
avatars: HashMap::new(), avatars: HashMap::new(),
new_relay_url: "".to_owned(), new_relay_url: "".to_owned(),
@ -187,19 +186,65 @@ impl eframe::App for GossipUi {
egui::TopBottomPanel::top("menu").show(ctx, |ui| { egui::TopBottomPanel::top("menu").show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.selectable_value(&mut self.page, Page::Feed, "Feed"); if ui
.add(SelectableLabel::new(
self.page == Page::FeedGeneral
|| self.page == Page::FeedThread
|| self.page == Page::FeedPerson,
"Feed",
))
.clicked()
{
self.page = Page::FeedGeneral;
}
ui.separator(); ui.separator();
ui.selectable_value(&mut self.page, Page::PeopleList, "People"); if ui
.add(SelectableLabel::new(
self.page == Page::PeopleList
|| self.page == Page::PeopleFollow
|| self.page == Page::Person,
"People",
))
.clicked()
{
self.page = Page::PeopleList;
}
ui.separator(); ui.separator();
ui.selectable_value(&mut self.page, Page::You, "You"); if ui
.add(SelectableLabel::new(self.page == Page::You, "You"))
.clicked()
{
self.page = Page::You;
}
ui.separator(); ui.separator();
ui.selectable_value(&mut self.page, Page::Relays, "Relays"); if ui
.add(SelectableLabel::new(self.page == Page::Relays, "Relays"))
.clicked()
{
self.page = Page::Relays;
}
ui.separator(); ui.separator();
ui.selectable_value(&mut self.page, Page::Settings, "Settings"); if ui
.add(SelectableLabel::new(
self.page == Page::Settings,
"Settings",
))
.clicked()
{
self.page = Page::Settings;
}
ui.separator(); ui.separator();
ui.selectable_value(&mut self.page, Page::Stats, "Stats"); if ui
ui.separator(); .add(SelectableLabel::new(
ui.selectable_value(&mut self.page, Page::HelpHelp, "Help"); self.page == Page::HelpHelp
|| self.page == Page::HelpStats
|| self.page == Page::HelpAbout,
"Help",
))
.clicked()
{
self.page = Page::HelpHelp;
}
ui.separator(); ui.separator();
}); });
}); });
@ -216,16 +261,18 @@ impl eframe::App for GossipUi {
}); });
egui::CentralPanel::default().show(ctx, |ui| match self.page { egui::CentralPanel::default().show(ctx, |ui| match self.page {
Page::Feed => feed::update(self, ctx, frame, ui), Page::FeedGeneral | Page::FeedThread | Page::FeedPerson => {
Page::PeopleList => people::update(self, ctx, frame, ui), feed::update(self, ctx, frame, ui)
Page::PeopleFollow => people::update(self, ctx, frame, ui), }
Page::Person => people::update(self, ctx, frame, ui), Page::PeopleList | Page::PeopleFollow | Page::Person => {
people::update(self, ctx, frame, ui)
}
Page::You => you::update(self, ctx, frame, ui), Page::You => you::update(self, ctx, frame, ui),
Page::Relays => relays::update(self, ctx, frame, ui), Page::Relays => relays::update(self, ctx, frame, ui),
Page::Settings => settings::update(self, ctx, frame, ui, darkmode), Page::Settings => settings::update(self, ctx, frame, ui, darkmode),
Page::Stats => stats::update(self, ctx, frame, ui), Page::HelpHelp | Page::HelpStats | Page::HelpAbout => {
Page::HelpHelp => help::update(self, ctx, frame, ui), help::update(self, ctx, frame, ui)
Page::HelpAbout => help::update(self, ctx, frame, ui), }
}); });
} }
} }

View File

@ -1,237 +0,0 @@
use super::{GossipUi, Page};
use crate::comms::BusMessage;
use crate::db::DbPerson;
use crate::globals::GLOBALS;
use eframe::egui;
use egui::{Context, Image, RichText, ScrollArea, Sense, TextEdit, TopBottomPanel, Ui, Vec2};
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let maybe_person = if let Some(pubkeyhex) = &app.person_view_pubkey {
GLOBALS.people.blocking_write().get(pubkeyhex)
} else {
None
};
TopBottomPanel::top("people_menu").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut app.page, Page::PeopleList, "Followed");
ui.separator();
ui.selectable_value(&mut app.page, Page::PeopleFollow, "Follow Someone New");
ui.separator();
if let Some(person) = &maybe_person {
ui.selectable_value(&mut app.page, Page::Person, get_name(person));
ui.separator();
}
});
});
if app.page == Page::PeopleFollow {
ui.add_space(30.0);
ui.heading("NOTICE: Gossip doesn't update the filters when you follow someone yet, so you have to restart the client to fetch their events. Will fix soon.");
ui.heading("NOTICE: Gossip is not synchronizing with data on the nostr relays. This is a separate list and it won't overwrite anything.");
ui.label("NOTICE: use CTRL-V to paste (middle/right click wont work)");
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("NIP-35: Follow a DNS ID");
ui.horizontal(|ui| {
ui.label("Enter user@domain");
ui.add(TextEdit::singleline(&mut app.nip35follow).hint_text("user@domain"));
});
if ui.button("follow").clicked() {
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "follow_nip35".to_string(),
json_payload: serde_json::to_string(&app.nip35follow).unwrap(),
});
app.nip35follow = "".to_owned();
}
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("Follow a bech32 public key");
ui.horizontal(|ui| {
ui.label("Enter bech32 public key");
ui.add(TextEdit::singleline(&mut app.follow_bech32_pubkey).hint_text("npub1..."));
});
ui.horizontal(|ui| {
ui.label("Enter a relay URL where we can find them");
ui.add(TextEdit::singleline(&mut app.follow_pubkey_at_relay).hint_text("wss://..."));
});
if ui.button("follow").clicked() {
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "follow_bech32".to_string(),
json_payload: serde_json::to_string(&(
&app.follow_bech32_pubkey,
&app.follow_pubkey_at_relay,
))
.unwrap(),
});
app.follow_bech32_pubkey = "".to_owned();
app.follow_pubkey_at_relay = "".to_owned();
}
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("Follow a hex public key");
ui.horizontal(|ui| {
ui.label("Enter hex-encoded public key");
ui.add(
TextEdit::singleline(&mut app.follow_hex_pubkey).hint_text("0123456789abcdef..."),
);
});
ui.horizontal(|ui| {
ui.label("Enter a relay URL where we can find them");
ui.add(TextEdit::singleline(&mut app.follow_pubkey_at_relay).hint_text("wss://..."));
});
if ui.button("follow").clicked() {
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "follow_hexkey".to_string(),
json_payload: serde_json::to_string(&(
&app.follow_hex_pubkey,
&app.follow_pubkey_at_relay,
))
.unwrap(),
});
app.follow_hex_pubkey = "".to_owned();
app.follow_pubkey_at_relay = "".to_owned();
}
} else if app.page == Page::PeopleList {
ui.add_space(24.0);
ui.heading("NOTICE: Gossip is not synchronizing with data on the nostr relays. This is a separate list and it won't overwrite anything.");
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("People Followed");
ui.add_space(18.0);
let people = GLOBALS.people.blocking_write().get_all();
ScrollArea::vertical().show(ui, |ui| {
for person in people.iter() {
if person.followed != 1 {
continue;
}
ui.horizontal(|ui| {
// Avatar first
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) {
avatar
} else {
app.placeholder_avatar.clone()
};
if ui
.add(
Image::new(
&avatar,
Vec2 {
x: crate::AVATAR_SIZE_F32,
y: crate::AVATAR_SIZE_F32,
},
)
.sense(Sense::click()),
)
.clicked()
{
set_person_view(app, person);
};
ui.vertical(|ui| {
ui.label(RichText::new(GossipUi::hex_pubkey_short(&person.pubkey)).weak());
GossipUi::render_person_name_line(ui, Some(person));
});
});
ui.add_space(4.0);
ui.separator();
}
});
} else if app.page == Page::Person {
if maybe_person.is_none() || app.person_view_pubkey.is_none() {
ui.label("ERROR");
} else {
let person = maybe_person.as_ref().unwrap();
let pubkeyhex = app.person_view_pubkey.as_ref().unwrap().clone();
ui.add_space(24.0);
ui.heading(get_name(person));
ui.horizontal(|ui| {
// Avatar first
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &pubkeyhex) {
avatar
} else {
app.placeholder_avatar.clone()
};
ui.image(&avatar, Vec2 { x: 36.0, y: 36.0 });
ui.vertical(|ui| {
ui.label(RichText::new(GossipUi::hex_pubkey_short(&pubkeyhex)).weak());
GossipUi::render_person_name_line(ui, Some(person));
});
});
ui.add_space(12.0);
if let Some(about) = person.about.as_deref() {
ui.label(about);
}
ui.add_space(12.0);
#[allow(clippy::collapsible_else_if)]
if person.followed == 0 {
if ui.button("FOLLOW").clicked() {
GLOBALS.people.blocking_write().follow(&pubkeyhex, true);
}
} else {
if ui.button("UNFOLLOW").clicked() {
GLOBALS.people.blocking_write().follow(&pubkeyhex, false);
}
}
if ui.button("UPDATE METADATA").clicked() {
let _ = GLOBALS.to_overlord.send(BusMessage {
target: "overlord".to_string(),
kind: "update_metadata".to_string(),
json_payload: serde_json::to_string(&pubkeyhex).unwrap(),
});
}
}
}
}
fn get_name(person: &DbPerson) -> String {
if let Some(name) = &person.name {
name.to_owned()
} else {
GossipUi::hex_pubkey_short(&person.pubkey)
}
}
fn set_person_view(app: &mut GossipUi, person: &DbPerson) {
app.person_view_pubkey = Some(person.pubkey.clone());
app.page = Page::Person;
}

93
src/ui/people/follow.rs Normal file
View File

@ -0,0 +1,93 @@
use super::GossipUi;
use crate::comms::BusMessage;
use crate::globals::GLOBALS;
use eframe::egui;
use egui::{Context, TextEdit, Ui};
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.add_space(30.0);
ui.heading("NOTICE: Gossip doesn't update the filters when you follow someone yet, so you have to restart the client to fetch their events. Will fix soon.");
ui.heading("NOTICE: Gossip is not synchronizing with data on the nostr relays. This is a separate list and it won't overwrite anything.");
ui.label("NOTICE: use CTRL-V to paste (middle/right click wont work)");
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("NIP-35: Follow a DNS ID");
ui.horizontal(|ui| {
ui.label("Enter user@domain");
ui.add(TextEdit::singleline(&mut app.nip35follow).hint_text("user@domain"));
});
if ui.button("follow").clicked() {
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "follow_nip35".to_string(),
json_payload: serde_json::to_string(&app.nip35follow).unwrap(),
});
app.nip35follow = "".to_owned();
}
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("Follow a bech32 public key");
ui.horizontal(|ui| {
ui.label("Enter bech32 public key");
ui.add(TextEdit::singleline(&mut app.follow_bech32_pubkey).hint_text("npub1..."));
});
ui.horizontal(|ui| {
ui.label("Enter a relay URL where we can find them");
ui.add(TextEdit::singleline(&mut app.follow_pubkey_at_relay).hint_text("wss://..."));
});
if ui.button("follow").clicked() {
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "follow_bech32".to_string(),
json_payload: serde_json::to_string(&(
&app.follow_bech32_pubkey,
&app.follow_pubkey_at_relay,
))
.unwrap(),
});
app.follow_bech32_pubkey = "".to_owned();
app.follow_pubkey_at_relay = "".to_owned();
}
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("Follow a hex public key");
ui.horizontal(|ui| {
ui.label("Enter hex-encoded public key");
ui.add(TextEdit::singleline(&mut app.follow_hex_pubkey).hint_text("0123456789abcdef..."));
});
ui.horizontal(|ui| {
ui.label("Enter a relay URL where we can find them");
ui.add(TextEdit::singleline(&mut app.follow_pubkey_at_relay).hint_text("wss://..."));
});
if ui.button("follow").clicked() {
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "follow_hexkey".to_string(),
json_payload: serde_json::to_string(&(
&app.follow_hex_pubkey,
&app.follow_pubkey_at_relay,
))
.unwrap(),
});
app.follow_hex_pubkey = "".to_owned();
app.follow_pubkey_at_relay = "".to_owned();
}
}

101
src/ui/people/mod.rs Normal file
View File

@ -0,0 +1,101 @@
use super::{GossipUi, Page};
use crate::db::DbPerson;
use crate::globals::GLOBALS;
use eframe::egui;
use egui::{Context, Image, RichText, ScrollArea, Sense, Ui, Vec2};
mod follow;
mod person;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let maybe_person = if let Some(pubkeyhex) = &app.person_view_pubkey {
GLOBALS.people.blocking_write().get(pubkeyhex)
} else {
None
};
ui.horizontal(|ui| {
ui.selectable_value(&mut app.page, Page::PeopleList, "Followed");
ui.separator();
ui.selectable_value(&mut app.page, Page::PeopleFollow, "Follow Someone New");
ui.separator();
if let Some(person) = &maybe_person {
ui.selectable_value(&mut app.page, Page::Person, get_name(person));
ui.separator();
}
});
ui.separator();
if app.page == Page::PeopleList {
ui.add_space(24.0);
ui.heading("NOTICE: Gossip is not synchronizing with data on the nostr relays. This is a separate list and it won't overwrite anything.");
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("People Followed");
ui.add_space(18.0);
let people = GLOBALS.people.blocking_write().get_all();
ScrollArea::vertical().show(ui, |ui| {
for person in people.iter() {
if person.followed != 1 {
continue;
}
ui.horizontal(|ui| {
// Avatar first
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) {
avatar
} else {
app.placeholder_avatar.clone()
};
if ui
.add(
Image::new(
&avatar,
Vec2 {
x: crate::AVATAR_SIZE_F32,
y: crate::AVATAR_SIZE_F32,
},
)
.sense(Sense::click()),
)
.clicked()
{
set_person_view(app, person);
};
ui.vertical(|ui| {
ui.label(RichText::new(GossipUi::hex_pubkey_short(&person.pubkey)).weak());
GossipUi::render_person_name_line(ui, Some(person));
});
});
ui.add_space(4.0);
ui.separator();
}
});
} else if app.page == Page::PeopleFollow {
follow::update(app, ctx, _frame, ui);
} else if app.page == Page::Person {
person::update(app, ctx, _frame, ui);
}
}
fn get_name(person: &DbPerson) -> String {
if let Some(name) = &person.name {
name.to_owned()
} else {
GossipUi::hex_pubkey_short(&person.pubkey)
}
}
fn set_person_view(app: &mut GossipUi, person: &DbPerson) {
app.person_view_pubkey = Some(person.pubkey.clone());
app.page = Page::Person;
}

80
src/ui/people/person.rs Normal file
View File

@ -0,0 +1,80 @@
use super::{GossipUi, Page};
use crate::comms::BusMessage;
use crate::db::DbPerson;
use crate::globals::GLOBALS;
use eframe::egui;
use egui::{Context, RichText, Ui, Vec2};
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let maybe_person = if let Some(pubkeyhex) = &app.person_view_pubkey {
GLOBALS.people.blocking_write().get(pubkeyhex)
} else {
None
};
if maybe_person.is_none() || app.person_view_pubkey.is_none() {
ui.label("ERROR");
} else {
let person = maybe_person.as_ref().unwrap();
let pubkeyhex = app.person_view_pubkey.as_ref().unwrap().clone();
ui.add_space(24.0);
ui.heading(get_name(person));
ui.horizontal(|ui| {
// Avatar first
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &pubkeyhex) {
avatar
} else {
app.placeholder_avatar.clone()
};
ui.image(&avatar, Vec2 { x: 36.0, y: 36.0 });
ui.vertical(|ui| {
ui.label(RichText::new(GossipUi::hex_pubkey_short(&pubkeyhex)).weak());
GossipUi::render_person_name_line(ui, Some(person));
});
});
ui.add_space(12.0);
if let Some(about) = person.about.as_deref() {
ui.label(about);
}
ui.add_space(12.0);
#[allow(clippy::collapsible_else_if)]
if person.followed == 0 {
if ui.button("FOLLOW").clicked() {
GLOBALS.people.blocking_write().follow(&pubkeyhex, true);
}
} else {
if ui.button("UNFOLLOW").clicked() {
GLOBALS.people.blocking_write().follow(&pubkeyhex, false);
}
}
if ui.button("UPDATE METADATA").clicked() {
let _ = GLOBALS.to_overlord.send(BusMessage {
target: "overlord".to_string(),
kind: "update_metadata".to_string(),
json_payload: serde_json::to_string(&pubkeyhex).unwrap(),
});
}
if ui.button("VIEW THEIR FEED").clicked() {
GLOBALS.feed.set_feed_to_person(pubkeyhex.clone());
app.page = Page::FeedPerson;
}
}
}
fn get_name(person: &DbPerson) -> String {
if let Some(name) = &person.name {
name.to_owned()
} else {
GossipUi::hex_pubkey_short(&person.pubkey)
}
}

View File

@ -57,10 +57,6 @@ pub(super) fn update(
ui.heading("Feed"); ui.heading("Feed");
ui.checkbox(&mut app.settings.view_threaded, "Threaded feed")
.on_hover_text("If selected, replies are under what they reply to and the newest replied-to thread comes first. Otherwise all posts are independent and in time order.");
ui.add_space(24.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Recompute feed every (milliseconds): ") ui.label("Recompute feed every (milliseconds): ")
.on_hover_text( .on_hover_text(