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
|
/// User has clicked the quote reply action
|
||||||
Reply(NoteId),
|
Reply(NoteId),
|
||||||
|
|
||||||
|
/// User has clicked the like/reaction button
|
||||||
|
React(ReactAction),
|
||||||
|
|
||||||
/// User has clicked the repost button
|
/// User has clicked the repost button
|
||||||
Repost(NoteId),
|
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)]
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
pub enum ZapAction {
|
pub enum ZapAction {
|
||||||
Send(ZapTargetAmount),
|
Send(ZapTargetAmount),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
mod action;
|
mod action;
|
||||||
mod context;
|
mod context;
|
||||||
|
|
||||||
pub use action::{NoteAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
pub use action::{NoteAction, ReactAction, ScrollInfo, ZapAction, ZapTargetAmount};
|
||||||
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
|
||||||
|
|
||||||
use crate::Accounts;
|
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)
|
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 egui_nav::Percent;
|
||||||
use enostr::{NoteId, Pubkey, RelayPool};
|
use enostr::{FilledKeypair, NoteId, Pubkey, RelayPool};
|
||||||
use nostrdb::{Ndb, NoteKey, Transaction};
|
use nostrdb::{IngestMetadata, Ndb, NoteBuilder, NoteKey, Transaction};
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
|
get_wallet_for,
|
||||||
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
|
note::{reaction_sent_id, ReactAction, ZapTargetAmount},
|
||||||
|
Accounts, GlobalWallet, Images, NoteAction, NoteCache, NoteZapTargetOwned, UnknownIds,
|
||||||
|
ZapAction, ZapTarget, ZappingError, Zaps,
|
||||||
};
|
};
|
||||||
use notedeck_ui::media::MediaViewerFlags;
|
use notedeck_ui::media::MediaViewerFlags;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
@@ -76,6 +78,21 @@ fn execute_note_action(
|
|||||||
router_action = Some(RouterAction::route_to(Route::accounts()));
|
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) => {
|
NoteAction::Profile(pubkey) => {
|
||||||
let kind = TimelineKind::Profile(pubkey);
|
let kind = TimelineKind::Profile(pubkey);
|
||||||
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone())));
|
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone())));
|
||||||
@@ -262,6 +279,94 @@ pub fn execute_and_process_note_action(
|
|||||||
resp.router_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(
|
fn send_zap(
|
||||||
sender: &Pubkey,
|
sender: &Pubkey,
|
||||||
zaps: &mut Zaps,
|
zaps: &mut Zaps,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use notedeck::fonts::get_font_size;
|
|||||||
use notedeck::name::get_display_name;
|
use notedeck::name::get_display_name;
|
||||||
use notedeck::ui::is_narrow;
|
use notedeck::ui::is_narrow;
|
||||||
use notedeck::{tr_plural, JobsCache, Muted, NotedeckTextStyle};
|
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 notedeck_ui::{ProfilePic, ProfilePreview};
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
@@ -514,7 +514,7 @@ enum ReferencedNoteType {
|
|||||||
impl CompositeType {
|
impl CompositeType {
|
||||||
fn image(&self, darkmode: bool) -> egui::Image<'static> {
|
fn image(&self, darkmode: bool) -> egui::Image<'static> {
|
||||||
match self {
|
match self {
|
||||||
CompositeType::Reaction => like_image(),
|
CompositeType::Reaction => like_image_filled(),
|
||||||
CompositeType::Repost => {
|
CompositeType::Repost => {
|
||||||
repost_image(darkmode).tint(Color32::from_rgb(0x68, 0xC3, 0x51))
|
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)
|
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> {
|
pub fn like_image() -> Image<'static> {
|
||||||
Image::new(include_image!("../../../assets/icons/like_icon_4x.png"))
|
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 contents::{render_note_preview, NoteContents};
|
||||||
pub use context::NoteContextButton;
|
pub use context::NoteContextButton;
|
||||||
use notedeck::get_current_wallet;
|
use notedeck::get_current_wallet;
|
||||||
use notedeck::note::ZapTargetAmount;
|
use notedeck::note::{reaction_sent_id, ZapTargetAmount};
|
||||||
use notedeck::ui::is_narrow;
|
use notedeck::ui::is_narrow;
|
||||||
use notedeck::Accounts;
|
use notedeck::Accounts;
|
||||||
use notedeck::GlobalWallet;
|
use notedeck::GlobalWallet;
|
||||||
@@ -26,7 +26,7 @@ use egui::{Id, Pos2, Rect, Response, Sense};
|
|||||||
use enostr::{KeypairUnowned, NoteId, Pubkey};
|
use enostr::{KeypairUnowned, NoteId, Pubkey};
|
||||||
use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
|
use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
note::{NoteAction, NoteContext, ZapAction},
|
note::{NoteAction, NoteContext, ReactAction, ZapAction},
|
||||||
tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, ZapTarget, Zaps,
|
tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, ZapTarget, Zaps,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -461,6 +461,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
),
|
),
|
||||||
self.note.id(),
|
self.note.id(),
|
||||||
self.note.pubkey(),
|
self.note.pubkey(),
|
||||||
|
self.note_context.accounts.selected_account_pubkey(),
|
||||||
note_key,
|
note_key,
|
||||||
self.note_context.i18n,
|
self.note_context.i18n,
|
||||||
)
|
)
|
||||||
@@ -549,6 +550,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
),
|
),
|
||||||
self.note.id(),
|
self.note.id(),
|
||||||
self.note.pubkey(),
|
self.note.pubkey(),
|
||||||
|
self.note_context.accounts.selected_account_pubkey(),
|
||||||
note_key,
|
note_key,
|
||||||
self.note_context.i18n,
|
self.note_context.i18n,
|
||||||
)
|
)
|
||||||
@@ -848,6 +850,7 @@ fn render_note_actionbar(
|
|||||||
zapper: Option<Zapper<'_>>,
|
zapper: Option<Zapper<'_>>,
|
||||||
note_id: &[u8; 32],
|
note_id: &[u8; 32],
|
||||||
note_pubkey: &[u8; 32],
|
note_pubkey: &[u8; 32],
|
||||||
|
current_user_pubkey: &Pubkey,
|
||||||
note_key: NoteKey,
|
note_key: NoteKey,
|
||||||
i18n: &mut Localization,
|
i18n: &mut Localization,
|
||||||
) -> Option<NoteAction> {
|
) -> Option<NoteAction> {
|
||||||
@@ -859,6 +862,14 @@ fn render_note_actionbar(
|
|||||||
let reply_resp =
|
let reply_resp =
|
||||||
reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
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 =
|
let quote_resp =
|
||||||
quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
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)));
|
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() {
|
if quote_resp.clicked() {
|
||||||
action = Some(NoteAction::Repost(NoteId::new(*note_id)));
|
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)
|
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> {
|
fn repost_icon(dark_mode: bool) -> egui::Image<'static> {
|
||||||
if dark_mode {
|
if dark_mode {
|
||||||
app_images::repost_dark_image()
|
app_images::repost_dark_image()
|
||||||
|
|||||||
Reference in New Issue
Block a user