Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/icons/like_icon_4x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icons/like_icon_filled_4x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions crates/notedeck/src/note/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down Expand Up @@ -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),
Expand Down
8 changes: 7 additions & 1 deletion crates/notedeck/src/note/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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))
}
113 changes: 109 additions & 4 deletions crates/notedeck_columns/src/actionbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())));
Expand Down Expand Up @@ -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(&note);

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(&note) 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,
Expand Down
4 changes: 2 additions & 2 deletions crates/notedeck_columns/src/ui/timeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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))
}
Expand Down
6 changes: 6 additions & 0 deletions crates/notedeck_ui/src/app_images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,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"))
}
Expand Down
58 changes: 56 additions & 2 deletions crates/notedeck_ui/src/note/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
};

Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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> {
Expand All @@ -859,13 +862,28 @@ 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);

if reply_resp.clicked() {
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)));
}
Expand Down Expand Up @@ -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()
Expand Down