post quote reposts impl

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind
2024-09-17 12:20:29 -04:00
parent 8e32f757f0
commit de9e0e4ca1
12 changed files with 275 additions and 20 deletions

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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 {

View File

@@ -89,4 +89,29 @@ 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()
);
let builder = NoteBuilder::new().kind(1).content(&new_content);
let builder = builder
.start_tag()
.tag_str("q")
.tag_str(&hex::encode(quoting.id()))
.sign(seckey);
let builder = builder
.start_tag()
.tag_str("p")
.tag_str(&hex::encode(quoting.pubkey()));
builder
.sign(seckey)
.build()
.expect("expected build to work")
}
}

View File

@@ -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"),

View File

@@ -6,7 +6,10 @@ use crate::{
notecache::NoteCache,
thread::Threads,
timeline::TimelineId,
ui::{self, note::post::PostResponse},
ui::{
self,
note::{post::PostResponse, QuoteRepostView},
},
};
use enostr::{NoteId, RelayPool};
@@ -17,6 +20,7 @@ pub enum TimelineRoute {
Timeline(TimelineId),
Thread(NoteId),
Reply(NoteId),
Quote(NoteId),
}
pub enum TimelineRouteResponse {
@@ -49,7 +53,7 @@ pub fn render_timeline_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);
ui::timeline::postbox_view(ndb, kp, pool, drafts, img_cache, note_cache, ui);
}
}
@@ -109,5 +113,34 @@ pub fn render_timeline_route(
None
}
}
TimelineRoute::Quote(id) => {
let txn = if let Ok(txn) = Transaction::new(ndb) {
txn
} else {
ui.label("Quote of unknown note");
return None;
};
let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) {
note
} else {
ui.label("Quote of unknown note");
return None;
};
let id = egui::Id::new(("post", col, note.key().unwrap()));
if let Some(poster) = accounts.selected_or_first_nsec() {
let response = egui::ScrollArea::vertical().show(ui, |ui| {
QuoteRepostView::new(ndb, poster, pool, note_cache, img_cache, drafts, &note)
.id_source(id)
.show(ui)
});
Some(TimelineRouteResponse::post(response.inner))
} else {
None
}
}
}
}

View File

@@ -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,

View File

@@ -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,28 @@ 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 id = ui.id().with(("quote-anim", note_key));
let size = 8.0;
let expand_size = 5.0;
let anim_speed = 0.05;
let (rect, size, resp) = ui::anim::hover_expand(ui, id, size, expand_size, anim_speed);
let color = if ui.style().visuals.dark_mode {
egui::Color32::WHITE
} else {
egui::Color32::BLACK
};
ui.painter_at(rect).text(
rect.center(),
egui::Align2::CENTER_CENTER,
"Q",
egui::FontId::proportional(size + 2.0),
color,
);
resp
}

View File

@@ -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,9 @@ mod preview {
PostPreview {
ndb,
img_cache: ImageCache::new(".".into()),
note_cache: NoteCache {
cache: Default::default(),
},
draft: Draft::new(),
poster: FullKeypair::generate(),
}
@@ -184,7 +221,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);

View File

@@ -0,0 +1,88 @@
use enostr::{FilledKeypair, RelayPool};
use nostrdb::Ndb;
use tracing::info;
use crate::{draft::Drafts, imgcache::ImageCache, notecache::NoteCache, ui};
use super::{PostAction, PostResponse};
pub struct QuoteRepostView<'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,
quoting_note: &'a nostrdb::Note<'a>,
id_source: Option<egui::Id>,
}
impl<'a> QuoteRepostView<'a> {
pub fn new(
ndb: &'a Ndb,
poster: FilledKeypair<'a>,
pool: &'a mut RelayPool,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
drafts: &'a mut Drafts,
quoting_note: &'a nostrdb::Note<'a>,
) -> Self {
let id_source: Option<egui::Id> = None;
QuoteRepostView {
ndb,
poster,
pool,
note_cache,
img_cache,
drafts,
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();
let post_response = {
let draft = self.drafts.quote_mut(quoting_note_id);
ui::PostView::new(
self.ndb,
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)
};
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_quote(&seckey, self.quoting_note);
let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
info!("sending {}", raw_msg);
self.pool.send(&enostr::ClientMessage::raw(raw_msg));
self.drafts.quote_mut(quoting_note_id).clear();
}
}
}
post_response
}
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"))
}
}

View File

@@ -80,9 +80,16 @@ impl<'a> PostReplyView<'a> {
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,
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 {

View File

@@ -173,11 +173,20 @@ pub fn postbox_view<'a>(
pool: &'a mut RelayPool,
drafts: &'a mut Drafts,
img_cache: &'a mut ImageCache,
note_cache: &'a mut NoteCache,
ui: &'a mut egui::Ui,
) {
// 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);
let response = ui::PostView::new(
ndb,
drafts.compose_mut(),
crate::draft::DraftSource::Compose,
img_cache,
note_cache,
key,
)
.ui(&txn, ui);
if let Some(action) = response.action {
match action {