Switch to unified timeline cache via TimelineKinds

This is a fairly large rewrite which unifies our threads, timelines and
profiles. Now all timelines have a MultiSubscriber, and can be added
and removed to columns just like Threads and Profiles.

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-01-22 15:59:21 -08:00
parent d46e526a45
commit 0cc1d8a600
39 changed files with 1395 additions and 2055 deletions

View File

@@ -143,32 +143,39 @@ impl AddColumnOption {
ndb: &Ndb,
cur_account: Option<&UserAccount>,
) -> Option<AddColumnResponse> {
let txn = Transaction::new(ndb).unwrap();
match self {
AddColumnOption::Algo(algo_option) => Some(AddColumnResponse::Algo(algo_option)),
AddColumnOption::Universe => TimelineKind::Universe
.into_timeline(ndb, None)
.map(AddColumnResponse::Timeline),
AddColumnOption::Notification(pubkey) => TimelineKind::Notifications(pubkey)
.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes()))
.into_timeline(&txn, ndb)
.map(AddColumnResponse::Timeline),
AddColumnOption::Notification(pubkey) => {
TimelineKind::Notifications(*pubkey.to_pubkey(&cur_account.map(|kp| kp.pubkey)?))
.into_timeline(&txn, ndb)
.map(AddColumnResponse::Timeline)
}
AddColumnOption::UndecidedNotification => {
Some(AddColumnResponse::UndecidedNotification)
}
AddColumnOption::Contacts(pubkey) => {
let tlk = TimelineKind::contact_list(pubkey);
tlk.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes()))
AddColumnOption::Contacts(pk_src) => {
let tlk = TimelineKind::contact_list(
*pk_src.to_pubkey(&cur_account.map(|kp| kp.pubkey)?),
);
tlk.into_timeline(&txn, ndb)
.map(AddColumnResponse::Timeline)
}
AddColumnOption::ExternalNotification => Some(AddColumnResponse::ExternalNotification),
AddColumnOption::UndecidedHashtag => Some(AddColumnResponse::Hashtag),
AddColumnOption::Hashtag(hashtag) => TimelineKind::Hashtag(hashtag)
.into_timeline(ndb, None)
.into_timeline(&txn, ndb)
.map(AddColumnResponse::Timeline),
AddColumnOption::UndecidedIndividual => Some(AddColumnResponse::UndecidedIndividual),
AddColumnOption::ExternalIndividual => Some(AddColumnResponse::ExternalIndividual),
AddColumnOption::Individual(pubkey_source) => {
let tlk = TimelineKind::profile(pubkey_source);
tlk.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes()))
let tlk = TimelineKind::profile(
*pubkey_source.to_pubkey(&cur_account.map(|kp| kp.pubkey)?),
);
tlk.into_timeline(&txn, ndb)
.map(AddColumnResponse::Timeline)
}
}
@@ -232,13 +239,17 @@ impl<'a> AddColumnView<'a> {
})
}
fn algo_last_per_pk_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
fn algo_last_per_pk_ui(
&mut self,
ui: &mut Ui,
deck_author: Pubkey,
) -> Option<AddColumnResponse> {
let algo_option = ColumnOptionData {
title: "Contact List",
description: "Source the last note for each user in your contact list",
icon: egui::include_image!("../../../../assets/icons/home_icon_dark_4x.png"),
option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Decided(
ListKind::contact_list(PubkeySource::DeckAuthor),
ListKind::contact_list(deck_author),
))),
};
@@ -319,18 +330,22 @@ impl<'a> AddColumnView<'a> {
}
let resp = if let Some(keypair) = key_state.get_login_keypair() {
let txn = Transaction::new(self.ndb).expect("txn");
if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, keypair.pubkey.bytes()) {
egui::Frame::window(ui.style())
.outer_margin(Margin {
left: 4.0,
right: 4.0,
top: 12.0,
bottom: 32.0,
})
.show(ui, |ui| {
ProfilePreview::new(&profile, self.img_cache).ui(ui);
});
{
let txn = Transaction::new(self.ndb).expect("txn");
if let Ok(profile) =
self.ndb.get_profile_by_pubkey(&txn, keypair.pubkey.bytes())
{
egui::Frame::window(ui.style())
.outer_margin(Margin {
left: 4.0,
right: 4.0,
top: 12.0,
bottom: 32.0,
})
.show(ui, |ui| {
ProfilePreview::new(&profile, self.img_cache).ui(ui);
});
}
}
if ui.add(add_column_button()).clicked() {
@@ -470,7 +485,7 @@ impl<'a> AddColumnView<'a> {
title: "Contacts",
description: "See notes from your contacts",
icon: egui::include_image!("../../../../assets/icons/home_icon_dark_4x.png"),
option: AddColumnOption::Contacts(source.clone()),
option: AddColumnOption::Contacts(source),
});
}
vec.push(ColumnOptionData {
@@ -609,7 +624,13 @@ pub fn render_add_column_routes(
AddColumnRoute::Base => add_column_view.ui(ui),
AddColumnRoute::Algo(r) => match r {
AddAlgoRoute::Base => add_column_view.algo_ui(ui),
AddAlgoRoute::LastPerPubkey => add_column_view.algo_last_per_pk_ui(ui),
AddAlgoRoute::LastPerPubkey => {
if let Some(deck_author) = ctx.accounts.get_selected_account() {
add_column_view.algo_last_per_pk_ui(ui, deck_author.pubkey)
} else {
None
}
}
},
AddColumnRoute::UndecidedNotification => add_column_view.notifications_ui(ui),
AddColumnRoute::ExternalNotification => add_column_view.external_notification_ui(ui),
@@ -628,13 +649,16 @@ pub fn render_add_column_routes(
ctx.pool,
ctx.note_cache,
app.since_optimize,
ctx.accounts
.get_selected_account()
.as_ref()
.map(|sa| &sa.pubkey),
);
app.columns_mut(ctx.accounts)
.add_timeline_to_column(col, timeline);
.column_mut(col)
.router_mut()
.route_to_replaced(Route::timeline(timeline.kind.clone()));
app.timeline_cache
.timelines
.insert(timeline.kind.clone(), timeline);
}
AddColumnResponse::Algo(algo_option) => match algo_option {
@@ -654,14 +678,8 @@ pub fn render_add_column_routes(
// add it to our list of timelines
AlgoOption::LastPerPubkey(Decision::Decided(list_kind)) => {
let maybe_timeline = {
let default_user = ctx
.accounts
.get_selected_account()
.as_ref()
.map(|sa| sa.pubkey.bytes());
TimelineKind::last_per_pubkey(list_kind.clone())
.into_timeline(ctx.ndb, default_user)
let txn = Transaction::new(ctx.ndb).unwrap();
TimelineKind::last_per_pubkey(list_kind).into_timeline(&txn, ctx.ndb)
};
if let Some(mut timeline) = maybe_timeline {
@@ -672,14 +690,16 @@ pub fn render_add_column_routes(
ctx.pool,
ctx.note_cache,
app.since_optimize,
ctx.accounts
.get_selected_account()
.as_ref()
.map(|sa| &sa.pubkey),
);
app.columns_mut(ctx.accounts)
.add_timeline_to_column(col, timeline);
.column_mut(col)
.router_mut()
.route_to_replaced(Route::timeline(timeline.kind.clone()));
app.timeline_cache
.timelines
.insert(timeline.kind.clone(), timeline);
} else {
// we couldn't fetch the timeline yet... let's let
// the user know ?

View File

@@ -5,7 +5,7 @@ use crate::nav::SwitchingAction;
use crate::{
column::Columns,
route::Route,
timeline::{ColumnTitle, TimelineId, TimelineKind, TimelineRoute},
timeline::{ColumnTitle, TimelineKind},
ui::{
self,
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
@@ -22,7 +22,6 @@ pub struct NavTitle<'a> {
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
columns: &'a Columns,
deck_author: Option<&'a Pubkey>,
routes: &'a [Route],
col_id: usize,
}
@@ -32,7 +31,6 @@ impl<'a> NavTitle<'a> {
ndb: &'a Ndb,
img_cache: &'a mut ImageCache,
columns: &'a Columns,
deck_author: Option<&'a Pubkey>,
routes: &'a [Route],
col_id: usize,
) -> Self {
@@ -40,7 +38,6 @@ impl<'a> NavTitle<'a> {
ndb,
img_cache,
columns,
deck_author,
routes,
col_id,
}
@@ -123,14 +120,14 @@ impl<'a> NavTitle<'a> {
// not it looks cool
self.title_pfp(ui, prev, 32.0);
let column_title = prev.title(self.columns);
let column_title = prev.title();
let back_resp = match &column_title {
ColumnTitle::Simple(title) => ui.add(Self::back_label(title, color)),
ColumnTitle::NeedsDb(need_db) => {
let txn = Transaction::new(self.ndb).unwrap();
let title = need_db.title(&txn, self.ndb, self.deck_author);
let title = need_db.title(&txn, self.ndb);
ui.add(Self::back_label(title, color))
}
};
@@ -402,14 +399,11 @@ impl<'a> NavTitle<'a> {
})
}
fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: TimelineId, pfp_size: f32) {
fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) {
let txn = Transaction::new(self.ndb).unwrap();
if let Some(pfp) = self
.columns
.find_timeline(id)
.and_then(|tl| tl.kind.pubkey_source())
.and_then(|pksrc| self.deck_author.map(|da| pksrc.to_pubkey(da)))
if let Some(pfp) = id
.pubkey()
.and_then(|pk| self.pubkey_pfp(&txn, pk.bytes(), pfp_size))
{
ui.add(pfp);
@@ -422,34 +416,35 @@ impl<'a> NavTitle<'a> {
fn title_pfp(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) {
match top {
Route::Timeline(tlr) => match tlr {
TimelineRoute::Timeline(tlid) => {
let is_hashtag = self
.columns
.find_timeline(*tlid)
.is_some_and(|tl| matches!(tl.kind, TimelineKind::Hashtag(_)));
if is_hashtag {
ui.add(
egui::Image::new(egui::include_image!(
"../../../../../assets/icons/hashtag_icon_4x.png"
))
.fit_to_exact_size(egui::vec2(pfp_size, pfp_size)),
);
} else {
self.timeline_pfp(ui, *tlid, pfp_size);
}
Route::Timeline(kind) => match kind {
TimelineKind::Hashtag(_ht) => {
ui.add(
egui::Image::new(egui::include_image!(
"../../../../../assets/icons/hashtag_icon_4x.png"
))
.fit_to_exact_size(egui::vec2(pfp_size, pfp_size)),
);
}
TimelineRoute::Thread(_note_id) => {}
TimelineRoute::Reply(_note_id) => {}
TimelineRoute::Quote(_note_id) => {}
TimelineRoute::Profile(pubkey) => {
TimelineKind::Profile(pubkey) => {
self.show_profile(ui, pubkey, pfp_size);
}
TimelineKind::Thread(_) => {
// no pfp for threads
}
TimelineKind::Universe
| TimelineKind::Algo(_)
| TimelineKind::Notifications(_)
| TimelineKind::Generic(_)
| TimelineKind::List(_) => {
self.timeline_pfp(ui, kind, pfp_size);
}
},
Route::Reply(_) => {}
Route::Quote(_) => {}
Route::Accounts(_as) => {}
Route::ComposeNote => {}
Route::AddColumn(_add_col_route) => {}
@@ -480,7 +475,7 @@ impl<'a> NavTitle<'a> {
}
fn title_label(&self, ui: &mut egui::Ui, top: &Route) {
let column_title = top.title(self.columns);
let column_title = top.title();
match &column_title {
ColumnTitle::Simple(title) => {
@@ -489,7 +484,7 @@ impl<'a> NavTitle<'a> {
ColumnTitle::NeedsDb(need_db) => {
let txn = Transaction::new(self.ndb).unwrap();
let title = need_db.title(&txn, self.ndb, self.deck_author);
let title = need_db.title(&txn, self.ndb);
ui.add(Self::title_label_value(title));
}
};

View File

@@ -5,7 +5,7 @@ pub mod preview;
pub use edit::EditProfileView;
use egui::load::TexturePoll;
use egui::{vec2, Color32, Label, Layout, Rect, RichText, Rounding, ScrollArea, Sense, Stroke};
use enostr::{Pubkey, PubkeyRef};
use enostr::Pubkey;
use nostrdb::{Ndb, ProfileRecord, Transaction};
pub use picture::ProfilePic;
pub use preview::ProfilePreview;
@@ -15,7 +15,7 @@ use crate::{
actionbar::NoteAction,
colors, images,
profile::get_display_name,
timeline::{TimelineCache, TimelineCacheKey},
timeline::{TimelineCache, TimelineKind},
ui::{
note::NoteOptions,
timeline::{tabs_ui, TimelineTabView},
@@ -90,7 +90,7 @@ impl<'a> ProfileView<'a> {
self.ndb,
self.note_cache,
&txn,
TimelineCacheKey::Profile(PubkeyRef::new(self.pubkey.bytes())),
&TimelineKind::Profile(*self.pubkey),
)
.get_ptr();

View File

@@ -288,7 +288,7 @@ impl<'a> DesktopSidePanel<'a> {
if router
.routes()
.iter()
.any(|&r| r == Route::Accounts(AccountsRoute::Accounts))
.any(|r| r == &Route::Accounts(AccountsRoute::Accounts))
{
// return if we are already routing to accounts
router.go_back();
@@ -297,7 +297,7 @@ impl<'a> DesktopSidePanel<'a> {
}
}
SidePanelAction::Settings => {
if router.routes().iter().any(|&r| r == Route::Relays) {
if router.routes().iter().any(|r| r == &Route::Relays) {
// return if we are already routing to accounts
router.go_back();
} else {
@@ -308,7 +308,7 @@ impl<'a> DesktopSidePanel<'a> {
if router
.routes()
.iter()
.any(|&r| matches!(r, Route::AddColumn(_)))
.any(|r| matches!(r, Route::AddColumn(_)))
{
router.go_back();
} else {
@@ -316,7 +316,7 @@ impl<'a> DesktopSidePanel<'a> {
}
}
SidePanelAction::ComposeNote => {
if router.routes().iter().any(|&r| r == Route::ComposeNote) {
if router.routes().iter().any(|r| r == &Route::ComposeNote) {
router.go_back();
} else {
router.route_to(Route::ComposeNote);
@@ -331,7 +331,7 @@ impl<'a> DesktopSidePanel<'a> {
info!("Clicked expand side panel button");
}
SidePanelAction::Support => {
if router.routes().iter().any(|&r| r == Route::Support) {
if router.routes().iter().any(|r| r == &Route::Support) {
router.go_back();
} else {
support.refresh();
@@ -339,7 +339,7 @@ impl<'a> DesktopSidePanel<'a> {
}
}
SidePanelAction::NewDeck => {
if router.routes().iter().any(|&r| r == Route::NewDeck) {
if router.routes().iter().any(|r| r == &Route::NewDeck) {
router.go_back();
} else {
router.route_to(Route::NewDeck);
@@ -351,7 +351,7 @@ impl<'a> DesktopSidePanel<'a> {
)))
}
SidePanelAction::EditDeck(index) => {
if router.routes().iter().any(|&r| r == Route::EditDeck(index)) {
if router.routes().iter().any(|r| r == &Route::EditDeck(index)) {
router.go_back();
} else {
switching_response = Some(crate::nav::SwitchingAction::Decks(

View File

@@ -1,6 +1,6 @@
use crate::{
actionbar::NoteAction,
timeline::{TimelineCache, TimelineCacheKey},
timeline::{ThreadSelection, TimelineCache, TimelineKind},
ui::note::NoteOptions,
};
@@ -83,7 +83,7 @@ impl<'a> ThreadView<'a> {
self.ndb,
self.note_cache,
&txn,
TimelineCacheKey::Thread(root_id),
&TimelineKind::Thread(ThreadSelection::from_root_id(root_id.to_owned())),
)
.get_ptr();

View File

@@ -3,8 +3,7 @@ use std::f32::consts::PI;
use crate::actionbar::NoteAction;
use crate::timeline::TimelineTab;
use crate::{
column::Columns,
timeline::{TimelineId, ViewFilter},
timeline::{TimelineCache, TimelineKind, ViewFilter},
ui,
ui::note::NoteOptions,
};
@@ -19,8 +18,8 @@ use tracing::{error, warn};
use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
pub struct TimelineView<'a> {
timeline_id: TimelineId,
columns: &'a mut Columns,
timeline_id: &'a TimelineKind,
timeline_cache: &'a mut TimelineCache,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
@@ -31,8 +30,8 @@ pub struct TimelineView<'a> {
impl<'a> TimelineView<'a> {
pub fn new(
timeline_id: TimelineId,
columns: &'a mut Columns,
timeline_id: &'a TimelineKind,
timeline_cache: &'a mut TimelineCache,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
@@ -43,7 +42,7 @@ impl<'a> TimelineView<'a> {
TimelineView {
ndb,
timeline_id,
columns,
timeline_cache,
note_cache,
img_cache,
reverse,
@@ -57,7 +56,7 @@ impl<'a> TimelineView<'a> {
ui,
self.ndb,
self.timeline_id,
self.columns,
self.timeline_cache,
self.note_cache,
self.img_cache,
self.reverse,
@@ -76,8 +75,8 @@ impl<'a> TimelineView<'a> {
fn timeline_ui(
ui: &mut egui::Ui,
ndb: &Ndb,
timeline_id: TimelineId,
columns: &mut Columns,
timeline_id: &TimelineKind,
timeline_cache: &mut TimelineCache,
note_cache: &mut NoteCache,
img_cache: &mut ImageCache,
reversed: bool,
@@ -92,7 +91,7 @@ fn timeline_ui(
*/
let scroll_id = {
let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) {
let timeline = if let Some(timeline) = timeline_cache.timelines.get_mut(timeline_id) {
timeline
} else {
error!("tried to render timeline in column, but timeline was missing");
@@ -142,7 +141,7 @@ fn timeline_ui(
}
let scroll_output = scroll_area.show(ui, |ui| {
let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) {
let timeline = if let Some(timeline) = timeline_cache.timelines.get(timeline_id) {
timeline
} else {
error!("tried to render timeline in column, but timeline was missing");