commit 05fe164a49357f4e50e61ef9c61b46e30af1c709
parent 2d566cc637a4fd1f14cdc698e54bcba2f20a2387
Author: William Casarin <jb55@jb55.com>
Date: Fri, 19 Apr 2024 22:00:19 -0700
ui: add initial Profile hover previews
The idea with these is that on notedeck you can just hover your cursor
over a profile link to see the profile. I just have a stub for now, but
full design coming soon after.
Also simplify the preview system even further with a macro. In the
future I imagine we can grep every preview in the codebase, and then
include this as a string inside this macro. This is some kind of
template metaprogramming insanity but in theory it could work.
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
13 files changed, 148 insertions(+), 28 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -3,6 +3,7 @@
perf.data
perf.data.old
target
+queries/damus-notifs.json
.git
cache
/dist
diff --git a/Cargo.lock b/Cargo.lock
@@ -2557,8 +2557,7 @@ dependencies = [
[[package]]
name = "nostrdb"
version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4d574f011d7bca4fcb7167da332da7a73d5612b24a187df08f5a000de08d3f4"
+source = "git+https://github.com/damus-io/nostrdb-rs?rev=b6c5d8fb9f3f79f14d69dd2d6eca2712e0f0a42d#b6c5d8fb9f3f79f14d69dd2d6eca2712e0f0a42d"
dependencies = [
"bindgen",
"cc",
diff --git a/Cargo.toml b/Cargo.toml
@@ -31,8 +31,8 @@ serde_json = "1.0.89"
env_logger = "0.10.0"
puffin_egui = { version = "0.27.0", optional = true }
puffin = { version = "0.19.0", optional = true }
-#nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2675e7244554e40c9ee10d82b42bc647fef4c17d" }
-nostrdb = "0.3.2"
+nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "b6c5d8fb9f3f79f14d69dd2d6eca2712e0f0a42d" }
+#nostrdb = "0.3.2"
hex = "0.4.3"
base32 = "0.4.0"
nostr-sdk = "0.29.0"
diff --git a/preview b/preview
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+cargo run --bin ui_preview -- "$@"
diff --git a/src/profile.rs b/src/profile.rs
@@ -1,7 +1,7 @@
use nostrdb::ProfileRecord;
pub fn get_profile_name<'a>(record: &'a ProfileRecord) -> Option<&'a str> {
- let profile = record.record.profile()?;
+ let profile = record.record().profile()?;
let display_name = profile.display_name();
let name = profile.name();
diff --git a/src/test_data.rs b/src/test_data.rs
@@ -1,4 +1,5 @@
use enostr::RelayPool;
+use nostrdb::ProfileRecord;
#[allow(unused_must_use)]
pub fn sample_pool() -> RelayPool {
@@ -17,3 +18,34 @@ pub fn sample_pool() -> RelayPool {
pool
}
+
+const TEST_PROFILE_DATA: [u8; 384] = [
+ 0x04, 0x00, 0x00, 0x00, 0x94, 0xfe, 0xff, 0xff, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x19, 0xef, 0x9c, 0x65, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x57, 0x26, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x66, 0x69, 0x78, 0x6d,
+ 0x65, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xff,
+ 0xd8, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00,
+ 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x20, 0x00,
+ 0x1c, 0x00, 0x08, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x10, 0x00,
+ 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6a, 0x62, 0x35, 0x35,
+ 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x64, 0x61, 0x6d, 0x75, 0x73, 0x2e, 0x69, 0x6f,
+ 0x20, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x2e, 0x20, 0x62, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e,
+ 0x20, 0x61, 0x6e, 0x64, 0x20, 0x6e, 0x6f, 0x73, 0x74, 0x72, 0x20, 0x64, 0x65, 0x76, 0x00, 0x00,
+ 0x1e, 0x00, 0x00, 0x00, 0x57, 0x69, 0x6c, 0x6c, 0x69, 0x61, 0x6d, 0x20, 0x43, 0x61, 0x73, 0x61,
+ 0x72, 0x69, 0x6e, 0x20, 0xf0, 0x9f, 0x87, 0xa8, 0xf0, 0x9f, 0x87, 0xa6, 0xe2, 0x9a, 0xa1, 0xef,
+ 0xb8, 0x8f, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f,
+ 0x63, 0x64, 0x6e, 0x2e, 0x6a, 0x62, 0x35, 0x35, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x6d, 0x67,
+ 0x2f, 0x72, 0x65, 0x64, 0x2d, 0x6d, 0x65, 0x2e, 0x6a, 0x70, 0x67, 0x00, 0x0d, 0x00, 0x00, 0x00,
+ 0x6a, 0x62, 0x35, 0x35, 0x40, 0x6a, 0x62, 0x35, 0x35, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x6c, 0x6e, 0x75, 0x72,
+ 0x6c, 0x31, 0x64, 0x70, 0x36, 0x38, 0x67, 0x75, 0x72, 0x6e, 0x38, 0x67, 0x68, 0x6a, 0x37, 0x75,
+ 0x6d, 0x39, 0x64, 0x65, 0x6a, 0x38, 0x78, 0x63, 0x74, 0x35, 0x77, 0x76, 0x68, 0x78, 0x63, 0x6d,
+ 0x6d, 0x76, 0x39, 0x75, 0x68, 0x38, 0x77, 0x65, 0x74, 0x76, 0x64, 0x73, 0x6b, 0x6b, 0x6b, 0x6d,
+ 0x6e, 0x30, 0x77, 0x61, 0x68, 0x7a, 0x37, 0x6d, 0x72, 0x77, 0x77, 0x34, 0x65, 0x78, 0x63, 0x75,
+ 0x70, 0x30, 0x64, 0x66, 0x33, 0x72, 0x32, 0x64, 0x67, 0x33, 0x6d, 0x6a, 0x34, 0x34, 0x34, 0x00,
+ 0x0c, 0x00, 0x24, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x1c, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
+];
+
+pub fn test_profile_record() -> ProfileRecord<'static> {
+ ProfileRecord::new_owned(&TEST_PROFILE_DATA).unwrap()
+}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
@@ -1,10 +1,12 @@
pub mod note;
pub mod preview;
+pub mod profile;
pub mod relay;
pub mod username;
pub use note::Note;
pub use preview::{Preview, PreviewApp};
+pub use profile::ProfilePreview;
pub use relay::RelayView;
pub use username::Username;
diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs
@@ -123,16 +123,27 @@ fn render_note_contents(
match block.blocktype() {
BlockType::MentionBech32 => match block.as_mention().unwrap() {
Mention::Pubkey(npub) => {
- ui.colored_label(colors::PURPLE, "@");
- let profile = damus.ndb.get_profile_by_pubkey(txn, npub.pubkey()).ok();
- if let Some(name) = profile
- .as_ref()
- .and_then(|p| crate::profile::get_profile_name(p))
- {
- ui.colored_label(colors::PURPLE, name);
- } else {
- ui.colored_label(colors::PURPLE, "nostrich");
- }
+ ui.horizontal(|ui| {
+ let profile = damus.ndb.get_profile_by_pubkey(txn, npub.pubkey()).ok();
+
+ let name: String = if let Some(name) =
+ profile.as_ref().and_then(crate::profile::get_profile_name)
+ {
+ format!("@{}", name)
+ } else {
+ "@nostrich".to_string()
+ };
+
+ let resp = ui.colored_label(colors::PURPLE, &name);
+
+ if let Some(rec) = profile.as_ref() {
+ resp.on_hover_ui_at_pointer(|ui| {
+ egui::Frame::default().show(ui, |ui| {
+ ui.add(ui::ProfilePreview::new(rec));
+ });
+ });
+ }
+ });
}
Mention::Note(note) if options.has_note_previews() => {
diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs
@@ -106,7 +106,7 @@ impl<'a> Note<'a> {
match profile
.as_ref()
.ok()
- .and_then(|p| p.record.profile()?.picture())
+ .and_then(|p| p.record().profile()?.picture())
{
// these have different lifetimes and types,
// so the calls must be separate
diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs
@@ -0,0 +1,3 @@
+pub mod preview;
+
+pub use preview::ProfilePreview;
diff --git a/src/ui/profile/preview.rs b/src/ui/profile/preview.rs
@@ -0,0 +1,65 @@
+use nostrdb::ProfileRecord;
+
+pub struct ProfilePreview<'a> {
+ profile: &'a ProfileRecord<'a>,
+}
+
+impl<'a> ProfilePreview<'a> {
+ pub fn new(profile: &'a ProfileRecord<'a>) -> Self {
+ ProfilePreview { profile }
+ }
+}
+
+impl<'a> egui::Widget for ProfilePreview<'a> {
+ fn ui(self, ui: &mut egui::Ui) -> egui::Response {
+ ui.horizontal(|ui| {
+ ui.label("Profile");
+ let name = if let Some(name) = crate::profile::get_profile_name(self.profile) {
+ name
+ } else {
+ "nostrich"
+ };
+ ui.label(name);
+ })
+ .response
+ }
+}
+
+mod previews {
+ use super::*;
+ use crate::test_data::test_profile_record;
+ use crate::ui::{Preview, View};
+ use egui::Widget;
+
+ pub struct ProfilePreviewPreview<'a> {
+ profile: ProfileRecord<'a>,
+ }
+
+ impl<'a> ProfilePreviewPreview<'a> {
+ pub fn new() -> Self {
+ let profile = test_profile_record();
+ ProfilePreviewPreview { profile }
+ }
+ }
+
+ impl<'a> Default for ProfilePreviewPreview<'a> {
+ fn default() -> Self {
+ ProfilePreviewPreview::new()
+ }
+ }
+
+ impl<'a> View for ProfilePreviewPreview<'a> {
+ fn ui(&mut self, ui: &mut egui::Ui) {
+ ProfilePreview::new(&self.profile).ui(ui);
+ }
+ }
+
+ impl<'a> Preview for ProfilePreview<'a> {
+ /// A preview of the profile preview :D
+ type Prev = ProfilePreviewPreview<'a>;
+
+ fn preview() -> Self::Prev {
+ ProfilePreviewPreview::new()
+ }
+ }
+}
diff --git a/src/ui/username.rs b/src/ui/username.rs
@@ -44,7 +44,7 @@ impl<'a> Widget for Username<'a> {
};
if let Some(profile) = self.profile {
- if let Some(prof) = profile.record.profile() {
+ if let Some(prof) = profile.record().profile() {
if prof.display_name().is_some() && prof.display_name().unwrap() != "" {
ui_abbreviate_name(ui, prof.display_name().unwrap(), self.abbrev, color);
} else if let Some(name) = prof.name() {
diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs
@@ -2,8 +2,7 @@ use notedeck::account_login_view::AccountLoginView;
use notedeck::app_creation::{
generate_mobile_emulator_native_options, generate_native_options, setup_cc,
};
-use notedeck::relay_view::RelayView;
-use notedeck::ui::{Preview, PreviewApp};
+use notedeck::ui::{Preview, PreviewApp, ProfilePreview, RelayView};
use std::env;
struct PreviewRunner {
@@ -38,6 +37,20 @@ impl PreviewRunner {
}
}
+macro_rules! previews {
+ // Accept a runner and name variable, followed by one or more identifiers for the views
+ ($runner:expr, $name:expr, $($view:ident),* $(,)?) => {
+ match $name.as_ref() {
+ $(
+ stringify!($view) => {
+ $runner.run($view::preview()).await;
+ }
+ )*
+ _ => println!("Component not found."),
+ }
+ };
+}
+
#[tokio::main]
async fn main() {
let mut name: Option<String> = None;
@@ -60,13 +73,5 @@ async fn main() {
let runner = PreviewRunner::new(is_mobile);
- match name.as_ref() {
- "AccountLoginView" => {
- runner.run(AccountLoginView::preview()).await;
- }
- "RelayView" => {
- runner.run(RelayView::preview()).await;
- }
- _ => println!("Component not found."),
- }
+ previews!(runner, name, RelayView, AccountLoginView, ProfilePreview,);
}