Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Note more options #318

Merged
merged 3 commits into from
Sep 26, 2024
Merged
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
4 changes: 4 additions & 0 deletions enostr/src/pubkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ impl Pubkey {
Ok(Pubkey(data.1.try_into().unwrap()))
}
}

pub fn to_bech(&self) -> Option<String> {
nostr::bech32::encode::<nostr::bech32::Bech32>(HRP_NPUB, &self.0).ok()
}
}

impl fmt::Display for Pubkey {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod login_manager;
mod macos_key_storage;
mod nav;
mod note;
mod note_options;
mod notecache;
mod post;
mod post_action_executor;
Expand Down
40 changes: 40 additions & 0 deletions src/note_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use enostr::{NoteId, Pubkey};
use nostrdb::Note;

#[derive(Clone)]
#[allow(clippy::enum_variant_names)]
pub enum NoteOptionSelection {
CopyText,
CopyPubkey,
CopyNoteId,
}

pub fn process_note_selection(
ui: &mut egui::Ui,
selection: Option<NoteOptionSelection>,
note: &Note<'_>,
) {
if let Some(option) = selection {
match option {
NoteOptionSelection::CopyText => {
ui.output_mut(|w| {
w.copied_text = note.content().to_string();
});
}
NoteOptionSelection::CopyPubkey => {
ui.output_mut(|w| {
if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() {
w.copied_text = bech;
}
});
}
NoteOptionSelection::CopyNoteId => {
ui.output_mut(|w| {
if let Some(bech) = NoteId::new(*note.id()).to_bech() {
w.copied_text = bech;
}
});
}
}
}
}
8 changes: 1 addition & 7 deletions src/notecache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ impl NoteCache {
pub struct CachedNote {
reltime: TimeCached<String>,
pub reply: NoteReplyBuf,
pub bar_open: bool,
}

impl CachedNote {
Expand All @@ -46,12 +45,7 @@ impl CachedNote {
Box::new(move || time_ago_since(created_at)),
);
let reply = NoteReply::new(note.tags()).to_owned();
let bar_open = false;
CachedNote {
reltime,
reply,
bar_open,
}
CachedNote { reltime, reply }
}

pub fn reltime_str_mut(&mut self) -> &str {
Expand Down
6 changes: 5 additions & 1 deletion src/ui/note/contents.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::images::ImageType;
use crate::imgcache::ImageCache;
use crate::note_options::process_note_selection;
use crate::notecache::NoteCache;
use crate::ui::note::NoteOptions;
use crate::ui::ProfilePic;
Expand Down Expand Up @@ -103,12 +104,15 @@ pub fn render_note_preview(
ui.visuals().noninteractive().bg_stroke.color,
))
.show(ui, |ui| {
ui::NoteView::new(ndb, note_cache, img_cache, &note)
let resp = ui::NoteView::new(ndb, note_cache, img_cache, &note)
.actionbar(false)
.small_pfp(true)
.wide(true)
.note_previews(false)
.use_more_options_button(true)
.show(ui);

process_note_selection(ui, resp.option_selection, &note);
})
.response
}
Expand Down
167 changes: 151 additions & 16 deletions src/ui/note/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ use crate::{
app_style::NotedeckTextStyle,
colors,
imgcache::ImageCache,
note_options::NoteOptionSelection,
notecache::{CachedNote, NoteCache},
ui::{self, View},
};
use egui::{Id, Label, Response, RichText, Sense};
use egui::{menu::BarState, Align, Id, InnerResponse, Label, Layout, Response, RichText, Sense};
use enostr::NoteId;
use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction};

Expand All @@ -30,11 +31,34 @@ pub struct NoteView<'a> {
img_cache: &'a mut ImageCache,
note: &'a nostrdb::Note<'a>,
flags: NoteOptions,
use_options: bool,
}

pub struct NoteResponse {
pub response: egui::Response,
pub action: Option<BarAction>,
pub option_selection: Option<NoteOptionSelection>,
}

impl NoteResponse {
pub fn new(response: egui::Response) -> Self {
Self {
response,
action: None,
option_selection: None,
}
}

pub fn with_action(self, action: Option<BarAction>) -> Self {
Self { action, ..self }
}

pub fn select_option(self, option_selection: Option<NoteOptionSelection>) -> Self {
Self {
option_selection,
..self
}
}
}

impl<'a> View for NoteView<'a> {
Expand Down Expand Up @@ -177,6 +201,7 @@ impl<'a> NoteView<'a> {
img_cache,
note,
flags,
use_options: false,
}
}

Expand Down Expand Up @@ -215,6 +240,13 @@ impl<'a> NoteView<'a> {
self
}

pub fn use_more_options_button(self, enable: bool) -> Self {
Self {
use_options: enable,
..self
}
}

pub fn options(&self) -> NoteOptions {
self.flags
}
Expand Down Expand Up @@ -324,10 +356,7 @@ impl<'a> NoteView<'a> {

pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse {
if self.options().has_textmode() {
NoteResponse {
response: self.textmode_ui(ui),
action: None,
}
NoteResponse::new(self.textmode_ui(ui))
} else {
let txn = self.note.txn().expect("txn");
if let Some(note_to_repost) = get_reposted_note(self.ndb, txn, self.note) {
Expand Down Expand Up @@ -369,17 +398,29 @@ impl<'a> NoteView<'a> {
note_cache: &mut NoteCache,
note: &Note,
profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
) -> egui::Response {
use_options_button: bool,
) -> NoteResponse {
let note_key = note.key().unwrap();

ui.horizontal(|ui| {
let inner_response = ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
ui.add(ui::Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20));

let cached_note = note_cache.cached_note_or_insert_mut(note_key, note);
render_reltime(ui, cached_note, true);
})
.response

if use_options_button {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
let more_options_resp = more_options_button(ui, note_key, 8.0);
options_context_menu(ui, more_options_resp)
})
.inner
} else {
None
}
});

NoteResponse::new(inner_response.response).select_option(inner_response.inner)
}

fn show_standard(&mut self, ui: &mut egui::Ui) -> NoteResponse {
Expand All @@ -388,6 +429,7 @@ impl<'a> NoteView<'a> {
let note_key = self.note.key().expect("todo: support non-db notes");
let txn = self.note.txn().expect("todo: support non-db notes");
let mut note_action: Option<BarAction> = None;
let mut selected_option: Option<NoteOptionSelection> = None;
let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey());
let maybe_hitbox = maybe_note_hitbox(ui, note_key);

Expand All @@ -400,7 +442,14 @@ impl<'a> NoteView<'a> {
ui.vertical(|ui| {
ui.add_sized([size.x, self.options().pfp_size()], |ui: &mut egui::Ui| {
ui.horizontal_centered(|ui| {
NoteView::note_header(ui, self.note_cache, self.note, &profile);
selected_option = NoteView::note_header(
ui,
self.note_cache,
self.note,
&profile,
self.use_options,
)
.option_selection;
})
.response
});
Expand Down Expand Up @@ -440,8 +489,14 @@ impl<'a> NoteView<'a> {
self.pfp(note_key, &profile, ui);

ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
NoteView::note_header(ui, self.note_cache, self.note, &profile);

selected_option = NoteView::note_header(
ui,
self.note_cache,
self.note,
&profile,
self.use_options,
)
.option_selection;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;

Expand Down Expand Up @@ -483,10 +538,9 @@ impl<'a> NoteView<'a> {
note_action,
);

NoteResponse {
response,
action: note_action,
}
NoteResponse::new(response)
.with_action(note_action)
.select_option(selected_option)
}
}

Expand Down Expand Up @@ -631,3 +685,84 @@ fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {

resp.union(put_resp)
}

fn more_options_button(ui: &mut egui::Ui, note_key: NoteKey, max_height: f32) -> egui::Response {
let id = ui.id().with(("more_options_anim", note_key));

let expansion_multiple = 2.0;
let max_radius = max_height;
let min_radius = max_radius / expansion_multiple;
let max_distance_between_circles = 2.0;
let min_distance_between_circles = max_distance_between_circles / expansion_multiple;
let max_width = max_radius * 3.0 + max_distance_between_circles * 2.0;

let anim_speed = 0.05;
let expanded_size = egui::vec2(max_width, max_height);
let (rect, response) = ui.allocate_exact_size(expanded_size, egui::Sense::click());

let animation_progress = ui
.ctx()
.animate_bool_with_time(id, response.hovered(), anim_speed);
let cur_distance = min_distance_between_circles
+ (max_distance_between_circles - min_distance_between_circles) * animation_progress;
let cur_radius = min_radius + (max_radius - min_radius) * animation_progress;

let center = rect.center();
let left_circle_center = center - egui::vec2(cur_distance + cur_radius, 0.0);
let right_circle_center = center + egui::vec2(cur_distance + cur_radius, 0.0);

let translated_radius = (cur_radius - 1.0) / 2.0;

let color = if ui.style().visuals.dark_mode {
egui::Color32::WHITE
} else {
egui::Color32::BLACK
};

// Draw circles
let painter = ui.painter_at(rect);
painter.circle_filled(left_circle_center, translated_radius, color);
painter.circle_filled(center, translated_radius, color);
painter.circle_filled(right_circle_center, translated_radius, color);

response
}

fn options_context_menu(
ui: &mut egui::Ui,
more_options_button_resp: egui::Response,
) -> Option<NoteOptionSelection> {
let mut selected_option: Option<NoteOptionSelection> = None;

stationary_arbitrary_menu_button(ui, more_options_button_resp, |ui| {
ui.set_max_width(200.0);
if ui.button("Copy text").clicked() {
selected_option = Some(NoteOptionSelection::CopyText);
ui.close_menu();
}
if ui.button("Copy user public key").clicked() {
selected_option = Some(NoteOptionSelection::CopyPubkey);
ui.close_menu();
}
if ui.button("Copy note id").clicked() {
selected_option = Some(NoteOptionSelection::CopyNoteId);
ui.close_menu();
}
});

selected_option
}

fn stationary_arbitrary_menu_button<R>(
ui: &mut egui::Ui,
button_response: egui::Response,
add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();
let mut bar_state = BarState::load(ui.ctx(), bar_id);

let inner = bar_state.bar_menu(&button_response, add_contents);

bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
1 change: 1 addition & 0 deletions src/ui/note/reply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl<'a> PostReplyView<'a> {
ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, self.note)
.actionbar(false)
.medium_pfp(true)
.use_more_options_button(true)
.show(ui);
});

Expand Down
13 changes: 8 additions & 5 deletions src/ui/thread.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
actionbar::BarAction, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui,
actionbar::BarAction, imgcache::ImageCache, note_options::process_note_selection,
notecache::NoteCache, thread::Threads, ui,
};
use nostrdb::{Ndb, NoteKey, Transaction};
use tracing::{error, warn};
Expand Down Expand Up @@ -115,15 +116,17 @@ impl<'a> ThreadView<'a> {
};

ui::padding(8.0, ui, |ui| {
if let Some(bar_action) =
let note_response =
ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, &note)
.note_previews(!self.textmode)
.textmode(self.textmode)
.show(ui)
.action
{
.use_more_options_button(!self.textmode)
.show(ui);
if let Some(bar_action) = note_response.action {
action = Some(bar_action);
}

process_note_selection(ui, note_response.option_selection, &note);
});

ui::hline(ui);
Expand Down
Loading
Loading