integrate new threads conception
Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
@@ -625,6 +625,7 @@ fn chrome_handle_app_action(
|
|||||||
cols,
|
cols,
|
||||||
0,
|
0,
|
||||||
&mut columns.timeline_cache,
|
&mut columns.timeline_cache,
|
||||||
|
&mut columns.threads,
|
||||||
ctx.note_cache,
|
ctx.note_cache,
|
||||||
ctx.pool,
|
ctx.pool,
|
||||||
&txn,
|
&txn,
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ struct NoteActionResponse {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn execute_note_action(
|
fn execute_note_action(
|
||||||
action: NoteAction,
|
action: NoteAction,
|
||||||
ndb: &Ndb,
|
ndb: &mut Ndb,
|
||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
|
threads: &mut Threads,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
@@ -52,6 +53,7 @@ fn execute_note_action(
|
|||||||
images: &mut Images,
|
images: &mut Images,
|
||||||
router_type: RouterType,
|
router_type: RouterType,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
col: usize,
|
||||||
) -> NoteActionResponse {
|
) -> NoteActionResponse {
|
||||||
let mut timeline_res = None;
|
let mut timeline_res = None;
|
||||||
let mut router_action = None;
|
let mut router_action = None;
|
||||||
@@ -74,13 +76,16 @@ fn execute_note_action(
|
|||||||
break 'ex;
|
break 'ex;
|
||||||
};
|
};
|
||||||
|
|
||||||
let kind = TimelineKind::Thread(thread_selection);
|
timeline_res = threads
|
||||||
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone())));
|
.open(ndb, txn, pool, &thread_selection, preview, col)
|
||||||
// NOTE!!: you need the note_id to timeline root id thing
|
.map(NotesOpenResult::Thread);
|
||||||
|
|
||||||
timeline_res = timeline_cache
|
let route = Route::Thread(thread_selection);
|
||||||
.open(ndb, note_cache, txn, pool, &kind)
|
|
||||||
.map(NotesOpenResult::Timeline);
|
router_action = Some(RouterAction::Overlay {
|
||||||
|
route,
|
||||||
|
make_new: preview,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
NoteAction::Hashtag(htag) => {
|
NoteAction::Hashtag(htag) => {
|
||||||
let kind = TimelineKind::Hashtag(htag.clone());
|
let kind = TimelineKind::Hashtag(htag.clone());
|
||||||
@@ -151,10 +156,11 @@ fn execute_note_action(
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn execute_and_process_note_action(
|
pub fn execute_and_process_note_action(
|
||||||
action: NoteAction,
|
action: NoteAction,
|
||||||
ndb: &Ndb,
|
ndb: &mut Ndb,
|
||||||
columns: &mut Columns,
|
columns: &mut Columns,
|
||||||
col: usize,
|
col: usize,
|
||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
|
threads: &mut Threads,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
@@ -179,6 +185,7 @@ pub fn execute_and_process_note_action(
|
|||||||
action,
|
action,
|
||||||
ndb,
|
ndb,
|
||||||
timeline_cache,
|
timeline_cache,
|
||||||
|
threads,
|
||||||
note_cache,
|
note_cache,
|
||||||
pool,
|
pool,
|
||||||
txn,
|
txn,
|
||||||
@@ -188,6 +195,7 @@ pub fn execute_and_process_note_action(
|
|||||||
images,
|
images,
|
||||||
router_type,
|
router_type,
|
||||||
ui,
|
ui,
|
||||||
|
col,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(br) = resp.timeline_res {
|
if let Some(br) = resp.timeline_res {
|
||||||
@@ -195,7 +203,9 @@ pub fn execute_and_process_note_action(
|
|||||||
NotesOpenResult::Timeline(timeline_open_result) => {
|
NotesOpenResult::Timeline(timeline_open_result) => {
|
||||||
timeline_open_result.process(ndb, note_cache, txn, timeline_cache, unknown_ids);
|
timeline_open_result.process(ndb, note_cache, txn, timeline_cache, unknown_ids);
|
||||||
}
|
}
|
||||||
NotesOpenResult::Thread(new_thread_notes) => todo!(),
|
NotesOpenResult::Thread(thread_open_result) => {
|
||||||
|
thread_open_result.process(threads, ndb, txn, unknown_ids, note_cache);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +268,7 @@ impl NewNotes {
|
|||||||
unknown_ids: &mut UnknownIds,
|
unknown_ids: &mut UnknownIds,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
) {
|
) {
|
||||||
let reversed = matches!(&self.id, TimelineKind::Thread(_));
|
let reversed = false;
|
||||||
|
|
||||||
let timeline = if let Some(profile) = timeline_cache.timelines.get_mut(&self.id) {
|
let timeline = if let Some(profile) = timeline_cache.timelines.get_mut(&self.id) {
|
||||||
profile
|
profile
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
storage,
|
storage,
|
||||||
subscriptions::{SubKind, Subscriptions},
|
subscriptions::{SubKind, Subscriptions},
|
||||||
support::Support,
|
support::Support,
|
||||||
timeline::{self, TimelineCache},
|
timeline::{self, thread::Threads, TimelineCache},
|
||||||
ui::{self, DesktopSidePanel},
|
ui::{self, DesktopSidePanel},
|
||||||
view_state::ViewState,
|
view_state::ViewState,
|
||||||
Result,
|
Result,
|
||||||
@@ -45,6 +45,7 @@ pub struct Damus {
|
|||||||
pub subscriptions: Subscriptions,
|
pub subscriptions: Subscriptions,
|
||||||
pub support: Support,
|
pub support: Support,
|
||||||
pub jobs: JobsCache,
|
pub jobs: JobsCache,
|
||||||
|
pub threads: Threads,
|
||||||
|
|
||||||
//frame_history: crate::frame_history::FrameHistory,
|
//frame_history: crate::frame_history::FrameHistory,
|
||||||
|
|
||||||
@@ -443,6 +444,8 @@ impl Damus {
|
|||||||
|
|
||||||
ctx.accounts.with_fallback(FALLBACK_PUBKEY());
|
ctx.accounts.with_fallback(FALLBACK_PUBKEY());
|
||||||
|
|
||||||
|
let threads = Threads::default();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
subscriptions: Subscriptions::default(),
|
subscriptions: Subscriptions::default(),
|
||||||
since_optimize: parsed_args.since_optimize,
|
since_optimize: parsed_args.since_optimize,
|
||||||
@@ -458,6 +461,7 @@ impl Damus {
|
|||||||
debug,
|
debug,
|
||||||
unrecognized_args,
|
unrecognized_args,
|
||||||
jobs,
|
jobs,
|
||||||
|
threads,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,6 +506,7 @@ impl Damus {
|
|||||||
decks_cache,
|
decks_cache,
|
||||||
unrecognized_args: BTreeSet::default(),
|
unrecognized_args: BTreeSet::default(),
|
||||||
jobs: JobsCache::default(),
|
jobs: JobsCache::default(),
|
||||||
|
threads: Threads::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ use crate::{
|
|||||||
profile_state::ProfileState,
|
profile_state::ProfileState,
|
||||||
relay_pool_manager::RelayPoolManager,
|
relay_pool_manager::RelayPoolManager,
|
||||||
route::{Route, Router, SingletonRouter},
|
route::{Route, Router, SingletonRouter},
|
||||||
timeline::{route::render_timeline_route, TimelineCache},
|
timeline::{
|
||||||
|
route::{render_thread_route, render_timeline_route},
|
||||||
|
TimelineCache,
|
||||||
|
},
|
||||||
ui::{
|
ui::{
|
||||||
self,
|
self,
|
||||||
add_column::render_add_column_routes,
|
add_column::render_add_column_routes,
|
||||||
@@ -210,7 +213,7 @@ fn process_nav_resp(
|
|||||||
|
|
||||||
if let Some(action) = response.action {
|
if let Some(action) = response.action {
|
||||||
match action {
|
match action {
|
||||||
NavAction::Returned(_) => {
|
NavAction::Returned(return_type) => {
|
||||||
let r = app
|
let r = app
|
||||||
.columns_mut(ctx.accounts)
|
.columns_mut(ctx.accounts)
|
||||||
.column_mut(col)
|
.column_mut(col)
|
||||||
@@ -223,6 +226,12 @@ fn process_nav_resp(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(Route::Thread(selection)) = &r {
|
||||||
|
tracing::info!("Return type: {:?}", return_type);
|
||||||
|
app.threads
|
||||||
|
.close(ctx.ndb, ctx.pool, selection, return_type, col);
|
||||||
|
}
|
||||||
|
|
||||||
process_result = Some(ProcessNavResult::SwitchOccurred);
|
process_result = Some(ProcessNavResult::SwitchOccurred);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +364,7 @@ fn process_render_nav_action(
|
|||||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
|
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
|
||||||
col,
|
col,
|
||||||
&mut app.timeline_cache,
|
&mut app.timeline_cache,
|
||||||
|
&mut app.threads,
|
||||||
ctx.note_cache,
|
ctx.note_cache,
|
||||||
ctx.pool,
|
ctx.pool,
|
||||||
&txn,
|
&txn,
|
||||||
@@ -426,6 +436,17 @@ fn render_nav_body(
|
|||||||
&mut note_context,
|
&mut note_context,
|
||||||
&mut app.jobs,
|
&mut app.jobs,
|
||||||
),
|
),
|
||||||
|
Route::Thread(selection) => render_thread_route(
|
||||||
|
ctx.unknown_ids,
|
||||||
|
&mut app.threads,
|
||||||
|
ctx.accounts,
|
||||||
|
selection,
|
||||||
|
col,
|
||||||
|
app.note_options,
|
||||||
|
ui,
|
||||||
|
&mut note_context,
|
||||||
|
&mut app.jobs,
|
||||||
|
),
|
||||||
Route::Accounts(amr) => {
|
Route::Accounts(amr) => {
|
||||||
let mut action = render_accounts_route(
|
let mut action = render_accounts_route(
|
||||||
ui,
|
ui,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use enostr::{NoteId, Pubkey};
|
use enostr::{NoteId, Pubkey};
|
||||||
use notedeck::{NoteZapTargetOwned, WalletType};
|
use notedeck::{NoteZapTargetOwned, RootNoteIdBuf, WalletType};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self},
|
fmt::{self},
|
||||||
ops::Range,
|
ops::Range,
|
||||||
@@ -20,6 +20,7 @@ use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
|
|||||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||||
pub enum Route {
|
pub enum Route {
|
||||||
Timeline(TimelineKind),
|
Timeline(TimelineKind),
|
||||||
|
Thread(ThreadSelection),
|
||||||
Accounts(AccountsRoute),
|
Accounts(AccountsRoute),
|
||||||
Reply(NoteId),
|
Reply(NoteId),
|
||||||
Quote(NoteId),
|
Quote(NoteId),
|
||||||
@@ -53,7 +54,7 @@ impl Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn thread(thread_selection: ThreadSelection) -> Self {
|
pub fn thread(thread_selection: ThreadSelection) -> Self {
|
||||||
Route::Timeline(TimelineKind::Thread(thread_selection))
|
Route::Thread(thread_selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profile(pubkey: Pubkey) -> Self {
|
pub fn profile(pubkey: Pubkey) -> Self {
|
||||||
@@ -79,6 +80,18 @@ impl Route {
|
|||||||
pub fn serialize_tokens(&self, writer: &mut TokenWriter) {
|
pub fn serialize_tokens(&self, writer: &mut TokenWriter) {
|
||||||
match self {
|
match self {
|
||||||
Route::Timeline(timeline_kind) => timeline_kind.serialize_tokens(writer),
|
Route::Timeline(timeline_kind) => timeline_kind.serialize_tokens(writer),
|
||||||
|
Route::Thread(selection) => {
|
||||||
|
writer.write_token("thread");
|
||||||
|
|
||||||
|
if let Some(reply) = selection.selected_note {
|
||||||
|
writer.write_token("root");
|
||||||
|
writer.write_token(&NoteId::new(*selection.root_id.bytes()).hex());
|
||||||
|
writer.write_token("reply");
|
||||||
|
writer.write_token(&reply.hex());
|
||||||
|
} else {
|
||||||
|
writer.write_token(&NoteId::new(*selection.root_id.bytes()).hex());
|
||||||
|
}
|
||||||
|
}
|
||||||
Route::Accounts(routes) => routes.serialize_tokens(writer),
|
Route::Accounts(routes) => routes.serialize_tokens(writer),
|
||||||
Route::AddColumn(routes) => routes.serialize_tokens(writer),
|
Route::AddColumn(routes) => routes.serialize_tokens(writer),
|
||||||
Route::Search => writer.write_token("search"),
|
Route::Search => writer.write_token("search"),
|
||||||
@@ -199,6 +212,31 @@ impl Route {
|
|||||||
Ok(Route::Search)
|
Ok(Route::Search)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|p| {
|
||||||
|
p.parse_all(|p| {
|
||||||
|
p.parse_token("thread")?;
|
||||||
|
p.parse_token("root")?;
|
||||||
|
|
||||||
|
let root = tokenator::parse_hex_id(p)?;
|
||||||
|
|
||||||
|
p.parse_token("reply")?;
|
||||||
|
|
||||||
|
let selected = tokenator::parse_hex_id(p)?;
|
||||||
|
|
||||||
|
Ok(Route::Thread(ThreadSelection {
|
||||||
|
root_id: RootNoteIdBuf::new_unsafe(root),
|
||||||
|
selected_note: Some(NoteId::new(selected)),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|p| {
|
||||||
|
p.parse_all(|p| {
|
||||||
|
p.parse_token("thread")?;
|
||||||
|
Ok(Route::Thread(ThreadSelection::from_root_id(
|
||||||
|
RootNoteIdBuf::new_unsafe(tokenator::parse_hex_id(p)?),
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -206,6 +244,7 @@ impl Route {
|
|||||||
pub fn title(&self) -> ColumnTitle<'_> {
|
pub fn title(&self) -> ColumnTitle<'_> {
|
||||||
match self {
|
match self {
|
||||||
Route::Timeline(kind) => kind.to_title(),
|
Route::Timeline(kind) => kind.to_title(),
|
||||||
|
Route::Thread(_) => ColumnTitle::simple("Thread"),
|
||||||
Route::Reply(_id) => ColumnTitle::simple("Reply"),
|
Route::Reply(_id) => ColumnTitle::simple("Reply"),
|
||||||
Route::Quote(_id) => ColumnTitle::simple("Quote"),
|
Route::Quote(_id) => ColumnTitle::simple("Quote"),
|
||||||
Route::Relays => ColumnTitle::simple("Relays"),
|
Route::Relays => ColumnTitle::simple("Relays"),
|
||||||
@@ -423,9 +462,9 @@ impl fmt::Display for Route {
|
|||||||
TimelineKind::Generic(_) => write!(f, "Custom"),
|
TimelineKind::Generic(_) => write!(f, "Custom"),
|
||||||
TimelineKind::Search(_) => write!(f, "Search"),
|
TimelineKind::Search(_) => write!(f, "Search"),
|
||||||
TimelineKind::Hashtag(ht) => write!(f, "Hashtag ({})", ht),
|
TimelineKind::Hashtag(ht) => write!(f, "Hashtag ({})", ht),
|
||||||
TimelineKind::Thread(_id) => write!(f, "Thread"),
|
|
||||||
TimelineKind::Profile(_id) => write!(f, "Profile"),
|
TimelineKind::Profile(_id) => write!(f, "Profile"),
|
||||||
},
|
},
|
||||||
|
Route::Thread(_) => write!(f, "Thread"),
|
||||||
Route::Reply(_id) => write!(f, "Reply"),
|
Route::Reply(_id) => write!(f, "Reply"),
|
||||||
Route::Quote(_id) => write!(f, "Quote"),
|
Route::Quote(_id) => write!(f, "Quote"),
|
||||||
Route::Relays => write!(f, "Relays"),
|
Route::Relays => write!(f, "Relays"),
|
||||||
@@ -482,3 +521,30 @@ impl<R: Clone> Default for SingletonRouter<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use enostr::NoteId;
|
||||||
|
use tokenator::{TokenParser, TokenWriter};
|
||||||
|
|
||||||
|
use crate::{timeline::ThreadSelection, Route};
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use notedeck::RootNoteIdBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_thread_route_serialize() {
|
||||||
|
let note_id_hex = "1c54e5b0c386425f7e017d9e068ddef8962eb2ce1bb08ed27e24b93411c12e60";
|
||||||
|
let note_id = NoteId::from_hex(note_id_hex).unwrap();
|
||||||
|
let data_str = format!("thread:{}", note_id_hex);
|
||||||
|
let data = &data_str.split(":").collect::<Vec<&str>>();
|
||||||
|
let mut token_writer = TokenWriter::default();
|
||||||
|
let mut parser = TokenParser::new(&data);
|
||||||
|
let parsed = Route::parse(&mut parser, &Pubkey::new(*note_id.bytes())).unwrap();
|
||||||
|
let expected = Route::Thread(ThreadSelection::from_root_id(RootNoteIdBuf::new_unsafe(
|
||||||
|
*note_id.bytes(),
|
||||||
|
)));
|
||||||
|
parsed.serialize_tokens(&mut token_writer);
|
||||||
|
assert_eq!(expected, parsed);
|
||||||
|
assert_eq!(token_writer.str(), data_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -208,8 +208,6 @@ pub enum TimelineKind {
|
|||||||
|
|
||||||
Profile(Pubkey),
|
Profile(Pubkey),
|
||||||
|
|
||||||
Thread(ThreadSelection),
|
|
||||||
|
|
||||||
Universe,
|
Universe,
|
||||||
|
|
||||||
/// Generic filter, references a hash of a filter
|
/// Generic filter, references a hash of a filter
|
||||||
@@ -266,7 +264,6 @@ impl Display for TimelineKind {
|
|||||||
TimelineKind::Profile(_) => f.write_str("Profile"),
|
TimelineKind::Profile(_) => f.write_str("Profile"),
|
||||||
TimelineKind::Universe => f.write_str("Universe"),
|
TimelineKind::Universe => f.write_str("Universe"),
|
||||||
TimelineKind::Hashtag(_) => f.write_str("Hashtag"),
|
TimelineKind::Hashtag(_) => f.write_str("Hashtag"),
|
||||||
TimelineKind::Thread(_) => f.write_str("Thread"),
|
|
||||||
TimelineKind::Search(_) => f.write_str("Search"),
|
TimelineKind::Search(_) => f.write_str("Search"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,7 +279,6 @@ impl TimelineKind {
|
|||||||
TimelineKind::Universe => None,
|
TimelineKind::Universe => None,
|
||||||
TimelineKind::Generic(_) => None,
|
TimelineKind::Generic(_) => None,
|
||||||
TimelineKind::Hashtag(_ht) => None,
|
TimelineKind::Hashtag(_ht) => None,
|
||||||
TimelineKind::Thread(_ht) => None,
|
|
||||||
TimelineKind::Search(query) => query.author(),
|
TimelineKind::Search(query) => query.author(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,7 +294,6 @@ impl TimelineKind {
|
|||||||
TimelineKind::Universe => true,
|
TimelineKind::Universe => true,
|
||||||
TimelineKind::Generic(_) => true,
|
TimelineKind::Generic(_) => true,
|
||||||
TimelineKind::Hashtag(_ht) => true,
|
TimelineKind::Hashtag(_ht) => true,
|
||||||
TimelineKind::Thread(_ht) => true,
|
|
||||||
TimelineKind::Search(_q) => true,
|
TimelineKind::Search(_q) => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,10 +316,6 @@ impl TimelineKind {
|
|||||||
writer.write_token("profile");
|
writer.write_token("profile");
|
||||||
PubkeySource::pubkey(*pk).serialize_tokens(writer);
|
PubkeySource::pubkey(*pk).serialize_tokens(writer);
|
||||||
}
|
}
|
||||||
TimelineKind::Thread(root_note_id) => {
|
|
||||||
writer.write_token("thread");
|
|
||||||
writer.write_token(&root_note_id.root_id.hex());
|
|
||||||
}
|
|
||||||
TimelineKind::Universe => {
|
TimelineKind::Universe => {
|
||||||
writer.write_token("universe");
|
writer.write_token("universe");
|
||||||
}
|
}
|
||||||
@@ -377,12 +368,6 @@ impl TimelineKind {
|
|||||||
TokenParser::alt(
|
TokenParser::alt(
|
||||||
parser,
|
parser,
|
||||||
&[
|
&[
|
||||||
|p| {
|
|
||||||
p.parse_token("thread")?;
|
|
||||||
Ok(TimelineKind::Thread(ThreadSelection::from_root_id(
|
|
||||||
RootNoteIdBuf::new_unsafe(tokenator::parse_hex_id(p)?),
|
|
||||||
)))
|
|
||||||
},
|
|
||||||
|p| {
|
|p| {
|
||||||
p.parse_token("universe")?;
|
p.parse_token("universe")?;
|
||||||
Ok(TimelineKind::Universe)
|
Ok(TimelineKind::Universe)
|
||||||
@@ -425,10 +410,6 @@ impl TimelineKind {
|
|||||||
TimelineKind::Profile(pk)
|
TimelineKind::Profile(pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn thread(selected_note: ThreadSelection) -> Self {
|
|
||||||
TimelineKind::Thread(selected_note)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_notifications(&self) -> bool {
|
pub fn is_notifications(&self) -> bool {
|
||||||
matches!(self, TimelineKind::Notifications(_))
|
matches!(self, TimelineKind::Notifications(_))
|
||||||
}
|
}
|
||||||
@@ -474,17 +455,6 @@ impl TimelineKind {
|
|||||||
todo!("implement generic filter lookups")
|
todo!("implement generic filter lookups")
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineKind::Thread(selection) => FilterState::ready(vec![
|
|
||||||
nostrdb::Filter::new()
|
|
||||||
.kinds([1])
|
|
||||||
.event(selection.root_id.bytes())
|
|
||||||
.build(),
|
|
||||||
nostrdb::Filter::new()
|
|
||||||
.ids([selection.root_id.bytes()])
|
|
||||||
.limit(1)
|
|
||||||
.build(),
|
|
||||||
]),
|
|
||||||
|
|
||||||
TimelineKind::Profile(pk) => FilterState::ready(vec![Filter::new()
|
TimelineKind::Profile(pk) => FilterState::ready(vec![Filter::new()
|
||||||
.authors([pk.bytes()])
|
.authors([pk.bytes()])
|
||||||
.kinds([1])
|
.kinds([1])
|
||||||
@@ -510,8 +480,6 @@ impl TimelineKind {
|
|||||||
TimelineTab::full_tabs(),
|
TimelineTab::full_tabs(),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
TimelineKind::Thread(root_id) => Some(Timeline::thread(root_id)),
|
|
||||||
|
|
||||||
TimelineKind::Generic(_filter_id) => {
|
TimelineKind::Generic(_filter_id) => {
|
||||||
warn!("you can't convert a TimelineKind::Generic to a Timeline");
|
warn!("you can't convert a TimelineKind::Generic to a Timeline");
|
||||||
// TODO: you actually can! just need to look up the filter id
|
// TODO: you actually can! just need to look up the filter id
|
||||||
@@ -609,7 +577,6 @@ impl TimelineKind {
|
|||||||
},
|
},
|
||||||
TimelineKind::Notifications(_pubkey_source) => ColumnTitle::simple("Notifications"),
|
TimelineKind::Notifications(_pubkey_source) => ColumnTitle::simple("Notifications"),
|
||||||
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
|
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
|
||||||
TimelineKind::Thread(_root_id) => ColumnTitle::simple("Thread"),
|
|
||||||
TimelineKind::Universe => ColumnTitle::simple("Universe"),
|
TimelineKind::Universe => ColumnTitle::simple("Universe"),
|
||||||
TimelineKind::Generic(_) => ColumnTitle::simple("Custom"),
|
TimelineKind::Generic(_) => ColumnTitle::simple("Custom"),
|
||||||
TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()),
|
TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()),
|
||||||
|
|||||||
@@ -214,24 +214,6 @@ impl Timeline {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn thread(selection: ThreadSelection) -> Self {
|
|
||||||
let filter = vec![
|
|
||||||
nostrdb::Filter::new()
|
|
||||||
.kinds([1])
|
|
||||||
.event(selection.root_id.bytes())
|
|
||||||
.build(),
|
|
||||||
nostrdb::Filter::new()
|
|
||||||
.ids([selection.root_id.bytes()])
|
|
||||||
.limit(1)
|
|
||||||
.build(),
|
|
||||||
];
|
|
||||||
Timeline::new(
|
|
||||||
TimelineKind::Thread(selection),
|
|
||||||
FilterState::ready(filter),
|
|
||||||
TimelineTab::only_notes_and_replies(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn last_per_pubkey(list: &Note, list_kind: &ListKind) -> Result<Self> {
|
pub fn last_per_pubkey(list: &Note, list_kind: &ListKind) -> Result<Self> {
|
||||||
let kind = 1;
|
let kind = 1;
|
||||||
let notes_per_pk = 1;
|
let notes_per_pk = 1;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
nav::RenderNavAction,
|
nav::RenderNavAction,
|
||||||
profile::ProfileAction,
|
profile::ProfileAction,
|
||||||
timeline::{TimelineCache, TimelineKind},
|
timeline::{thread::Threads, ThreadSelection, TimelineCache, TimelineKind},
|
||||||
ui::{self, ProfileView},
|
ui::{self, ProfileView},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ pub fn render_timeline_route(
|
|||||||
accounts: &mut Accounts,
|
accounts: &mut Accounts,
|
||||||
kind: &TimelineKind,
|
kind: &TimelineKind,
|
||||||
col: usize,
|
col: usize,
|
||||||
mut note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
note_context: &mut NoteContext,
|
note_context: &mut NoteContext,
|
||||||
@@ -74,32 +74,40 @@ pub fn render_timeline_route(
|
|||||||
note_action.map(RenderNavAction::NoteAction)
|
note_action.map(RenderNavAction::NoteAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineKind::Thread(id) => {
|
|
||||||
// don't truncate thread notes for now, since they are
|
|
||||||
// default truncated everywher eelse
|
|
||||||
note_options.set_truncate(false);
|
|
||||||
|
|
||||||
// text is selectable in threads
|
|
||||||
note_options.set_selectable_text(true);
|
|
||||||
|
|
||||||
ui::ThreadView::new(
|
|
||||||
timeline_cache,
|
|
||||||
unknown_ids,
|
|
||||||
id.selected_or_root(),
|
|
||||||
note_options,
|
|
||||||
&accounts.mutefun(),
|
|
||||||
note_context,
|
|
||||||
&accounts.get_selected_account().map(|a| (&a.key).into()),
|
|
||||||
jobs,
|
|
||||||
)
|
|
||||||
.id_source(egui::Id::new(("threadscroll", col)))
|
|
||||||
.ui(ui)
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn render_thread_route(
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
|
threads: &mut Threads,
|
||||||
|
accounts: &mut Accounts,
|
||||||
|
selection: &ThreadSelection,
|
||||||
|
col: usize,
|
||||||
|
mut note_options: NoteOptions,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
note_context: &mut NoteContext,
|
||||||
|
jobs: &mut JobsCache,
|
||||||
|
) -> Option<RenderNavAction> {
|
||||||
|
// don't truncate thread notes for now, since they are
|
||||||
|
// default truncated everywher eelse
|
||||||
|
note_options.set_truncate(false);
|
||||||
|
|
||||||
|
ui::ThreadView::new(
|
||||||
|
threads,
|
||||||
|
unknown_ids,
|
||||||
|
selection.selected_or_root(),
|
||||||
|
note_options,
|
||||||
|
&accounts.mutefun(),
|
||||||
|
note_context,
|
||||||
|
&accounts.get_selected_account().map(|a| (&a.key).into()),
|
||||||
|
jobs,
|
||||||
|
)
|
||||||
|
.id_source(col)
|
||||||
|
.ui(ui)
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_profile_route(
|
pub fn render_profile_route(
|
||||||
pubkey: &Pubkey,
|
pubkey: &Pubkey,
|
||||||
@@ -139,30 +147,3 @@ pub fn render_profile_route(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use enostr::NoteId;
|
|
||||||
use tokenator::{TokenParser, TokenWriter};
|
|
||||||
|
|
||||||
use crate::timeline::{ThreadSelection, TimelineKind};
|
|
||||||
use enostr::Pubkey;
|
|
||||||
use notedeck::RootNoteIdBuf;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_timeline_route_serialize() {
|
|
||||||
let note_id_hex = "1c54e5b0c386425f7e017d9e068ddef8962eb2ce1bb08ed27e24b93411c12e60";
|
|
||||||
let note_id = NoteId::from_hex(note_id_hex).unwrap();
|
|
||||||
let data_str = format!("thread:{}", note_id_hex);
|
|
||||||
let data = &data_str.split(":").collect::<Vec<&str>>();
|
|
||||||
let mut token_writer = TokenWriter::default();
|
|
||||||
let mut parser = TokenParser::new(&data);
|
|
||||||
let parsed = TimelineKind::parse(&mut parser, &Pubkey::new(*note_id.bytes())).unwrap();
|
|
||||||
let expected = TimelineKind::Thread(ThreadSelection::from_root_id(
|
|
||||||
RootNoteIdBuf::new_unsafe(*note_id.bytes()),
|
|
||||||
));
|
|
||||||
parsed.serialize_tokens(&mut token_writer);
|
|
||||||
assert_eq!(expected, parsed);
|
|
||||||
assert_eq!(token_writer.str(), data_str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::column::ColumnsAction;
|
use crate::column::ColumnsAction;
|
||||||
use crate::nav::RenderNavAction;
|
use crate::nav::RenderNavAction;
|
||||||
use crate::nav::SwitchingAction;
|
use crate::nav::SwitchingAction;
|
||||||
|
use crate::timeline::ThreadSelection;
|
||||||
use crate::{
|
use crate::{
|
||||||
column::Columns,
|
column::Columns,
|
||||||
route::Route,
|
route::Route,
|
||||||
@@ -437,11 +438,6 @@ impl<'a> NavTitle<'a> {
|
|||||||
|
|
||||||
TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
|
TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
|
||||||
|
|
||||||
TimelineKind::Thread(_) => {
|
|
||||||
// no pfp for threads
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
TimelineKind::Search(_sq) => {
|
TimelineKind::Search(_sq) => {
|
||||||
// TODO: show author pfp if author field set?
|
// TODO: show author pfp if author field set?
|
||||||
|
|
||||||
@@ -467,6 +463,9 @@ impl<'a> NavTitle<'a> {
|
|||||||
Route::Search => Some(ui.add(ui::side_panel::search_button())),
|
Route::Search => Some(ui.add(ui::side_panel::search_button())),
|
||||||
Route::Wallet(_) => None,
|
Route::Wallet(_) => None,
|
||||||
Route::CustomizeZapAmount(_) => None,
|
Route::CustomizeZapAmount(_) => None,
|
||||||
|
Route::Thread(thread_selection) => {
|
||||||
|
Some(self.thread_pfp(ui, thread_selection, pfp_size))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,6 +487,23 @@ impl<'a> NavTitle<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thread_pfp(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
selection: &ThreadSelection,
|
||||||
|
pfp_size: f32,
|
||||||
|
) -> egui::Response {
|
||||||
|
let txn = Transaction::new(self.ndb).unwrap();
|
||||||
|
|
||||||
|
if let Ok(note) = self.ndb.get_note_by_id(&txn, selection.selected_or_root()) {
|
||||||
|
if let Some(mut pfp) = self.pubkey_pfp(&txn, note.pubkey(), pfp_size) {
|
||||||
|
return ui.add(&mut pfp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add(&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()).size(pfp_size))
|
||||||
|
}
|
||||||
|
|
||||||
fn title_label_value(title: &str) -> egui::Label {
|
fn title_label_value(title: &str) -> egui::Label {
|
||||||
egui::Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style()))
|
egui::Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style()))
|
||||||
.selectable(false)
|
.selectable(false)
|
||||||
|
|||||||
@@ -3,21 +3,19 @@ use egui_virtual_list::VirtualList;
|
|||||||
use enostr::KeypairUnowned;
|
use enostr::KeypairUnowned;
|
||||||
use nostrdb::{Note, Transaction};
|
use nostrdb::{Note, Transaction};
|
||||||
use notedeck::note::root_note_id_from_selected_id;
|
use notedeck::note::root_note_id_from_selected_id;
|
||||||
use notedeck::{MuteFun, NoteAction, NoteContext, RootNoteId, UnknownIds};
|
use notedeck::{MuteFun, NoteAction, NoteContext, UnknownIds};
|
||||||
use notedeck_ui::jobs::JobsCache;
|
use notedeck_ui::jobs::JobsCache;
|
||||||
use notedeck_ui::note::NoteResponse;
|
use notedeck_ui::note::NoteResponse;
|
||||||
use notedeck_ui::{NoteOptions, NoteView};
|
use notedeck_ui::{NoteOptions, NoteView};
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::timeline::thread::NoteSeenFlags;
|
use crate::timeline::thread::{NoteSeenFlags, ParentState, Threads};
|
||||||
use crate::timeline::{ThreadSelection, TimelineCache, TimelineKind};
|
|
||||||
use crate::ui::timeline::TimelineTabView;
|
|
||||||
|
|
||||||
pub struct ThreadView<'a, 'd> {
|
pub struct ThreadView<'a, 'd> {
|
||||||
timeline_cache: &'a mut TimelineCache,
|
threads: &'a mut Threads,
|
||||||
unknown_ids: &'a mut UnknownIds,
|
unknown_ids: &'a mut UnknownIds,
|
||||||
selected_note_id: &'a [u8; 32],
|
selected_note_id: &'a [u8; 32],
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
|
col: usize,
|
||||||
id_source: egui::Id,
|
id_source: egui::Id,
|
||||||
is_muted: &'a MuteFun,
|
is_muted: &'a MuteFun,
|
||||||
note_context: &'a mut NoteContext<'d>,
|
note_context: &'a mut NoteContext<'d>,
|
||||||
@@ -28,7 +26,7 @@ pub struct ThreadView<'a, 'd> {
|
|||||||
impl<'a, 'd> ThreadView<'a, 'd> {
|
impl<'a, 'd> ThreadView<'a, 'd> {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
timeline_cache: &'a mut TimelineCache,
|
threads: &'a mut Threads,
|
||||||
unknown_ids: &'a mut UnknownIds,
|
unknown_ids: &'a mut UnknownIds,
|
||||||
selected_note_id: &'a [u8; 32],
|
selected_note_id: &'a [u8; 32],
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
@@ -39,7 +37,7 @@ impl<'a, 'd> ThreadView<'a, 'd> {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let id_source = egui::Id::new("threadscroll_threadview");
|
let id_source = egui::Id::new("threadscroll_threadview");
|
||||||
ThreadView {
|
ThreadView {
|
||||||
timeline_cache,
|
threads,
|
||||||
unknown_ids,
|
unknown_ids,
|
||||||
selected_note_id,
|
selected_note_id,
|
||||||
note_options,
|
note_options,
|
||||||
@@ -48,11 +46,13 @@ impl<'a, 'd> ThreadView<'a, 'd> {
|
|||||||
note_context,
|
note_context,
|
||||||
cur_acc,
|
cur_acc,
|
||||||
jobs,
|
jobs,
|
||||||
|
col: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn id_source(mut self, id: egui::Id) -> Self {
|
pub fn id_source(mut self, col: usize) -> Self {
|
||||||
self.id_source = id;
|
self.col = col;
|
||||||
|
self.id_source = egui::Id::new(("threadscroll", col));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,62 +73,88 @@ impl<'a, 'd> ThreadView<'a, 'd> {
|
|||||||
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = scroll_area.show(ui, |ui| {
|
let output = scroll_area.show(ui, |ui| self.notes(ui, &txn));
|
||||||
let root_id = match RootNoteId::new(
|
|
||||||
self.note_context.ndb,
|
|
||||||
self.note_context.note_cache,
|
|
||||||
&txn,
|
|
||||||
self.selected_note_id,
|
|
||||||
) {
|
|
||||||
Ok(root_id) => root_id,
|
|
||||||
|
|
||||||
Err(err) => {
|
|
||||||
ui.label(format!("Error loading thread: {:?}", err));
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let thread_timeline = self
|
|
||||||
.timeline_cache
|
|
||||||
.notes(
|
|
||||||
self.note_context.ndb,
|
|
||||||
self.note_context.note_cache,
|
|
||||||
&txn,
|
|
||||||
&TimelineKind::Thread(ThreadSelection::from_root_id(root_id.to_owned())),
|
|
||||||
)
|
|
||||||
.get_ptr();
|
|
||||||
|
|
||||||
// TODO(jb55): skip poll if ThreadResult is fresh?
|
|
||||||
|
|
||||||
let reversed = true;
|
|
||||||
// poll for new notes and insert them into our existing notes
|
|
||||||
if let Err(err) = thread_timeline.poll_notes_into_view(
|
|
||||||
self.note_context.ndb,
|
|
||||||
&txn,
|
|
||||||
self.unknown_ids,
|
|
||||||
self.note_context.note_cache,
|
|
||||||
reversed,
|
|
||||||
) {
|
|
||||||
error!("error polling notes into thread timeline: {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
TimelineTabView::new(
|
|
||||||
thread_timeline.current_view(),
|
|
||||||
true,
|
|
||||||
self.note_options,
|
|
||||||
&txn,
|
|
||||||
self.is_muted,
|
|
||||||
self.note_context,
|
|
||||||
self.cur_acc,
|
|
||||||
self.jobs,
|
|
||||||
)
|
|
||||||
.show(ui)
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y));
|
ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y));
|
||||||
|
|
||||||
output.inner
|
output.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn notes(&mut self, ui: &mut egui::Ui, txn: &Transaction) -> Option<NoteAction> {
|
||||||
|
let Ok(cur_note) = self
|
||||||
|
.note_context
|
||||||
|
.ndb
|
||||||
|
.get_note_by_id(txn, self.selected_note_id)
|
||||||
|
else {
|
||||||
|
let id = *self.selected_note_id;
|
||||||
|
tracing::error!("ndb: Did not find note {}", enostr::NoteId::new(id).hex());
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.threads.update(
|
||||||
|
&cur_note,
|
||||||
|
self.note_context.note_cache,
|
||||||
|
self.note_context.ndb,
|
||||||
|
txn,
|
||||||
|
self.unknown_ids,
|
||||||
|
self.col,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cur_node = self.threads.threads.get(&self.selected_note_id).unwrap();
|
||||||
|
|
||||||
|
let full_chain = cur_node.have_all_ancestors;
|
||||||
|
let mut note_builder = ThreadNoteBuilder::new(cur_note);
|
||||||
|
|
||||||
|
let mut parent_state = cur_node.prev.clone();
|
||||||
|
while let ParentState::Parent(id) = parent_state {
|
||||||
|
if let Ok(note) = self.note_context.ndb.get_note_by_id(txn, id.bytes()) {
|
||||||
|
note_builder.add_chain(note);
|
||||||
|
if let Some(res) = self.threads.threads.get(&id.bytes()) {
|
||||||
|
parent_state = res.prev.clone();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent_state = ParentState::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
for note_ref in &cur_node.replies {
|
||||||
|
if let Ok(note) = self.note_context.ndb.get_note_by_key(txn, note_ref.key) {
|
||||||
|
note_builder.add_reply(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let list = &mut self
|
||||||
|
.threads
|
||||||
|
.threads
|
||||||
|
.get_mut(&self.selected_note_id)
|
||||||
|
.unwrap()
|
||||||
|
.list;
|
||||||
|
|
||||||
|
let notes = note_builder.into_notes(&mut self.threads.seen_flags);
|
||||||
|
|
||||||
|
if !full_chain {
|
||||||
|
// TODO(kernelkind): insert UI denoting we don't have the full chain yet
|
||||||
|
ui.colored_label(ui.visuals().error_fg_color, "LOADING NOTES");
|
||||||
|
}
|
||||||
|
|
||||||
|
let zapping_acc = self
|
||||||
|
.cur_acc
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| self.note_context.current_account_has_wallet)
|
||||||
|
.or(self.cur_acc.as_ref());
|
||||||
|
|
||||||
|
show_notes(
|
||||||
|
ui,
|
||||||
|
list,
|
||||||
|
¬es,
|
||||||
|
self.note_context,
|
||||||
|
zapping_acc,
|
||||||
|
self.note_options,
|
||||||
|
self.jobs,
|
||||||
|
txn,
|
||||||
|
self.is_muted,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
|||||||
Reference in New Issue
Block a user