mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-17 08:44:20 +01:00
Merge send reactions by kernel #1170
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
assets/icons/like_icon_filled_4x.png
Normal file
BIN
assets/icons/like_icon_filled_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -14,6 +14,9 @@ pub enum NoteAction {
|
||||
/// User has clicked the quote reply action
|
||||
Reply(NoteId),
|
||||
|
||||
/// User has clicked the like/reaction button
|
||||
React(ReactAction),
|
||||
|
||||
/// User has clicked the repost button
|
||||
Repost(NoteId),
|
||||
|
||||
@@ -53,6 +56,18 @@ impl NoteAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReactAction {
|
||||
pub note_id: NoteId,
|
||||
pub content: &'static str,
|
||||
}
|
||||
|
||||
impl ReactAction {
|
||||
pub const fn new(note_id: NoteId, content: &'static str) -> Self {
|
||||
Self { note_id, content }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum ZapAction {
|
||||
Send(ZapTargetAmount),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod action;
|
||||
mod context;
|
||||
|
||||
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||
pub use action::{NoteAction, ReactAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
||||
|
||||
use crate::Accounts;
|
||||
@@ -212,3 +212,9 @@ pub fn event_tag<'a>(ev: &nostrdb::Note<'a>, name: &str) -> Option<&'a str> {
|
||||
tag.get_str(1)
|
||||
})
|
||||
}
|
||||
|
||||
/// Temporary way of checking whether a user has sent a reaction.
|
||||
/// Should be replaced with nostrdb metadata
|
||||
pub fn reaction_sent_id(sender_pk: &enostr::Pubkey, note_reacted_to: &[u8; 32]) -> egui::Id {
|
||||
egui::Id::new(("sent-reaction-id", note_reacted_to, sender_pk))
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ use crate::{
|
||||
};
|
||||
|
||||
use egui_nav::Percent;
|
||||
use enostr::{NoteId, Pubkey, RelayPool};
|
||||
use nostrdb::{Ndb, NoteKey, Transaction};
|
||||
use enostr::{FilledKeypair, NoteId, Pubkey, RelayPool};
|
||||
use nostrdb::{IngestMetadata, Ndb, NoteBuilder, NoteKey, Transaction};
|
||||
use notedeck::{
|
||||
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
|
||||
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
|
||||
get_wallet_for,
|
||||
note::{reaction_sent_id, ReactAction, ZapTargetAmount},
|
||||
Accounts, GlobalWallet, Images, NoteAction, NoteCache, NoteZapTargetOwned, UnknownIds,
|
||||
ZapAction, ZapTarget, ZappingError, Zaps,
|
||||
};
|
||||
use notedeck_ui::media::MediaViewerFlags;
|
||||
use tracing::error;
|
||||
@@ -76,6 +78,21 @@ fn execute_note_action(
|
||||
router_action = Some(RouterAction::route_to(Route::accounts()));
|
||||
}
|
||||
}
|
||||
NoteAction::React(react_action) => {
|
||||
if let Some(filled) = accounts.selected_filled() {
|
||||
if let Err(err) = send_reaction_event(ndb, txn, pool, filled, &react_action) {
|
||||
tracing::error!("Failed to send reaction: {err}");
|
||||
}
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.insert_temp(
|
||||
reaction_sent_id(filled.pubkey, react_action.note_id.bytes()),
|
||||
true,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
router_action = Some(RouterAction::route_to(Route::accounts()));
|
||||
}
|
||||
}
|
||||
NoteAction::Profile(pubkey) => {
|
||||
let kind = TimelineKind::Profile(pubkey);
|
||||
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone())));
|
||||
@@ -262,6 +279,94 @@ pub fn execute_and_process_note_action(
|
||||
resp.router_action
|
||||
}
|
||||
|
||||
fn send_reaction_event(
|
||||
ndb: &mut Ndb,
|
||||
txn: &Transaction,
|
||||
pool: &mut RelayPool,
|
||||
kp: FilledKeypair<'_>,
|
||||
reaction: &ReactAction,
|
||||
) -> Result<(), String> {
|
||||
let Ok(note) = ndb.get_note_by_id(txn, reaction.note_id.bytes()) else {
|
||||
return Err(format!("noteid {:?} not found in ndb", reaction.note_id));
|
||||
};
|
||||
|
||||
let target_pubkey = Pubkey::new(*note.pubkey());
|
||||
let relay_hint: Option<String> = note.relays(txn).next().map(|s| s.to_owned());
|
||||
let target_kind = note.kind();
|
||||
let d_tag_value = find_addressable_d_tag(¬e);
|
||||
|
||||
let mut builder = NoteBuilder::new().kind(7).content(reaction.content);
|
||||
|
||||
builder = builder
|
||||
.start_tag()
|
||||
.tag_str("e")
|
||||
.tag_id(reaction.note_id.bytes())
|
||||
.tag_str(relay_hint.as_deref().unwrap_or(""))
|
||||
.tag_str(&target_pubkey.hex());
|
||||
|
||||
builder = builder
|
||||
.start_tag()
|
||||
.tag_str("p")
|
||||
.tag_id(target_pubkey.bytes());
|
||||
|
||||
if let Some(relay) = relay_hint.as_deref() {
|
||||
builder = builder.tag_str(relay);
|
||||
}
|
||||
|
||||
// we don't support addressable events yet... but why not future proof it?
|
||||
if let Some(d_value) = d_tag_value.as_deref() {
|
||||
let coordinates = format!("{}:{}:{}", target_kind, target_pubkey.hex(), d_value);
|
||||
|
||||
builder = builder.start_tag().tag_str("a").tag_str(&coordinates);
|
||||
|
||||
if let Some(relay) = relay_hint.as_deref() {
|
||||
builder = builder.tag_str(relay);
|
||||
}
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.start_tag()
|
||||
.tag_str("k")
|
||||
.tag_str(&target_kind.to_string());
|
||||
|
||||
let note = builder
|
||||
.sign(&kp.secret_key.secret_bytes())
|
||||
.build()
|
||||
.ok_or_else(|| "failed to build reaction event".to_owned())?;
|
||||
|
||||
let Ok(event) = &enostr::ClientMessage::event(¬e) else {
|
||||
return Err("failed to convert reaction note into client message".to_owned());
|
||||
};
|
||||
|
||||
let Ok(json) = event.to_json() else {
|
||||
return Err("failed to serialize reaction event to json".to_owned());
|
||||
};
|
||||
|
||||
let _ = ndb.process_event_with(&json, IngestMetadata::new().client(true));
|
||||
|
||||
pool.send(event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_addressable_d_tag(note: &nostrdb::Note<'_>) -> Option<String> {
|
||||
for tag in note.tags() {
|
||||
if tag.count() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if tag.get_unchecked(0).variant().str() != Some("d") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = tag.get_unchecked(1).variant().str() {
|
||||
return Some(value.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn send_zap(
|
||||
sender: &Pubkey,
|
||||
zaps: &mut Zaps,
|
||||
|
||||
@@ -7,7 +7,7 @@ use notedeck::fonts::get_font_size;
|
||||
use notedeck::name::get_display_name;
|
||||
use notedeck::ui::is_narrow;
|
||||
use notedeck::{tr_plural, JobsCache, Muted, NotedeckTextStyle};
|
||||
use notedeck_ui::app_images::{like_image, repost_image};
|
||||
use notedeck_ui::app_images::{like_image_filled, repost_image};
|
||||
use notedeck_ui::{ProfilePic, ProfilePreview};
|
||||
use std::f32::consts::PI;
|
||||
use tracing::{error, warn};
|
||||
@@ -514,7 +514,7 @@ enum ReferencedNoteType {
|
||||
impl CompositeType {
|
||||
fn image(&self, darkmode: bool) -> egui::Image<'static> {
|
||||
match self {
|
||||
CompositeType::Reaction => like_image(),
|
||||
CompositeType::Reaction => like_image_filled(),
|
||||
CompositeType::Repost => {
|
||||
repost_image(darkmode).tint(Color32::from_rgb(0x68, 0xC3, 0x51))
|
||||
}
|
||||
|
||||
@@ -253,6 +253,12 @@ pub fn zap_light_image() -> Image<'static> {
|
||||
zap_dark_image().tint(Color32::BLACK)
|
||||
}
|
||||
|
||||
pub fn like_image_filled() -> Image<'static> {
|
||||
Image::new(include_image!(
|
||||
"../../../assets/icons/like_icon_filled_4x.png"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn like_image() -> Image<'static> {
|
||||
Image::new(include_image!("../../../assets/icons/like_icon_4x.png"))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{widgets::x_button, ProfilePic, ProfilePreview, PulseAlpha, Username}
|
||||
pub use contents::{render_note_preview, NoteContents};
|
||||
pub use context::NoteContextButton;
|
||||
use notedeck::get_current_wallet;
|
||||
use notedeck::note::ZapTargetAmount;
|
||||
use notedeck::note::{reaction_sent_id, ZapTargetAmount};
|
||||
use notedeck::ui::is_narrow;
|
||||
use notedeck::Accounts;
|
||||
use notedeck::GlobalWallet;
|
||||
@@ -26,7 +26,7 @@ use egui::{Id, Pos2, Rect, Response, Sense};
|
||||
use enostr::{KeypairUnowned, NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
|
||||
use notedeck::{
|
||||
note::{NoteAction, NoteContext, ZapAction},
|
||||
note::{NoteAction, NoteContext, ReactAction, ZapAction},
|
||||
tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, ZapTarget, Zaps,
|
||||
};
|
||||
|
||||
@@ -461,6 +461,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
||||
),
|
||||
self.note.id(),
|
||||
self.note.pubkey(),
|
||||
self.note_context.accounts.selected_account_pubkey(),
|
||||
note_key,
|
||||
self.note_context.i18n,
|
||||
)
|
||||
@@ -549,6 +550,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
||||
),
|
||||
self.note.id(),
|
||||
self.note.pubkey(),
|
||||
self.note_context.accounts.selected_account_pubkey(),
|
||||
note_key,
|
||||
self.note_context.i18n,
|
||||
)
|
||||
@@ -848,6 +850,7 @@ fn render_note_actionbar(
|
||||
zapper: Option<Zapper<'_>>,
|
||||
note_id: &[u8; 32],
|
||||
note_pubkey: &[u8; 32],
|
||||
current_user_pubkey: &Pubkey,
|
||||
note_key: NoteKey,
|
||||
i18n: &mut Localization,
|
||||
) -> Option<NoteAction> {
|
||||
@@ -859,6 +862,14 @@ fn render_note_actionbar(
|
||||
let reply_resp =
|
||||
reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
let filled = ui
|
||||
.ctx()
|
||||
.data(|d| d.get_temp(reaction_sent_id(current_user_pubkey, note_id)))
|
||||
== Some(true);
|
||||
|
||||
let like_resp =
|
||||
like_button(ui, i18n, note_key, filled).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
let quote_resp =
|
||||
quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
@@ -866,6 +877,13 @@ fn render_note_actionbar(
|
||||
action = Some(NoteAction::Reply(NoteId::new(*note_id)));
|
||||
}
|
||||
|
||||
if like_resp.clicked() {
|
||||
action = Some(NoteAction::React(ReactAction::new(
|
||||
NoteId::new(*note_id),
|
||||
"🤙🏻",
|
||||
)));
|
||||
}
|
||||
|
||||
if quote_resp.clicked() {
|
||||
action = Some(NoteAction::Repost(NoteId::new(*note_id)));
|
||||
}
|
||||
@@ -918,6 +936,42 @@ fn reply_button(ui: &mut egui::Ui, i18n: &mut Localization, note_key: NoteKey) -
|
||||
resp.union(put_resp)
|
||||
}
|
||||
|
||||
fn like_button(
|
||||
ui: &mut egui::Ui,
|
||||
i18n: &mut Localization,
|
||||
note_key: NoteKey,
|
||||
filled: bool,
|
||||
) -> egui::Response {
|
||||
let img = {
|
||||
let img = if filled {
|
||||
app_images::like_image_filled()
|
||||
} else {
|
||||
app_images::like_image()
|
||||
};
|
||||
|
||||
if ui.visuals().dark_mode {
|
||||
img.tint(ui.visuals().text_color())
|
||||
} else {
|
||||
img
|
||||
}
|
||||
};
|
||||
|
||||
let (rect, size, resp) =
|
||||
crate::anim::hover_expand_small(ui, ui.id().with(("like_anim", note_key)));
|
||||
|
||||
// align rect to note contents
|
||||
let expand_size = 5.0; // from hover_expand_small
|
||||
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
|
||||
|
||||
let put_resp = ui.put(rect, img.max_width(size)).on_hover_text(tr!(
|
||||
i18n,
|
||||
"Like this note",
|
||||
"Hover text for like button"
|
||||
));
|
||||
|
||||
resp.union(put_resp)
|
||||
}
|
||||
|
||||
fn repost_icon(dark_mode: bool) -> egui::Image<'static> {
|
||||
if dark_mode {
|
||||
app_images::repost_dark_image()
|
||||
|
||||
Reference in New Issue
Block a user