Multiple feeds working

This commit is contained in:
Mike Dilger 2023-01-03 13:46:57 +13:00
parent 238c429017
commit 611b5ab12c
6 changed files with 287 additions and 222 deletions

View File

@ -1,10 +1,20 @@
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 std::time::{Duration, Instant};
use parking_lot::RwLock; use parking_lot::RwLock;
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub enum FeedKind {
General,
Thread(Id),
Person(PublicKeyHex),
}
pub struct Feed { pub struct Feed {
feed: RwLock<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: RwLock<u32>, interval_ms: RwLock<u32>,
@ -18,7 +28,8 @@ pub struct Feed {
impl Feed { impl Feed {
pub fn new() -> Feed { pub fn new() -> Feed {
Feed { Feed {
feed: RwLock::new(Vec::new()), current_feed_kind: RwLock::new(FeedKind::General),
general_feed: RwLock::new(Vec::new()),
interval_ms: RwLock::new(1000), // Every second, until we load from settings interval_ms: RwLock::new(1000), // Every second, until we load from settings
last_computed: RwLock::new(Instant::now()), last_computed: RwLock::new(Instant::now()),
my_event_ids: RwLock::new(Vec::new()), my_event_ids: RwLock::new(Vec::new()),
@ -26,14 +37,77 @@ impl Feed {
} }
} }
pub fn get(&self) -> Vec<Id> { pub fn set_feed_to_general(&self) {
*self.current_feed_kind.write() = FeedKind::General;
}
pub fn set_feed_to_thread(&self, id: Id) {
// get parent?
*self.current_feed_kind.write() = FeedKind::Thread(id);
}
pub fn set_feed_to_person(&self, pubkey: PublicKeyHex) {
// FIXME - TRIGGER OVERLORD TO FETCH THEIR EVENTS FURTHER BACK
*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.read() + Duration::from_millis(*self.interval_ms.read() 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.write() = now; *self.last_computed.write() = now;
} }
self.feed.read().clone() self.general_feed.read().clone()
}
pub fn get_thread_parent(&self, id: Id) -> Id {
// FIXME - TRIGGER OVERLORD TO FETCH THIS FEED
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)]
@ -88,26 +162,11 @@ impl Feed {
let mut events: Vec<Event> = events let mut events: Vec<Event> = events
.iter() .iter()
.filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id)) .filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id))
//.filter(|e| { for Threaded
//e.replies_to().is_none()
//})
.cloned() .cloned()
.collect(); .collect();
/* for threaded
if settings.view_threaded {
events.sort_unstable_by(|a, b| {
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)
});
}
*/
events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at)); events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at));
*self.feed.write() = events.iter().map(|e| e.id).collect(); *self.general_feed.write() = events.iter().map(|e| e.id).collect();
} }
} }

View File

@ -1,18 +1,16 @@
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};
mod person;
mod thread;
struct FeedPostParams { struct FeedPostParams {
id: Id, id: Id,
indent: usize, indent: usize,
@ -21,183 +19,208 @@ 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 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| { ui.horizontal(|ui| {
ui.selectable_value(&mut app.page, Page::FeedGeneral, "Following"); 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(); ui.separator();
if app.feed_thread_id.is_some() { if matches!(feed_kind, FeedKind::Thread(..)) {
ui.selectable_value(&mut app.page, Page::FeedThread, "Thread"); ui.selectable_value(&mut app.page, Page::FeedThread, "Thread");
ui.separator(); ui.separator();
} }
if app.feed_person_pubkey.is_some() { if matches!(feed_kind, FeedKind::Person(..)) {
ui.selectable_value(&mut app.page, Page::FeedPerson, "Person"); ui.selectable_value(&mut app.page, Page::FeedPerson, "Person");
ui.separator(); ui.separator();
} }
}); });
ui.separator(); ui.separator();
if app.page == Page::FeedGeneral { Globals::trim_desired_events_sync();
let feed = GLOBALS.feed.get(); let desired_count: isize = match GLOBALS.desired_events.try_read() {
Ok(v) => v.len() as isize,
Err(_) => -1,
};
let incoming_count: isize = match GLOBALS.incoming_events.try_read() {
Ok(v) => v.len() as isize,
Err(_) => -1,
};
Globals::trim_desired_events_sync(); ui.with_layout(Layout::right_to_left(Align::TOP), |ui| {
let desired_count: isize = match GLOBALS.desired_events.try_read() { if ui
Ok(v) => v.len() as isize, .button(&format!("QM {}", desired_count))
Err(_) => -1, .on_hover_text("Query Relays for Missing Events")
}; .clicked()
let incoming_count: isize = match GLOBALS.incoming_events.try_read() { {
Ok(v) => v.len() as isize, let tx = GLOBALS.to_overlord.clone();
Err(_) => -1, let _ = tx.send(BusMessage {
}; target: "overlord".to_string(),
kind: "get_missing_events".to_string(),
ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { json_payload: serde_json::to_string("").unwrap(),
if ui
.button(&format!("QM {}", desired_count))
.on_hover_text("Query Relays for Missing Events")
.clicked()
{
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "get_missing_events".to_string(),
json_payload: serde_json::to_string("").unwrap(),
});
}
if ui
.button(&format!("PQ {}", incoming_count))
.on_hover_text("Process Queue of Incoming Events")
.clicked()
{
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "process_incoming_events".to_string(),
json_payload: serde_json::to_string("").unwrap(),
});
}
if ui.button("close all").clicked() {
app.hides = feed.clone();
}
if ui.button("open all").clicked() {
app.hides.clear();
}
ui.label(&format!(
"RIF={}",
GLOBALS
.fetcher
.requests_in_flight
.load(std::sync::atomic::Ordering::Relaxed)
));
});
ui.vertical(|ui| {
if !GLOBALS.signer.blocking_read().is_ready() {
ui.horizontal(|ui| {
ui.label("You need to ");
if ui.link("setup your identity").clicked() {
app.page = Page::You;
}
ui.label(" to post.");
});
} else if !GLOBALS.relays.blocking_read().iter().any(|(_, r)| r.post) {
ui.horizontal(|ui| {
ui.label("You need to ");
if ui.link("choose relays").clicked() {
app.page = Page::Relays;
}
ui.label(" to post.");
});
} else {
if let Some(id) = app.replying_to {
render_post_actual(
app,
ctx,
frame,
ui,
FeedPostParams {
id,
indent: 0,
as_reply_to: true,
threaded: false,
},
);
}
ui.with_layout(Layout::right_to_left(Align::TOP), |ui| {
if ui.button("Send").clicked() && !app.draft.is_empty() {
let tx = GLOBALS.to_overlord.clone();
match app.replying_to {
Some(_id) => {
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "post_reply".to_string(),
json_payload: serde_json::to_string(&(
&app.draft,
&app.replying_to,
))
.unwrap(),
});
}
None => {
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "post_textnote".to_string(),
json_payload: serde_json::to_string(&app.draft).unwrap(),
});
}
}
app.draft = "".to_owned();
app.replying_to = None;
}
if ui.button("Cancel").clicked() {
app.draft = "".to_owned();
app.replying_to = None;
}
ui.add(
TextEdit::multiline(&mut app.draft)
.hint_text("Type your message here")
.desired_width(f32::INFINITY)
.lock_focus(true),
);
});
}
});
ui.separator();
let threaded = false;
ScrollArea::vertical().show(ui, |ui| {
let bgcolor = if ctx.style().visuals.dark_mode {
Color32::BLACK
} else {
Color32::WHITE
};
Frame::none().fill(bgcolor).show(ui, |ui| {
for id in feed.iter() {
render_post_maybe_fake(
app,
ctx,
frame,
ui,
FeedPostParams {
id: *id,
indent: 0,
as_reply_to: false,
threaded,
},
);
}
}); });
}); }
} else if app.page == Page::FeedThread {
thread::update(app, ctx, frame, ui); if ui
} else if app.page == Page::FeedPerson { .button(&format!("PQ {}", incoming_count))
person::update(app, ctx, frame, ui); .on_hover_text("Process Queue of Incoming Events")
.clicked()
{
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "process_incoming_events".to_string(),
json_payload: serde_json::to_string("").unwrap(),
});
}
ui.label(&format!(
"RIF={}",
GLOBALS
.fetcher
.requests_in_flight
.load(std::sync::atomic::Ordering::Relaxed)
));
});
ui.vertical(|ui| {
if !GLOBALS.signer.blocking_read().is_ready() {
ui.horizontal(|ui| {
ui.label("You need to ");
if ui.link("setup your identity").clicked() {
app.page = Page::You;
}
ui.label(" to post.");
});
} else if !GLOBALS.relays.blocking_read().iter().any(|(_, r)| r.post) {
ui.horizontal(|ui| {
ui.label("You need to ");
if ui.link("choose relays").clicked() {
app.page = Page::Relays;
}
ui.label(" to post.");
});
} else {
if let Some(id) = app.replying_to {
render_post_actual(
app,
ctx,
frame,
ui,
FeedPostParams {
id,
indent: 0,
as_reply_to: true,
threaded: false,
},
);
}
ui.with_layout(Layout::right_to_left(Align::TOP), |ui| {
if ui.button("Send").clicked() && !app.draft.is_empty() {
let tx = GLOBALS.to_overlord.clone();
match app.replying_to {
Some(_id) => {
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "post_reply".to_string(),
json_payload: serde_json::to_string(&(
&app.draft,
&app.replying_to,
))
.unwrap(),
});
}
None => {
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "post_textnote".to_string(),
json_payload: serde_json::to_string(&app.draft).unwrap(),
});
}
}
app.draft = "".to_owned();
app.replying_to = None;
}
if ui.button("Cancel").clicked() {
app.draft = "".to_owned();
app.replying_to = None;
}
ui.add(
TextEdit::multiline(&mut app.draft)
.hint_text("Type your message here")
.desired_width(f32::INFINITY)
.lock_focus(true),
);
});
}
});
ui.separator();
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| {
let bgcolor = if ctx.style().visuals.dark_mode {
Color32::BLACK
} else {
Color32::WHITE
};
Frame::none().fill(bgcolor).show(ui, |ui| {
for id in feed.iter() {
render_post_maybe_fake(
app,
ctx,
frame,
ui,
FeedPostParams {
id: *id,
indent: 0,
as_reply_to: false,
threaded,
},
);
}
});
});
}
fn render_post_maybe_fake( fn render_post_maybe_fake(
app: &mut GossipUi, app: &mut GossipUi,
ctx: &Context, ctx: &Context,
@ -238,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(
@ -352,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 {
@ -406,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();
} }
@ -424,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()
@ -475,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(

View File

@ -1,7 +0,0 @@
use super::GossipUi;
use eframe::egui;
use egui::{Context, Ui};
pub(super) fn update(_app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.label("Person TBD");
}

View File

@ -1,7 +0,0 @@
use super::GossipUi;
use eframe::egui;
use egui::{Context, Ui};
pub(super) fn update(_app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.label("Thread TBD");
}

View File

@ -85,12 +85,9 @@ 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,
feed_thread_id: Option<Id>,
feed_person_pubkey: Option<PublicKeyHex>,
} }
impl Drop for GossipUi { impl Drop for GossipUi {
@ -160,12 +157,9 @@ 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(),
feed_thread_id: None,
feed_person_pubkey: None,
} }
} }
} }

View File

@ -1,4 +1,4 @@
use super::GossipUi; use super::{GossipUi, Page};
use crate::comms::BusMessage; use crate::comms::BusMessage;
use crate::db::DbPerson; use crate::db::DbPerson;
use crate::globals::GLOBALS; use crate::globals::GLOBALS;
@ -63,6 +63,11 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
json_payload: serde_json::to_string(&pubkeyhex).unwrap(), 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;
}
} }
} }