commit 2208e68726f723944d01cb17b2e1fa5608728ad7
parent 8e32f757f0d36758fd5919ea2f2367aa1287fd96
Author: William Casarin <jb55@jb55.com>
Date: Wed, 18 Sep 2024 14:15:36 -0700
Merge 'Initial quote reposts #305'
kernelkind (6):
post quote reposts impl
make PostActionExecutor for code reuse
add repost button
address PR comments
make views pure
minor cleanup
Diffstat:
14 files changed, 296 insertions(+), 74 deletions(-)
diff --git a/enostr/src/note.rs b/enostr/src/note.rs
@@ -6,6 +6,8 @@ use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct NoteId([u8; 32]);
+static HRP_NOTE: nostr::bech32::Hrp = nostr::bech32::Hrp::parse_unchecked("note");
+
impl NoteId {
pub fn new(bytes: [u8; 32]) -> Self {
NoteId(bytes)
@@ -23,6 +25,10 @@ impl NoteId {
let evid = NoteId(hex::decode(hex_str)?.as_slice().try_into().unwrap());
Ok(evid)
}
+
+ pub fn to_bech(&self) -> Option<String> {
+ nostr::bech32::encode::<nostr::bech32::Bech32>(HRP_NOTE, &self.0).ok()
+ }
}
/// Event is the struct used to represent a Nostr event
diff --git a/src/actionbar.rs b/src/actionbar.rs
@@ -12,6 +12,7 @@ use uuid::Uuid;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum BarAction {
Reply(NoteId),
+ Quote(NoteId),
OpenThread(NoteId),
}
@@ -130,6 +131,12 @@ impl BarAction {
BarAction::OpenThread(note_id) => {
open_thread(ndb, txn, router, note_cache, pool, threads, note_id.bytes())
}
+
+ BarAction::Quote(note_id) => {
+ router.route_to(Route::quote(note_id));
+ router.navigating = true;
+ None
+ }
}
}
diff --git a/src/draft.rs b/src/draft.rs
@@ -8,6 +8,7 @@ pub struct Draft {
#[derive(Default)]
pub struct Drafts {
replies: HashMap<[u8; 32], Draft>,
+ quotes: HashMap<[u8; 32], Draft>,
compose: Draft,
}
@@ -19,14 +20,19 @@ impl Drafts {
pub fn reply_mut(&mut self, id: &[u8; 32]) -> &mut Draft {
self.replies.entry(*id).or_default()
}
+
+ pub fn quote_mut(&mut self, id: &[u8; 32]) -> &mut Draft {
+ self.quotes.entry(*id).or_default()
+ }
}
-/*
pub enum DraftSource<'a> {
Compose,
Reply(&'a [u8; 32]), // note id
+ Quote(&'a [u8; 32]), // note id
}
+/*
impl<'a> DraftSource<'a> {
pub fn draft(&self, drafts: &'a mut Drafts) -> &'a mut Draft {
match self {
diff --git a/src/lib.rs b/src/lib.rs
@@ -25,6 +25,7 @@ mod nav;
mod note;
mod notecache;
mod post;
+mod post_action_executor;
mod profile;
pub mod relay_pool_manager;
mod result;
diff --git a/src/post.rs b/src/post.rs
@@ -89,4 +89,25 @@ impl NewPost {
.build()
.expect("expected build to work")
}
+
+ pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note {
+ let new_content = format!(
+ "{}\nnostr:{}",
+ self.content,
+ enostr::NoteId::new(*quoting.id()).to_bech().unwrap()
+ );
+
+ NoteBuilder::new()
+ .kind(1)
+ .content(&new_content)
+ .start_tag()
+ .tag_str("q")
+ .tag_str(&hex::encode(quoting.id()))
+ .start_tag()
+ .tag_str("p")
+ .tag_str(&hex::encode(quoting.pubkey()))
+ .sign(seckey)
+ .build()
+ .expect("expected build to work")
+ }
}
diff --git a/src/post_action_executor.rs b/src/post_action_executor.rs
@@ -0,0 +1,28 @@
+use enostr::{FilledKeypair, RelayPool};
+use nostrdb::Note;
+use tracing::info;
+
+use crate::{draft::Draft, post::NewPost, ui::note::PostAction};
+
+pub struct PostActionExecutor {}
+
+impl PostActionExecutor {
+ pub fn execute<'a>(
+ poster: FilledKeypair<'_>,
+ action: &'a PostAction,
+ pool: &mut RelayPool,
+ draft: &mut Draft,
+ get_note: impl Fn(&'a NewPost, &[u8; 32]) -> Note<'a>,
+ ) {
+ match action {
+ PostAction::Post(np) => {
+ let note = get_note(np, &poster.secret_key.to_secret_bytes());
+
+ let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
+ info!("sending {}", raw_msg);
+ pool.send(&enostr::ClientMessage::raw(raw_msg));
+ draft.clear();
+ }
+ }
+ }
+}
diff --git a/src/route.rs b/src/route.rs
@@ -39,6 +39,10 @@ impl Route {
Route::Timeline(TimelineRoute::Reply(replying_to))
}
+ pub fn quote(quoting: NoteId) -> Self {
+ Route::Timeline(TimelineRoute::Quote(quoting))
+ }
+
pub fn accounts() -> Self {
Route::Accounts(AccountsRoute::Accounts)
}
@@ -110,6 +114,7 @@ impl fmt::Display for Route {
TimelineRoute::Timeline(name) => write!(f, "{}", name),
TimelineRoute::Thread(_id) => write!(f, "Thread"),
TimelineRoute::Reply(_id) => write!(f, "Reply"),
+ TimelineRoute::Quote(_id) => write!(f, "Quote"),
},
Route::Relays => write!(f, "Relays"),
diff --git a/src/timeline/route.rs b/src/timeline/route.rs
@@ -4,9 +4,13 @@ use crate::{
draft::Drafts,
imgcache::ImageCache,
notecache::NoteCache,
+ post_action_executor::PostActionExecutor,
thread::Threads,
timeline::TimelineId,
- ui::{self, note::post::PostResponse},
+ ui::{
+ self,
+ note::{post::PostResponse, QuoteRepostView},
+ },
};
use enostr::{NoteId, RelayPool};
@@ -17,6 +21,7 @@ pub enum TimelineRoute {
Timeline(TimelineId),
Thread(NoteId),
Reply(NoteId),
+ Quote(NoteId),
}
pub enum TimelineRouteResponse {
@@ -48,8 +53,15 @@ pub fn render_timeline_route(
match route {
TimelineRoute::Timeline(timeline_id) => {
if show_postbox {
- if let Some(kp) = accounts.selected_or_first_nsec() {
- ui::timeline::postbox_view(ndb, kp, pool, drafts, img_cache, ui);
+ let kp = accounts.selected_or_first_nsec()?;
+ let draft = drafts.compose_mut();
+ let response =
+ ui::timeline::postbox_view(ndb, kp, draft, img_cache, note_cache, ui);
+
+ if let Some(action) = response.action {
+ PostActionExecutor::execute(kp, &action, pool, draft, |np, seckey| {
+ np.to_note(seckey)
+ });
}
}
@@ -96,18 +108,51 @@ pub fn render_timeline_route(
};
let id = egui::Id::new(("post", col, note.key().unwrap()));
+ let poster = accounts.selected_or_first_nsec()?;
+ let draft = drafts.reply_mut(note.id());
+
+ let response = egui::ScrollArea::vertical().show(ui, |ui| {
+ ui::PostReplyView::new(ndb, poster, draft, note_cache, img_cache, ¬e)
+ .id_source(id)
+ .show(ui)
+ });
- if let Some(poster) = accounts.selected_or_first_nsec() {
- let response = egui::ScrollArea::vertical().show(ui, |ui| {
- ui::PostReplyView::new(ndb, poster, pool, drafts, note_cache, img_cache, ¬e)
- .id_source(id)
- .show(ui)
+ if let Some(action) = &response.inner.action {
+ PostActionExecutor::execute(poster, action, pool, draft, |np, seckey| {
+ np.to_reply(seckey, ¬e)
});
+ }
+
+ Some(TimelineRouteResponse::post(response.inner))
+ }
+
+ TimelineRoute::Quote(id) => {
+ let txn = Transaction::new(ndb).expect("txn");
- Some(TimelineRouteResponse::post(response.inner))
+ let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) {
+ note
} else {
- None
+ ui.label("Quote of unknown note");
+ return None;
+ };
+
+ let id = egui::Id::new(("post", col, note.key().unwrap()));
+
+ let poster = accounts.selected_or_first_nsec()?;
+ let draft = drafts.quote_mut(note.id());
+
+ let response = egui::ScrollArea::vertical().show(ui, |ui| {
+ QuoteRepostView::new(ndb, poster, note_cache, img_cache, draft, ¬e)
+ .id_source(id)
+ .show(ui)
+ });
+
+ if let Some(action) = &response.inner.action {
+ PostActionExecutor::execute(poster, action, pool, draft, |np, seckey| {
+ np.to_quote(seckey, ¬e)
+ });
}
+ Some(TimelineRouteResponse::post(response.inner))
}
}
}
diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs
@@ -58,7 +58,7 @@ impl egui::Widget for NoteContents<'_> {
/// Render an inline note preview with a border. These are used when
/// notes are references within a note
-fn render_note_preview(
+pub fn render_note_preview(
ui: &mut egui::Ui,
ndb: &Ndb,
note_cache: &mut NoteCache,
diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs
@@ -1,11 +1,13 @@
pub mod contents;
pub mod options;
pub mod post;
+pub mod quote_repost;
pub mod reply;
pub use contents::NoteContents;
pub use options::NoteOptions;
pub use post::{PostAction, PostResponse, PostView};
+pub use quote_repost::QuoteRepostView;
pub use reply::PostReplyView;
use crate::{
@@ -555,9 +557,12 @@ fn render_note_actionbar(
) -> egui::InnerResponse<Option<BarAction>> {
ui.horizontal(|ui| {
let reply_resp = reply_button(ui, note_key);
+ let quote_resp = quote_repost_button(ui, note_key);
if reply_resp.clicked() {
Some(BarAction::Reply(NoteId::new(*note_id)))
+ } else if quote_resp.clicked() {
+ Some(BarAction::Quote(NoteId::new(*note_id)))
} else {
None
}
@@ -614,3 +619,15 @@ fn repost_icon() -> egui::Image<'static> {
let img_data = egui::include_image!("../../../assets/icons/repost_icon_4x.png");
egui::Image::new(img_data)
}
+
+fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
+ let (rect, size, resp) =
+ ui::anim::hover_expand_small(ui, ui.id().with(("repost_anim", note_key)));
+
+ let expand_size = 5.0;
+ let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
+
+ let put_resp = ui.put(rect, repost_icon().max_width(size));
+
+ resp.union(put_resp)
+}
diff --git a/src/ui/note/post.rs b/src/ui/note/post.rs
@@ -1,16 +1,22 @@
-use crate::draft::Draft;
+use crate::draft::{Draft, DraftSource};
use crate::imgcache::ImageCache;
+use crate::notecache::NoteCache;
use crate::post::NewPost;
use crate::ui;
use crate::ui::{Preview, PreviewConfig, View};
use egui::widgets::text_edit::TextEdit;
+use egui::{Frame, Layout};
use enostr::{FilledKeypair, FullKeypair};
use nostrdb::{Config, Ndb, Transaction};
+use super::contents::render_note_preview;
+
pub struct PostView<'a> {
ndb: &'a Ndb,
draft: &'a mut Draft,
+ draft_source: DraftSource<'a>,
img_cache: &'a mut ImageCache,
+ note_cache: &'a mut NoteCache,
poster: FilledKeypair<'a>,
id_source: Option<egui::Id>,
}
@@ -28,7 +34,9 @@ impl<'a> PostView<'a> {
pub fn new(
ndb: &'a Ndb,
draft: &'a mut Draft,
+ draft_source: DraftSource<'a>,
img_cache: &'a mut ImageCache,
+ note_cache: &'a mut NoteCache,
poster: FilledKeypair<'a>,
) -> Self {
let id_source: Option<egui::Id> = None;
@@ -36,8 +44,10 @@ impl<'a> PostView<'a> {
ndb,
draft,
img_cache,
+ note_cache,
poster,
id_source,
+ draft_source,
}
}
@@ -129,18 +139,41 @@ impl<'a> PostView<'a> {
let edit_response = ui.horizontal(|ui| self.editbox(txn, ui)).inner;
let action = ui
- .with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
- if ui
- .add_sized([91.0, 32.0], egui::Button::new("Post now"))
- .clicked()
- {
- Some(PostAction::Post(NewPost::new(
- self.draft.buffer.clone(),
- self.poster.to_full(),
- )))
- } else {
- None
+ .horizontal(|ui| {
+ if let DraftSource::Quote(id) = self.draft_source {
+ let avail_size = ui.available_size_before_wrap();
+ ui.with_layout(Layout::left_to_right(egui::Align::TOP), |ui| {
+ Frame::none().show(ui, |ui| {
+ ui.vertical(|ui| {
+ ui.set_max_width(avail_size.x * 0.8);
+ render_note_preview(
+ ui,
+ self.ndb,
+ self.note_cache,
+ self.img_cache,
+ txn,
+ id,
+ "",
+ );
+ });
+ });
+ });
}
+
+ ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| {
+ if ui
+ .add_sized([91.0, 32.0], egui::Button::new("Post now"))
+ .clicked()
+ {
+ Some(PostAction::Post(NewPost::new(
+ self.draft.buffer.clone(),
+ self.poster.to_full(),
+ )))
+ } else {
+ None
+ }
+ })
+ .inner
})
.inner;
@@ -161,6 +194,7 @@ mod preview {
pub struct PostPreview {
ndb: Ndb,
img_cache: ImageCache,
+ note_cache: NoteCache,
draft: Draft,
poster: FullKeypair,
}
@@ -172,6 +206,7 @@ mod preview {
PostPreview {
ndb,
img_cache: ImageCache::new(".".into()),
+ note_cache: NoteCache::default(),
draft: Draft::new(),
poster: FullKeypair::generate(),
}
@@ -184,7 +219,9 @@ mod preview {
PostView::new(
&self.ndb,
&mut self.draft,
+ DraftSource::Compose,
&mut self.img_cache,
+ &mut self.note_cache,
self.poster.to_filled(),
)
.ui(&txn, ui);
diff --git a/src/ui/note/quote_repost.rs b/src/ui/note/quote_repost.rs
@@ -0,0 +1,64 @@
+use enostr::FilledKeypair;
+use nostrdb::Ndb;
+
+use crate::{draft::Draft, imgcache::ImageCache, notecache::NoteCache, ui};
+
+use super::PostResponse;
+
+pub struct QuoteRepostView<'a> {
+ ndb: &'a Ndb,
+ poster: FilledKeypair<'a>,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ draft: &'a mut Draft,
+ quoting_note: &'a nostrdb::Note<'a>,
+ id_source: Option<egui::Id>,
+}
+
+impl<'a> QuoteRepostView<'a> {
+ pub fn new(
+ ndb: &'a Ndb,
+ poster: FilledKeypair<'a>,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ draft: &'a mut Draft,
+ quoting_note: &'a nostrdb::Note<'a>,
+ ) -> Self {
+ let id_source: Option<egui::Id> = None;
+ QuoteRepostView {
+ ndb,
+ poster,
+ note_cache,
+ img_cache,
+ draft,
+ quoting_note,
+ id_source,
+ }
+ }
+
+ pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
+ let id = self.id();
+ let quoting_note_id = self.quoting_note.id();
+
+ ui::PostView::new(
+ self.ndb,
+ self.draft,
+ crate::draft::DraftSource::Quote(quoting_note_id),
+ self.img_cache,
+ self.note_cache,
+ self.poster,
+ )
+ .id_source(id)
+ .ui(self.quoting_note.txn().unwrap(), ui)
+ }
+
+ pub fn id_source(mut self, id: egui::Id) -> Self {
+ self.id_source = Some(id);
+ self
+ }
+
+ pub fn id(&self) -> egui::Id {
+ self.id_source
+ .unwrap_or_else(|| egui::Id::new("quote-repost-view"))
+ }
+}
diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs
@@ -1,19 +1,17 @@
-use crate::draft::Drafts;
+use crate::draft::Draft;
use crate::imgcache::ImageCache;
use crate::notecache::NoteCache;
use crate::ui;
-use crate::ui::note::{PostAction, PostResponse};
-use enostr::{FilledKeypair, RelayPool};
+use crate::ui::note::PostResponse;
+use enostr::FilledKeypair;
use nostrdb::Ndb;
-use tracing::info;
pub struct PostReplyView<'a> {
ndb: &'a Ndb,
poster: FilledKeypair<'a>,
- pool: &'a mut RelayPool,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
- drafts: &'a mut Drafts,
+ draft: &'a mut Draft,
note: &'a nostrdb::Note<'a>,
id_source: Option<egui::Id>,
}
@@ -22,8 +20,7 @@ impl<'a> PostReplyView<'a> {
pub fn new(
ndb: &'a Ndb,
poster: FilledKeypair<'a>,
- pool: &'a mut RelayPool,
- drafts: &'a mut Drafts,
+ draft: &'a mut Draft,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
note: &'a nostrdb::Note<'a>,
@@ -32,8 +29,7 @@ impl<'a> PostReplyView<'a> {
PostReplyView {
ndb,
poster,
- pool,
- drafts,
+ draft,
note,
note_cache,
img_cache,
@@ -79,27 +75,18 @@ impl<'a> PostReplyView<'a> {
let rect_before_post = ui.min_rect();
let post_response = {
- let draft = self.drafts.reply_mut(replying_to);
- ui::PostView::new(self.ndb, draft, self.img_cache, self.poster)
- .id_source(id)
- .ui(self.note.txn().unwrap(), ui)
+ ui::PostView::new(
+ self.ndb,
+ self.draft,
+ crate::draft::DraftSource::Reply(replying_to),
+ self.img_cache,
+ self.note_cache,
+ self.poster,
+ )
+ .id_source(id)
+ .ui(self.note.txn().unwrap(), ui)
};
- if let Some(action) = &post_response.action {
- match action {
- PostAction::Post(np) => {
- let seckey = self.poster.secret_key.to_secret_bytes();
-
- let note = np.to_reply(&seckey, self.note);
-
- let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
- info!("sending {}", raw_msg);
- self.pool.send(&enostr::ClientMessage::raw(raw_msg));
- self.drafts.reply_mut(replying_to).clear();
- }
- }
- }
-
//
// reply line
//
diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs
@@ -1,13 +1,16 @@
+use crate::draft::Draft;
use crate::{
- actionbar::BarAction, column::Columns, draft::Drafts, imgcache::ImageCache,
- notecache::NoteCache, timeline::TimelineId, ui, ui::note::PostAction,
+ actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache,
+ timeline::TimelineId, ui,
};
use egui::containers::scroll_area::ScrollBarVisibility;
use egui::{Direction, Layout};
use egui_tabs::TabColor;
-use enostr::{FilledKeypair, RelayPool};
+use enostr::FilledKeypair;
use nostrdb::{Ndb, Transaction};
-use tracing::{debug, error, info, warn};
+use tracing::{debug, error, warn};
+
+use super::note::PostResponse;
pub struct TimelineView<'a> {
timeline_id: TimelineId,
@@ -170,27 +173,22 @@ fn timeline_ui(
pub fn postbox_view<'a>(
ndb: &'a Ndb,
key: FilledKeypair<'a>,
- pool: &'a mut RelayPool,
- drafts: &'a mut Drafts,
+ draft: &'a mut Draft,
img_cache: &'a mut ImageCache,
+ note_cache: &'a mut NoteCache,
ui: &'a mut egui::Ui,
-) {
+) -> PostResponse {
// show a postbox in the first timeline
let txn = Transaction::new(ndb).expect("txn");
- let response = ui::PostView::new(ndb, drafts.compose_mut(), img_cache, key).ui(&txn, ui);
-
- if let Some(action) = response.action {
- match action {
- PostAction::Post(np) => {
- let seckey = key.secret_key.to_secret_bytes();
- let note = np.to_note(&seckey);
- let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
- info!("sending {}", raw_msg);
- pool.send(&enostr::ClientMessage::raw(raw_msg));
- drafts.compose_mut().clear();
- }
- }
- }
+ ui::PostView::new(
+ ndb,
+ draft,
+ crate::draft::DraftSource::Compose,
+ img_cache,
+ note_cache,
+ key,
+ )
+ .ui(&txn, ui)
}
fn tabs_ui(ui: &mut egui::Ui) -> i32 {