Merge notification dot by kernel
kernelkind (6):
extract notifications filter to own method
add `NotesFreshness` to `TimelineTab`
set fresh from `TimelineCache`
chrome: method to find whether there are unseen notifications
paint unseen indicator
use unseen notification indicator
Changelog-Added: Add notification dot on toolbar
This commit is contained in:
@@ -12,7 +12,9 @@ use notedeck::{
|
|||||||
UserAccount, WalletType,
|
UserAccount, WalletType,
|
||||||
};
|
};
|
||||||
use notedeck_columns::{
|
use notedeck_columns::{
|
||||||
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
|
column::SelectionResult,
|
||||||
|
timeline::{kind::ListKind, TimelineKind},
|
||||||
|
Damus,
|
||||||
};
|
};
|
||||||
use notedeck_dave::{Dave, DaveAvatar};
|
use notedeck_dave::{Dave, DaveAvatar};
|
||||||
use notedeck_notebook::Notebook;
|
use notedeck_notebook::Notebook;
|
||||||
@@ -385,7 +387,12 @@ impl Chrome {
|
|||||||
});
|
});
|
||||||
|
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
if let Some(action) = self.toolbar(ui) {
|
let pk = ctx.accounts.get_selected_account().key.pubkey;
|
||||||
|
|
||||||
|
let unseen_notification =
|
||||||
|
unseen_notification(self.get_columns_app(), ctx.ndb, pk);
|
||||||
|
|
||||||
|
if let Some(action) = self.toolbar(ui, unseen_notification) {
|
||||||
got_action = Some(ChromePanelAction::Toolbar(action))
|
got_action = Some(ChromePanelAction::Toolbar(action))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -394,7 +401,7 @@ impl Chrome {
|
|||||||
got_action
|
got_action
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toolbar(&mut self, ui: &mut egui::Ui) -> Option<ToolbarAction> {
|
fn toolbar(&mut self, ui: &mut egui::Ui, unseen_notification: bool) -> Option<ToolbarAction> {
|
||||||
use egui_tabs::{TabColor, Tabs};
|
use egui_tabs::{TabColor, Tabs};
|
||||||
|
|
||||||
let rect = ui.available_rect_before_wrap();
|
let rect = ui.available_rect_before_wrap();
|
||||||
@@ -438,7 +445,9 @@ impl Chrome {
|
|||||||
action = Some(ToolbarAction::Dave);
|
action = Some(ToolbarAction::Dave);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if index == 2 && notifications_button(ui, btn_size).clicked() {
|
} else if index == 2
|
||||||
|
&& notifications_button(ui, btn_size, unseen_notification).clicked()
|
||||||
|
{
|
||||||
action = Some(ToolbarAction::Notifications);
|
action = Some(ToolbarAction::Notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,6 +528,38 @@ impl Chrome {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn unseen_notification(
|
||||||
|
columns: Option<&mut Damus>,
|
||||||
|
ndb: &nostrdb::Ndb,
|
||||||
|
current_pk: notedeck::enostr::Pubkey,
|
||||||
|
) -> bool {
|
||||||
|
let Some(columns) = columns else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(tl) = columns
|
||||||
|
.timeline_cache
|
||||||
|
.get_mut(&TimelineKind::Notifications(current_pk))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let freshness = &mut tl.current_view_mut().freshness;
|
||||||
|
freshness.update(|timestamp_last_viewed| {
|
||||||
|
let filter = notedeck_columns::timeline::kind::notifications_filter(¤t_pk)
|
||||||
|
.since_mut(timestamp_last_viewed);
|
||||||
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
|
|
||||||
|
let Some(res) = ndb.query(&txn, &[filter], 1).ok() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
!res.is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
freshness.has_unseen()
|
||||||
|
}
|
||||||
|
|
||||||
impl notedeck::App for Chrome {
|
impl notedeck::App for Chrome {
|
||||||
fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> {
|
fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> {
|
||||||
if let Some(action) = self.show(ctx, ui) {
|
if let Some(action) = self.show(ctx, ui) {
|
||||||
@@ -572,6 +613,7 @@ fn expanding_button(
|
|||||||
light_img: egui::Image,
|
light_img: egui::Image,
|
||||||
dark_img: egui::Image,
|
dark_img: egui::Image,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
unseen_indicator: bool,
|
||||||
) -> egui::Response {
|
) -> egui::Response {
|
||||||
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
||||||
let img = if ui.visuals().dark_mode {
|
let img = if ui.visuals().dark_mode {
|
||||||
@@ -583,16 +625,34 @@ fn expanding_button(
|
|||||||
let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size));
|
let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size));
|
||||||
|
|
||||||
let cur_img_size = helper.scale_1d_pos(img_size);
|
let cur_img_size = helper.scale_1d_pos(img_size);
|
||||||
img.paint_at(
|
|
||||||
ui,
|
let paint_rect = helper
|
||||||
helper
|
.get_animation_rect()
|
||||||
.get_animation_rect()
|
.shrink((max_size - cur_img_size) / 2.0);
|
||||||
.shrink((max_size - cur_img_size) / 2.0),
|
img.paint_at(ui, paint_rect);
|
||||||
);
|
|
||||||
|
if unseen_indicator {
|
||||||
|
paint_unseen_indicator(ui, paint_rect, helper.scale_1d_pos(3.0));
|
||||||
|
}
|
||||||
|
|
||||||
helper.take_animation_response()
|
helper.take_animation_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_unseen_indicator(ui: &mut egui::Ui, rect: egui::Rect, radius: f32) {
|
||||||
|
let center = rect.center();
|
||||||
|
let top_right = rect.right_top();
|
||||||
|
let distance = center.distance(top_right);
|
||||||
|
let midpoint = {
|
||||||
|
let mut cur = center;
|
||||||
|
cur.x += distance / 2.0;
|
||||||
|
cur.y -= distance / 2.0;
|
||||||
|
cur
|
||||||
|
};
|
||||||
|
|
||||||
|
let painter = ui.painter_at(rect);
|
||||||
|
painter.circle_filled(midpoint, radius, notedeck_ui::colors::PINK);
|
||||||
|
}
|
||||||
|
|
||||||
fn support_button(ui: &mut egui::Ui) -> egui::Response {
|
fn support_button(ui: &mut egui::Ui) -> egui::Response {
|
||||||
expanding_button(
|
expanding_button(
|
||||||
"help-button",
|
"help-button",
|
||||||
@@ -600,6 +660,7 @@ fn support_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::help_light_image(),
|
app_images::help_light_image(),
|
||||||
app_images::help_dark_image(),
|
app_images::help_dark_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,16 +671,18 @@ fn settings_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::settings_light_image(),
|
app_images::settings_light_image(),
|
||||||
app_images::settings_dark_image(),
|
app_images::settings_dark_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notifications_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
|
fn notifications_button(ui: &mut egui::Ui, size: f32, unseen_indicator: bool) -> egui::Response {
|
||||||
expanding_button(
|
expanding_button(
|
||||||
"notifications-button",
|
"notifications-button",
|
||||||
size,
|
size,
|
||||||
app_images::notifications_light_image(),
|
app_images::notifications_light_image(),
|
||||||
app_images::notifications_dark_image(),
|
app_images::notifications_dark_image(),
|
||||||
ui,
|
ui,
|
||||||
|
unseen_indicator,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,6 +693,7 @@ fn home_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
|
|||||||
app_images::home_light_image(),
|
app_images::home_light_image(),
|
||||||
app_images::home_dark_image(),
|
app_images::home_dark_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,6 +704,7 @@ fn columns_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::columns_image(),
|
app_images::columns_image(),
|
||||||
app_images::columns_image(),
|
app_images::columns_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,6 +715,7 @@ fn accounts_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::accounts_image().tint(ui.visuals().text_color()),
|
app_images::accounts_image().tint(ui.visuals().text_color()),
|
||||||
app_images::accounts_image(),
|
app_images::accounts_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,6 +726,7 @@ fn notebook_button(ui: &mut egui::Ui) -> egui::Response {
|
|||||||
app_images::algo_image(),
|
app_images::algo_image(),
|
||||||
app_images::algo_image(),
|
app_images::algo_image(),
|
||||||
ui,
|
ui,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -545,6 +545,8 @@ fn render_nav_body(
|
|||||||
scroll_to_top,
|
scroll_to_top,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.timeline_cache.set_fresh(kind);
|
||||||
|
|
||||||
// always clear the scroll_to_top request
|
// always clear the scroll_to_top request
|
||||||
if scroll_to_top {
|
if scroll_to_top {
|
||||||
app.options.remove(AppOptions::ScrollToTop);
|
app.options.remove(AppOptions::ScrollToTop);
|
||||||
|
|||||||
@@ -221,6 +221,14 @@ impl TimelineCache {
|
|||||||
pub fn num_timelines(&self) -> usize {
|
pub fn num_timelines(&self) -> usize {
|
||||||
self.timelines.len()
|
self.timelines.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_fresh(&mut self, kind: &TimelineKind) {
|
||||||
|
let Some(tl) = self.get_mut(kind) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
tl.current_view_mut().freshness.set_fresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look for new thread notes since our last fetch
|
/// Look for new thread notes since our last fetch
|
||||||
|
|||||||
@@ -471,11 +471,9 @@ impl TimelineKind {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// TODO: still need to update this to fetch likes, zaps, etc
|
// TODO: still need to update this to fetch likes, zaps, etc
|
||||||
TimelineKind::Notifications(pubkey) => FilterState::ready(vec![Filter::new()
|
TimelineKind::Notifications(pubkey) => {
|
||||||
.pubkeys([pubkey.bytes()])
|
FilterState::ready(vec![notifications_filter(pubkey)])
|
||||||
.kinds([1])
|
}
|
||||||
.limit(default_limit())
|
|
||||||
.build()]),
|
|
||||||
|
|
||||||
TimelineKind::Hashtag(hashtag) => {
|
TimelineKind::Hashtag(hashtag) => {
|
||||||
let filters = hashtag
|
let filters = hashtag
|
||||||
@@ -573,11 +571,7 @@ impl TimelineKind {
|
|||||||
)),
|
)),
|
||||||
|
|
||||||
TimelineKind::Notifications(pk) => {
|
TimelineKind::Notifications(pk) => {
|
||||||
let notifications_filter = Filter::new()
|
let notifications_filter = notifications_filter(&pk);
|
||||||
.pubkeys([pk.bytes()])
|
|
||||||
.kinds([1])
|
|
||||||
.limit(default_limit())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Some(Timeline::new(
|
Some(Timeline::new(
|
||||||
TimelineKind::notifications(pk),
|
TimelineKind::notifications(pk),
|
||||||
@@ -628,6 +622,14 @@ impl TimelineKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn notifications_filter(pk: &Pubkey) -> Filter {
|
||||||
|
Filter::new()
|
||||||
|
.pubkeys([pk.bytes()])
|
||||||
|
.kinds([1])
|
||||||
|
.limit(default_limit())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TitleNeedsDb<'a> {
|
pub struct TitleNeedsDb<'a> {
|
||||||
kind: &'a TimelineKind,
|
kind: &'a TimelineKind,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
|
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
contacts::hybrid_contacts_filter,
|
contacts::hybrid_contacts_filter,
|
||||||
|
debouncer::Debouncer,
|
||||||
filter::{self, HybridFilter},
|
filter::{self, HybridFilter},
|
||||||
tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization,
|
tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization,
|
||||||
NoteCache, NoteRef, UnknownIds,
|
NoteCache, NoteRef, UnknownIds,
|
||||||
@@ -16,8 +17,11 @@ use notedeck::{
|
|||||||
use egui_virtual_list::VirtualList;
|
use egui_virtual_list::VirtualList;
|
||||||
use enostr::{PoolRelay, Pubkey, RelayPool};
|
use enostr::{PoolRelay, Pubkey, RelayPool};
|
||||||
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
|
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
|
||||||
use std::cell::RefCell;
|
use std::{
|
||||||
use std::rc::Rc;
|
cell::RefCell,
|
||||||
|
time::{Duration, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use std::{rc::Rc, time::SystemTime};
|
||||||
|
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
@@ -103,6 +107,7 @@ pub struct TimelineTab {
|
|||||||
pub selection: i32,
|
pub selection: i32,
|
||||||
pub filter: ViewFilter,
|
pub filter: ViewFilter,
|
||||||
pub list: Rc<RefCell<VirtualList>>,
|
pub list: Rc<RefCell<VirtualList>>,
|
||||||
|
pub freshness: NotesFreshness,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimelineTab {
|
impl TimelineTab {
|
||||||
@@ -138,6 +143,7 @@ impl TimelineTab {
|
|||||||
selection,
|
selection,
|
||||||
filter,
|
filter,
|
||||||
list,
|
list,
|
||||||
|
freshness: NotesFreshness::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,3 +786,101 @@ pub fn is_timeline_ready(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NotesFreshness {
|
||||||
|
debouncer: Debouncer,
|
||||||
|
state: NotesFreshnessState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum NotesFreshnessState {
|
||||||
|
Fresh {
|
||||||
|
timestamp_viewed: u64,
|
||||||
|
},
|
||||||
|
Stale {
|
||||||
|
have_unseen: bool,
|
||||||
|
timestamp_last_viewed: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NotesFreshness {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
debouncer: Debouncer::new(Duration::from_secs(2)),
|
||||||
|
state: NotesFreshnessState::Stale {
|
||||||
|
have_unseen: true,
|
||||||
|
timestamp_last_viewed: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotesFreshness {
|
||||||
|
pub fn set_fresh(&mut self) {
|
||||||
|
if !self.debouncer.should_act() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.state = NotesFreshnessState::Fresh {
|
||||||
|
timestamp_viewed: timestamp_now(),
|
||||||
|
};
|
||||||
|
self.debouncer.bounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, check_have_unseen: impl FnOnce(u64) -> bool) {
|
||||||
|
if !self.debouncer.should_act() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match &self.state {
|
||||||
|
NotesFreshnessState::Fresh { timestamp_viewed } => {
|
||||||
|
let Ok(dur) = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH + Duration::from_secs(*timestamp_viewed))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if dur > Duration::from_secs(2) {
|
||||||
|
self.state = NotesFreshnessState::Stale {
|
||||||
|
have_unseen: check_have_unseen(*timestamp_viewed),
|
||||||
|
timestamp_last_viewed: *timestamp_viewed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NotesFreshnessState::Stale {
|
||||||
|
have_unseen,
|
||||||
|
timestamp_last_viewed,
|
||||||
|
} => {
|
||||||
|
if *have_unseen {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = NotesFreshnessState::Stale {
|
||||||
|
have_unseen: check_have_unseen(*timestamp_last_viewed),
|
||||||
|
timestamp_last_viewed: *timestamp_last_viewed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.debouncer.bounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_unseen(&self) -> bool {
|
||||||
|
match &self.state {
|
||||||
|
NotesFreshnessState::Fresh {
|
||||||
|
timestamp_viewed: _,
|
||||||
|
} => false,
|
||||||
|
NotesFreshnessState::Stale {
|
||||||
|
have_unseen,
|
||||||
|
timestamp_last_viewed: _,
|
||||||
|
} => *have_unseen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timestamp_now() -> u64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or(Duration::ZERO)
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user