@@ -330,6 +330,14 @@ impl Accounts {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn contains_full_kp(&self, pubkey: &enostr::Pubkey) -> bool {
|
||||
if let Some(contains) = self.contains_account(pubkey.bytes()) {
|
||||
contains.has_nsec
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"]
|
||||
pub fn add_account(&mut self, account: Keypair) -> AddAccountAction {
|
||||
let pubkey = account.pubkey;
|
||||
@@ -567,6 +575,18 @@ impl Accounts {
|
||||
self.needs_relay_config = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_full<'a>(&'a self, pubkey: &[u8; 32]) -> Option<FilledKeypair<'a>> {
|
||||
if let Some(contains) = self.contains_account(pubkey) {
|
||||
if contains.has_nsec {
|
||||
if let Some(kp) = self.get_account(contains.index) {
|
||||
return kp.to_full();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option<usize> {
|
||||
|
||||
@@ -49,4 +49,11 @@ impl NotedeckTextStyle {
|
||||
pub fn get_font_id(&self, ctx: &Context) -> FontId {
|
||||
FontId::new(get_font_size(ctx, self), self.font_family())
|
||||
}
|
||||
|
||||
pub fn get_bolded_font(&self, ctx: &Context) -> FontId {
|
||||
FontId::new(
|
||||
get_font_size(ctx, self),
|
||||
egui::FontFamily::Name(crate::NamedFontFamily::Bold.as_str().into()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use notedeck_chrome::setup::generate_native_options;
|
||||
use notedeck_chrome::Notedeck;
|
||||
use notedeck_columns::ui::configure_deck::ConfigureDeckView;
|
||||
use notedeck_columns::ui::edit_deck::EditDeckView;
|
||||
use notedeck_columns::ui::profile::EditProfileView;
|
||||
use notedeck_columns::ui::{
|
||||
account_login_view::AccountLoginView, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic,
|
||||
ProfilePreview, RelayView,
|
||||
@@ -95,5 +96,6 @@ async fn main() {
|
||||
PostView,
|
||||
ConfigureDeckView,
|
||||
EditDeckView,
|
||||
EditProfileView,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ use egui::Color32;
|
||||
pub const ALMOST_WHITE: Color32 = Color32::from_rgb(0xFA, 0xFA, 0xFA);
|
||||
pub const MID_GRAY: Color32 = Color32::from_rgb(0xbd, 0xbd, 0xbd);
|
||||
pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9);
|
||||
pub const TEAL: Color32 = Color32::from_rgb(0x77, 0xDC, 0xE1);
|
||||
|
||||
@@ -23,6 +23,7 @@ mod nav;
|
||||
mod notes_holder;
|
||||
mod post;
|
||||
mod profile;
|
||||
mod profile_state;
|
||||
pub mod relay_pool_manager;
|
||||
mod route;
|
||||
mod subscriptions;
|
||||
@@ -42,6 +43,6 @@ pub mod storage;
|
||||
|
||||
pub use app::Damus;
|
||||
pub use error::Error;
|
||||
pub use profile::DisplayName;
|
||||
pub use profile::NostrName;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, error::Error>;
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::{
|
||||
deck_state::DeckState,
|
||||
decks::{Deck, DecksAction, DecksCache},
|
||||
notes_holder::NotesHolder,
|
||||
profile::Profile,
|
||||
profile::{Profile, ProfileAction, SaveProfileChanges},
|
||||
profile_state::ProfileState,
|
||||
relay_pool_manager::RelayPoolManager,
|
||||
route::Route,
|
||||
thread::Thread,
|
||||
@@ -21,6 +22,7 @@ use crate::{
|
||||
configure_deck::ConfigureDeckView,
|
||||
edit_deck::{EditDeckResponse, EditDeckView},
|
||||
note::{PostAction, PostType},
|
||||
profile::EditProfileView,
|
||||
support::SupportView,
|
||||
RelayView, View,
|
||||
},
|
||||
@@ -39,6 +41,7 @@ pub enum RenderNavAction {
|
||||
RemoveColumn,
|
||||
PostAction(PostAction),
|
||||
NoteAction(NoteAction),
|
||||
ProfileAction(ProfileAction),
|
||||
SwitchingAction(SwitchingAction),
|
||||
}
|
||||
|
||||
@@ -168,6 +171,16 @@ impl RenderNavResponse {
|
||||
RenderNavAction::SwitchingAction(switching_action) => {
|
||||
switching_occured = switching_action.process(&mut app.decks_cache, ctx);
|
||||
}
|
||||
RenderNavAction::ProfileAction(profile_action) => {
|
||||
profile_action.process(
|
||||
&mut app.view_state.pubkey_to_profile_state,
|
||||
ctx.ndb,
|
||||
ctx.pool,
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.column_mut(col)
|
||||
.router_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +381,35 @@ fn render_nav_body(
|
||||
|
||||
action
|
||||
}
|
||||
Route::EditProfile(pubkey) => {
|
||||
let mut action = None;
|
||||
if let Some(kp) = ctx.accounts.get_full(pubkey.bytes()) {
|
||||
let state = app
|
||||
.view_state
|
||||
.pubkey_to_profile_state
|
||||
.entry(*kp.pubkey)
|
||||
.or_insert_with(|| {
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
if let Ok(record) = ctx.ndb.get_profile_by_pubkey(&txn, kp.pubkey.bytes()) {
|
||||
ProfileState::from_profile(&record)
|
||||
} else {
|
||||
ProfileState::default()
|
||||
}
|
||||
});
|
||||
if EditProfileView::new(state, ctx.img_cache).ui(ui) {
|
||||
if let Some(taken_state) =
|
||||
app.view_state.pubkey_to_profile_state.remove(kp.pubkey)
|
||||
{
|
||||
action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
|
||||
SaveProfileChanges::new(kp.to_full(), taken_state),
|
||||
)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Pubkey in EditProfile route did not have an nsec attached in Accounts");
|
||||
}
|
||||
action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
use enostr::{Filter, Pubkey};
|
||||
use nostrdb::{FilterBuilder, Ndb, ProfileRecord, Transaction};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use enostr::{Filter, FullKeypair, Pubkey, RelayPool};
|
||||
use nostrdb::{
|
||||
FilterBuilder, Ndb, Note, NoteBuildOptions, NoteBuilder, ProfileRecord, Transaction,
|
||||
};
|
||||
|
||||
use notedeck::{filter::default_limit, FilterState, MuteFun, NoteCache, NoteRef};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
multi_subscriber::MultiSubscriber,
|
||||
notes_holder::NotesHolder,
|
||||
profile_state::ProfileState,
|
||||
route::{Route, Router},
|
||||
timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind, TimelineTab},
|
||||
};
|
||||
|
||||
pub enum DisplayName<'a> {
|
||||
One(&'a str),
|
||||
|
||||
Both {
|
||||
username: &'a str,
|
||||
display_name: &'a str,
|
||||
},
|
||||
pub struct NostrName<'a> {
|
||||
pub username: Option<&'a str>,
|
||||
pub display_name: Option<&'a str>,
|
||||
pub nip05: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> DisplayName<'a> {
|
||||
pub fn username(&self) -> &'a str {
|
||||
match self {
|
||||
Self::One(n) => n,
|
||||
Self::Both { username, .. } => username,
|
||||
impl<'a> NostrName<'a> {
|
||||
pub fn name(&self) -> &'a str {
|
||||
if let Some(name) = self.username {
|
||||
name
|
||||
} else if let Some(name) = self.display_name {
|
||||
name
|
||||
} else {
|
||||
self.nip05.unwrap_or("??")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Self {
|
||||
username: None,
|
||||
display_name: None,
|
||||
nip05: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,19 +46,35 @@ fn is_empty(s: &str) -> bool {
|
||||
s.chars().all(|c| c.is_whitespace())
|
||||
}
|
||||
|
||||
pub fn get_profile_name<'a>(record: &ProfileRecord<'a>) -> Option<DisplayName<'a>> {
|
||||
let profile = record.record().profile()?;
|
||||
let display_name = profile.display_name().filter(|n| !is_empty(n));
|
||||
let name = profile.name().filter(|n| !is_empty(n));
|
||||
pub fn get_display_name<'a>(record: Option<&ProfileRecord<'a>>) -> NostrName<'a> {
|
||||
if let Some(record) = record {
|
||||
if let Some(profile) = record.record().profile() {
|
||||
let display_name = profile.display_name().filter(|n| !is_empty(n));
|
||||
let username = profile.name().filter(|n| !is_empty(n));
|
||||
let nip05 = if let Some(raw_nip05) = profile.nip05() {
|
||||
if let Some(at_pos) = raw_nip05.find('@') {
|
||||
if raw_nip05.starts_with('_') {
|
||||
raw_nip05.get(at_pos + 1..)
|
||||
} else {
|
||||
Some(raw_nip05)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match (display_name, name) {
|
||||
(None, None) => None,
|
||||
(Some(disp), None) => Some(DisplayName::One(disp)),
|
||||
(None, Some(username)) => Some(DisplayName::One(username)),
|
||||
(Some(display_name), Some(username)) => Some(DisplayName::Both {
|
||||
display_name,
|
||||
username,
|
||||
}),
|
||||
NostrName {
|
||||
username,
|
||||
display_name,
|
||||
nip05,
|
||||
}
|
||||
} else {
|
||||
NostrName::unknown()
|
||||
}
|
||||
} else {
|
||||
NostrName::unknown()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,3 +162,62 @@ impl NotesHolder for Profile {
|
||||
self.multi_subscriber = Some(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SaveProfileChanges {
|
||||
pub kp: FullKeypair,
|
||||
pub state: ProfileState,
|
||||
}
|
||||
|
||||
impl SaveProfileChanges {
|
||||
pub fn new(kp: FullKeypair, state: ProfileState) -> Self {
|
||||
Self { kp, state }
|
||||
}
|
||||
pub fn to_note(&self) -> Note {
|
||||
let sec = &self.kp.secret_key.to_secret_bytes();
|
||||
add_client_tag(NoteBuilder::new())
|
||||
.kind(0)
|
||||
.content(&self.state.to_json())
|
||||
.options(NoteBuildOptions::default().created_at(true).sign(sec))
|
||||
.build()
|
||||
.expect("should build")
|
||||
}
|
||||
}
|
||||
|
||||
fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
|
||||
builder
|
||||
.start_tag()
|
||||
.tag_str("client")
|
||||
.tag_str("Damus Notedeck")
|
||||
}
|
||||
|
||||
pub enum ProfileAction {
|
||||
Edit(FullKeypair),
|
||||
SaveChanges(SaveProfileChanges),
|
||||
}
|
||||
|
||||
impl ProfileAction {
|
||||
pub fn process(
|
||||
&self,
|
||||
state_map: &mut HashMap<Pubkey, ProfileState>,
|
||||
ndb: &Ndb,
|
||||
pool: &mut RelayPool,
|
||||
router: &mut Router<Route>,
|
||||
) {
|
||||
match self {
|
||||
ProfileAction::Edit(kp) => {
|
||||
router.route_to(Route::EditProfile(kp.pubkey));
|
||||
}
|
||||
ProfileAction::SaveChanges(changes) => {
|
||||
let raw_msg = format!("[\"EVENT\",{}]", changes.to_note().json().unwrap());
|
||||
|
||||
let _ = ndb.process_client_event(raw_msg.as_str());
|
||||
let _ = state_map.remove_entry(&changes.kp.pubkey);
|
||||
|
||||
info!("sending {}", raw_msg);
|
||||
pool.send(&enostr::ClientMessage::raw(raw_msg));
|
||||
|
||||
router.go_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
crates/notedeck_columns/src/profile_state.rs
Normal file
79
crates/notedeck_columns/src/profile_state.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use nostrdb::{NdbProfile, ProfileRecord};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ProfileState {
|
||||
pub display_name: String,
|
||||
pub name: String,
|
||||
pub picture: String,
|
||||
pub banner: String,
|
||||
pub about: String,
|
||||
pub website: String,
|
||||
pub lud16: String,
|
||||
pub nip05: String,
|
||||
}
|
||||
|
||||
impl ProfileState {
|
||||
pub fn from_profile(record: &ProfileRecord<'_>) -> Self {
|
||||
let display_name = get_item(record, |p| p.display_name());
|
||||
let username = get_item(record, |p| p.name());
|
||||
let profile_picture = get_item(record, |p| p.picture());
|
||||
let cover_image = get_item(record, |p| p.banner());
|
||||
let about = get_item(record, |p| p.about());
|
||||
let website = get_item(record, |p| p.website());
|
||||
let lud16 = get_item(record, |p| p.lud16());
|
||||
let nip05 = get_item(record, |p| p.nip05());
|
||||
|
||||
Self {
|
||||
display_name,
|
||||
name: username,
|
||||
picture: profile_picture,
|
||||
banner: cover_image,
|
||||
about,
|
||||
website,
|
||||
lud16,
|
||||
nip05,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> String {
|
||||
let mut fields = Vec::new();
|
||||
|
||||
if !self.display_name.is_empty() {
|
||||
fields.push(format!(r#""display_name":"{}""#, self.display_name));
|
||||
}
|
||||
if !self.name.is_empty() {
|
||||
fields.push(format!(r#""name":"{}""#, self.name));
|
||||
}
|
||||
if !self.picture.is_empty() {
|
||||
fields.push(format!(r#""picture":"{}""#, self.picture));
|
||||
}
|
||||
if !self.banner.is_empty() {
|
||||
fields.push(format!(r#""banner":"{}""#, self.banner));
|
||||
}
|
||||
if !self.about.is_empty() {
|
||||
fields.push(format!(r#""about":"{}""#, self.about));
|
||||
}
|
||||
if !self.website.is_empty() {
|
||||
fields.push(format!(r#""website":"{}""#, self.website));
|
||||
}
|
||||
if !self.lud16.is_empty() {
|
||||
fields.push(format!(r#""lud16":"{}""#, self.lud16));
|
||||
}
|
||||
if !self.nip05.is_empty() {
|
||||
fields.push(format!(r#""nip05":"{}""#, self.nip05));
|
||||
}
|
||||
|
||||
format!("{{{}}}", fields.join(","))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_item<'a>(
|
||||
record: &ProfileRecord<'a>,
|
||||
item_retriever: fn(NdbProfile<'a>) -> Option<&'a str>,
|
||||
) -> String {
|
||||
record
|
||||
.record()
|
||||
.profile()
|
||||
.and_then(item_retriever)
|
||||
.map_or_else(String::new, ToString::to_string)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ pub enum Route {
|
||||
Relays,
|
||||
ComposeNote,
|
||||
AddColumn(AddColumnRoute),
|
||||
EditProfile(Pubkey),
|
||||
Support,
|
||||
NewDeck,
|
||||
EditDeck(usize),
|
||||
@@ -104,6 +105,7 @@ impl Route {
|
||||
Route::Support => ColumnTitle::simple("Damus Support"),
|
||||
Route::NewDeck => ColumnTitle::simple("Add Deck"),
|
||||
Route::EditDeck(_) => ColumnTitle::simple("Edit Deck"),
|
||||
Route::EditProfile(_) => ColumnTitle::simple("Edit Profile"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +217,7 @@ impl fmt::Display for Route {
|
||||
Route::Support => write!(f, "Support"),
|
||||
Route::NewDeck => write!(f, "Add Deck"),
|
||||
Route::EditDeck(_) => write!(f, "Edit Deck"),
|
||||
Route::EditProfile(_) => write!(f, "Edit Profile"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,6 +541,11 @@ fn serialize_route(route: &Route, columns: &Columns) -> Option<String> {
|
||||
selections.push(Selection::Keyword(Keyword::Edit));
|
||||
selections.push(Selection::Payload(index.to_string()));
|
||||
}
|
||||
Route::EditProfile(pubkey) => {
|
||||
selections.push(Selection::Keyword(Keyword::Profile));
|
||||
selections.push(Selection::Keyword(Keyword::Edit));
|
||||
selections.push(Selection::Payload(pubkey.hex()));
|
||||
}
|
||||
}
|
||||
|
||||
if selections.is_empty() {
|
||||
@@ -649,6 +654,15 @@ fn selections_to_route(selections: Vec<Selection>) -> Option<CleanIntermediaryRo
|
||||
Selection::Keyword(Keyword::DeckAuthor) => Some(CleanIntermediaryRoute::ToTimeline(
|
||||
TimelineKind::profile(PubkeySource::DeckAuthor),
|
||||
)),
|
||||
Selection::Keyword(Keyword::Edit) => {
|
||||
if let Selection::Payload(hex) = selections.get(2)? {
|
||||
Some(CleanIntermediaryRoute::ToRoute(Route::EditProfile(
|
||||
Pubkey::from_hex(hex.as_str()).ok()?,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Selection::Keyword(Keyword::Universe) => {
|
||||
|
||||
@@ -241,10 +241,9 @@ impl<'a> TitleNeedsDb<'a> {
|
||||
let pubkey = pubkey_source.to_pubkey(deck_author);
|
||||
let profile = ndb.get_profile_by_pubkey(txn, pubkey);
|
||||
let m_name = profile
|
||||
.ok()
|
||||
.as_ref()
|
||||
.and_then(|p| crate::profile::get_profile_name(p))
|
||||
.map(|display_name| display_name.username());
|
||||
.ok()
|
||||
.map(|p| crate::profile::get_display_name(Some(p)).name());
|
||||
|
||||
m_name.unwrap_or("Profile")
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
draft::Drafts,
|
||||
nav::RenderNavAction,
|
||||
notes_holder::NotesHolderStorage,
|
||||
profile::Profile,
|
||||
profile::{Profile, ProfileAction},
|
||||
thread::Thread,
|
||||
timeline::{TimelineId, TimelineKind},
|
||||
ui::{
|
||||
@@ -117,6 +117,7 @@ pub fn render_timeline_route(
|
||||
|
||||
TimelineRoute::Profile(pubkey) => render_profile_route(
|
||||
&pubkey,
|
||||
accounts,
|
||||
ndb,
|
||||
profiles,
|
||||
img_cache,
|
||||
@@ -155,6 +156,7 @@ pub fn render_timeline_route(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_profile_route(
|
||||
pubkey: &Pubkey,
|
||||
accounts: &Accounts,
|
||||
ndb: &Ndb,
|
||||
profiles: &mut NotesHolderStorage<Profile>,
|
||||
img_cache: &mut ImageCache,
|
||||
@@ -163,8 +165,9 @@ pub fn render_profile_route(
|
||||
ui: &mut egui::Ui,
|
||||
is_muted: &MuteFun,
|
||||
) -> Option<RenderNavAction> {
|
||||
let note_action = ProfileView::new(
|
||||
let action = ProfileView::new(
|
||||
pubkey,
|
||||
accounts,
|
||||
col,
|
||||
profiles,
|
||||
ndb,
|
||||
@@ -174,5 +177,16 @@ pub fn render_profile_route(
|
||||
)
|
||||
.ui(ui, is_muted);
|
||||
|
||||
note_action.map(RenderNavAction::NoteAction)
|
||||
if let Some(action) = action {
|
||||
match action {
|
||||
ui::profile::ProfileViewAction::EditProfile => accounts
|
||||
.get_full(pubkey.bytes())
|
||||
.map(|kp| RenderNavAction::ProfileAction(ProfileAction::Edit(kp.to_full()))),
|
||||
ui::profile::ProfileViewAction::Note(note_action) => {
|
||||
Some(RenderNavAction::NoteAction(note_action))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,15 +245,7 @@ impl<'a> NavTitle<'a> {
|
||||
TimelineRoute::Quote(_note_id) => {}
|
||||
|
||||
TimelineRoute::Profile(pubkey) => {
|
||||
let txn = Transaction::new(self.ndb).unwrap();
|
||||
if let Some(pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) {
|
||||
ui.add(pfp);
|
||||
} else {
|
||||
ui.add(
|
||||
ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url())
|
||||
.size(pfp_size),
|
||||
);
|
||||
}
|
||||
self.show_profile(ui, pubkey, pfp_size);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -264,9 +256,23 @@ impl<'a> NavTitle<'a> {
|
||||
Route::Relays => {}
|
||||
Route::NewDeck => {}
|
||||
Route::EditDeck(_) => {}
|
||||
Route::EditProfile(pubkey) => {
|
||||
self.show_profile(ui, pubkey, pfp_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_profile(&mut self, ui: &mut egui::Ui, pubkey: &Pubkey, pfp_size: f32) {
|
||||
let txn = Transaction::new(self.ndb).unwrap();
|
||||
if let Some(pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) {
|
||||
ui.add(pfp);
|
||||
} else {
|
||||
ui.add(
|
||||
ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
fn title_label_value(title: &str) -> egui::Label {
|
||||
egui::Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style()))
|
||||
.selectable(false)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::actionbar::NoteAction;
|
||||
use crate::ui;
|
||||
use crate::{actionbar::NoteAction, profile::get_display_name};
|
||||
use egui::Sense;
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
@@ -79,12 +79,7 @@ fn mention_ui(
|
||||
ui.horizontal(|ui| {
|
||||
let profile = ndb.get_profile_by_pubkey(txn, pk).ok();
|
||||
|
||||
let name: String =
|
||||
if let Some(name) = profile.as_ref().and_then(crate::profile::get_profile_name) {
|
||||
format!("@{}", name.username())
|
||||
} else {
|
||||
"@???".to_string()
|
||||
};
|
||||
let name: String = format!("@{}", get_display_name(profile.as_ref()).name());
|
||||
|
||||
let resp = ui.add(
|
||||
egui::Label::new(egui::RichText::new(name).color(link_color).size(size))
|
||||
|
||||
@@ -16,6 +16,7 @@ pub use reply_description::reply_desc;
|
||||
|
||||
use crate::{
|
||||
actionbar::NoteAction,
|
||||
profile::get_display_name,
|
||||
ui::{self, View},
|
||||
};
|
||||
|
||||
@@ -25,7 +26,7 @@ use enostr::{NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, Note, NoteKey, Transaction};
|
||||
use notedeck::{CachedNote, ImageCache, NoteCache, NotedeckTextStyle};
|
||||
|
||||
use super::profile::preview::{get_display_name, one_line_display_name_widget};
|
||||
use super::profile::preview::one_line_display_name_widget;
|
||||
|
||||
pub struct NoteView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
|
||||
205
crates/notedeck_columns/src/ui/profile/edit.rs
Normal file
205
crates/notedeck_columns/src/ui/profile/edit.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use core::f32;
|
||||
|
||||
use egui::{vec2, Button, Layout, Margin, RichText, Rounding, ScrollArea, TextEdit};
|
||||
use notedeck::{ImageCache, NotedeckTextStyle};
|
||||
|
||||
use crate::{colors, profile_state::ProfileState};
|
||||
|
||||
use super::{banner, unwrap_profile_url, ProfilePic};
|
||||
|
||||
pub struct EditProfileView<'a> {
|
||||
state: &'a mut ProfileState,
|
||||
img_cache: &'a mut ImageCache,
|
||||
}
|
||||
|
||||
impl<'a> EditProfileView<'a> {
|
||||
pub fn new(state: &'a mut ProfileState, img_cache: &'a mut ImageCache) -> Self {
|
||||
Self { state, img_cache }
|
||||
}
|
||||
|
||||
// return true to save
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
|
||||
ScrollArea::vertical()
|
||||
.show(ui, |ui| {
|
||||
banner(ui, Some(&self.state.banner), 188.0);
|
||||
|
||||
let padding = 24.0;
|
||||
crate::ui::padding(padding, ui, |ui| {
|
||||
self.inner(ui, padding);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let mut save = false;
|
||||
crate::ui::padding(padding, ui, |ui| {
|
||||
ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui
|
||||
.add(button("Save changes", 119.0).fill(colors::PINK))
|
||||
.clicked()
|
||||
{
|
||||
save = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
save
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
fn inner(&mut self, ui: &mut egui::Ui, padding: f32) {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(0.0, 16.0);
|
||||
let mut pfp_rect = ui.available_rect_before_wrap();
|
||||
let size = 80.0;
|
||||
pfp_rect.set_width(size);
|
||||
pfp_rect.set_height(size);
|
||||
let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0))));
|
||||
|
||||
let pfp_url = unwrap_profile_url(if self.state.picture.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&self.state.picture)
|
||||
});
|
||||
ui.put(
|
||||
pfp_rect,
|
||||
ProfilePic::new(self.img_cache, pfp_url).size(size),
|
||||
);
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Display name"));
|
||||
ui.add(singleline_textedit(&mut self.state.display_name));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Username"));
|
||||
ui.add(singleline_textedit(&mut self.state.name));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Profile picture"));
|
||||
ui.add(multiline_textedit(&mut self.state.picture));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Banner"));
|
||||
ui.add(multiline_textedit(&mut self.state.banner));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("About"));
|
||||
ui.add(multiline_textedit(&mut self.state.about));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Website"));
|
||||
ui.add(singleline_textedit(&mut self.state.website));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Lightning network address (lud16)"));
|
||||
ui.add(multiline_textedit(&mut self.state.lud16));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("NIP-05 verification"));
|
||||
ui.add(singleline_textedit(&mut self.state.nip05));
|
||||
let split = &mut self.state.nip05.split('@');
|
||||
let prefix = split.next();
|
||||
let suffix = split.next();
|
||||
if let Some(prefix) = prefix {
|
||||
if let Some(suffix) = suffix {
|
||||
let use_domain = if let Some(f) = prefix.chars().next() {
|
||||
f == '_'
|
||||
} else {
|
||||
false
|
||||
};
|
||||
ui.colored_label(
|
||||
ui.visuals().noninteractive().fg_stroke.color,
|
||||
RichText::new(if use_domain {
|
||||
format!("\"{}\" will be used for verification", suffix)
|
||||
} else {
|
||||
format!(
|
||||
"\"{}\" at \"{}\" will be used for verification",
|
||||
prefix, suffix
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn label(text: &str) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
ui.label(RichText::new(text).font(NotedeckTextStyle::Body.get_bolded_font(ui.ctx())))
|
||||
}
|
||||
}
|
||||
|
||||
fn singleline_textedit(data: &mut String) -> impl egui::Widget + '_ {
|
||||
TextEdit::singleline(data)
|
||||
.min_size(vec2(0.0, 40.0))
|
||||
.vertical_align(egui::Align::Center)
|
||||
.margin(Margin::symmetric(12.0, 10.0))
|
||||
.desired_width(f32::INFINITY)
|
||||
}
|
||||
|
||||
fn multiline_textedit(data: &mut String) -> impl egui::Widget + '_ {
|
||||
TextEdit::multiline(data)
|
||||
// .min_size(vec2(0.0, 40.0))
|
||||
.vertical_align(egui::Align::TOP)
|
||||
.margin(Margin::symmetric(12.0, 10.0))
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(1)
|
||||
}
|
||||
|
||||
fn in_frame(ui: &mut egui::Ui, contents: impl FnOnce(&mut egui::Ui)) {
|
||||
egui::Frame::none().show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(0.0, 8.0);
|
||||
contents(ui);
|
||||
});
|
||||
}
|
||||
|
||||
fn button(text: &str, width: f32) -> egui::Button<'static> {
|
||||
Button::new(text)
|
||||
.rounding(Rounding::same(8.0))
|
||||
.min_size(vec2(width, 40.0))
|
||||
}
|
||||
|
||||
mod preview {
|
||||
use notedeck::App;
|
||||
|
||||
use crate::{
|
||||
profile_state::ProfileState,
|
||||
test_data,
|
||||
ui::{Preview, PreviewConfig},
|
||||
};
|
||||
|
||||
use super::EditProfileView;
|
||||
|
||||
pub struct EditProfilePreivew {
|
||||
state: ProfileState,
|
||||
}
|
||||
|
||||
impl Default for EditProfilePreivew {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: ProfileState::from_profile(&test_data::test_profile_record()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App for EditProfilePreivew {
|
||||
fn update(&mut self, ctx: &mut notedeck::AppContext<'_>, ui: &mut egui::Ui) {
|
||||
EditProfileView::new(&mut self.state, ctx.img_cache).ui(ui);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Preview for EditProfileView<'a> {
|
||||
type Prev = EditProfilePreivew;
|
||||
|
||||
fn preview(_cfg: PreviewConfig) -> Self::Prev {
|
||||
EditProfilePreivew::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
pub mod edit;
|
||||
pub mod picture;
|
||||
pub mod preview;
|
||||
|
||||
use crate::notes_holder::NotesHolder;
|
||||
use crate::profile::get_display_name;
|
||||
use crate::ui::note::NoteOptions;
|
||||
use egui::{ScrollArea, Widget};
|
||||
use crate::{colors, images};
|
||||
use crate::{notes_holder::NotesHolder, NostrName};
|
||||
pub use edit::EditProfileView;
|
||||
use egui::load::TexturePoll;
|
||||
use egui::{vec2, Color32, Label, Layout, Rect, RichText, Rounding, ScrollArea, Sense, Stroke};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use nostrdb::{Ndb, ProfileRecord, Transaction};
|
||||
pub use picture::ProfilePic;
|
||||
pub use preview::ProfilePreview;
|
||||
use tracing::error;
|
||||
@@ -13,10 +18,11 @@ use tracing::error;
|
||||
use crate::{actionbar::NoteAction, notes_holder::NotesHolderStorage, profile::Profile};
|
||||
|
||||
use super::timeline::{tabs_ui, TimelineTabView};
|
||||
use notedeck::{ImageCache, MuteFun, NoteCache};
|
||||
use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, NotedeckTextStyle};
|
||||
|
||||
pub struct ProfileView<'a> {
|
||||
pubkey: &'a Pubkey,
|
||||
accounts: &'a Accounts,
|
||||
col_id: usize,
|
||||
profiles: &'a mut NotesHolderStorage<Profile>,
|
||||
note_options: NoteOptions,
|
||||
@@ -25,9 +31,16 @@ pub struct ProfileView<'a> {
|
||||
img_cache: &'a mut ImageCache,
|
||||
}
|
||||
|
||||
pub enum ProfileViewAction {
|
||||
EditProfile,
|
||||
Note(NoteAction),
|
||||
}
|
||||
|
||||
impl<'a> ProfileView<'a> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
pubkey: &'a Pubkey,
|
||||
accounts: &'a Accounts,
|
||||
col_id: usize,
|
||||
profiles: &'a mut NotesHolderStorage<Profile>,
|
||||
ndb: &'a Ndb,
|
||||
@@ -37,6 +50,7 @@ impl<'a> ProfileView<'a> {
|
||||
) -> Self {
|
||||
ProfileView {
|
||||
pubkey,
|
||||
accounts,
|
||||
col_id,
|
||||
profiles,
|
||||
ndb,
|
||||
@@ -46,15 +60,18 @@ impl<'a> ProfileView<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, is_muted: &MuteFun) -> Option<NoteAction> {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, is_muted: &MuteFun) -> Option<ProfileViewAction> {
|
||||
let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey));
|
||||
|
||||
ScrollArea::vertical()
|
||||
.id_salt(scroll_id)
|
||||
.show(ui, |ui| {
|
||||
let mut action = None;
|
||||
let txn = Transaction::new(self.ndb).expect("txn");
|
||||
if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, self.pubkey.bytes()) {
|
||||
ProfilePreview::new(&profile, self.img_cache).ui(ui);
|
||||
if self.profile_body(ui, profile) {
|
||||
action = Some(ProfileViewAction::EditProfile);
|
||||
}
|
||||
}
|
||||
let profile = self
|
||||
.profiles
|
||||
@@ -77,7 +94,7 @@ impl<'a> ProfileView<'a> {
|
||||
|
||||
let reversed = false;
|
||||
|
||||
TimelineTabView::new(
|
||||
if let Some(note_action) = TimelineTabView::new(
|
||||
profile.timeline.current_view(),
|
||||
reversed,
|
||||
self.note_options,
|
||||
@@ -87,7 +104,327 @@ impl<'a> ProfileView<'a> {
|
||||
self.img_cache,
|
||||
)
|
||||
.show(ui)
|
||||
{
|
||||
action = Some(ProfileViewAction::Note(note_action));
|
||||
}
|
||||
action
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
fn profile_body(&mut self, ui: &mut egui::Ui, profile: ProfileRecord<'_>) -> bool {
|
||||
let mut action = false;
|
||||
ui.vertical(|ui| {
|
||||
banner(
|
||||
ui,
|
||||
profile.record().profile().and_then(|p| p.banner()),
|
||||
120.0,
|
||||
);
|
||||
|
||||
let padding = 12.0;
|
||||
crate::ui::padding(padding, ui, |ui| {
|
||||
let mut pfp_rect = ui.available_rect_before_wrap();
|
||||
let size = 80.0;
|
||||
pfp_rect.set_width(size);
|
||||
pfp_rect.set_height(size);
|
||||
let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0))));
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.put(
|
||||
pfp_rect,
|
||||
ProfilePic::new(self.img_cache, get_profile_url(Some(&profile))).size(size),
|
||||
);
|
||||
|
||||
if ui.add(copy_key_widget(&pfp_rect)).clicked() {
|
||||
ui.output_mut(|w| {
|
||||
w.copied_text = if let Some(bech) = self.pubkey.to_bech() {
|
||||
bech
|
||||
} else {
|
||||
error!("Could not convert Pubkey to bech");
|
||||
String::new()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if self.accounts.contains_full_kp(self.pubkey) {
|
||||
ui.with_layout(Layout::right_to_left(egui::Align::Max), |ui| {
|
||||
if ui.add(edit_profile_button()).clicked() {
|
||||
action = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(18.0);
|
||||
|
||||
ui.add(display_name_widget(get_display_name(Some(&profile)), false));
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.add(about_section_widget(&profile));
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if let Some(website_url) = profile
|
||||
.record()
|
||||
.profile()
|
||||
.and_then(|p| p.website())
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
handle_link(ui, website_url);
|
||||
}
|
||||
|
||||
if let Some(lud16) = profile
|
||||
.record()
|
||||
.profile()
|
||||
.and_then(|p| p.lud16())
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
handle_lud16(ui, lud16);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_link(ui: &mut egui::Ui, website_url: &str) {
|
||||
ui.image(egui::include_image!(
|
||||
"../../../../../assets/icons/links_4x.png"
|
||||
));
|
||||
if ui
|
||||
.label(RichText::new(website_url).color(colors::PINK))
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.interact(Sense::click())
|
||||
.clicked()
|
||||
{
|
||||
if let Err(e) = open::that(website_url) {
|
||||
error!("Failed to open URL {} because: {}", website_url, e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_lud16(ui: &mut egui::Ui, lud16: &str) {
|
||||
ui.image(egui::include_image!(
|
||||
"../../../../../assets/icons/zap_4x.png"
|
||||
));
|
||||
|
||||
let _ = ui.label(RichText::new(lud16).color(colors::PINK));
|
||||
}
|
||||
|
||||
fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ {
|
||||
|ui: &mut egui::Ui| -> egui::Response {
|
||||
let painter = ui.painter();
|
||||
let copy_key_rect = painter.round_rect_to_pixels(egui::Rect::from_center_size(
|
||||
pfp_rect.center_bottom(),
|
||||
egui::vec2(48.0, 28.0),
|
||||
));
|
||||
let resp = ui.interact(
|
||||
copy_key_rect,
|
||||
ui.id().with("custom_painter"),
|
||||
Sense::click(),
|
||||
);
|
||||
|
||||
let copy_key_rounding = Rounding::same(100.0);
|
||||
let fill_color = if resp.hovered() {
|
||||
ui.visuals().widgets.inactive.weak_bg_fill
|
||||
} else {
|
||||
ui.visuals().noninteractive().bg_stroke.color
|
||||
};
|
||||
painter.rect_filled(copy_key_rect, copy_key_rounding, fill_color);
|
||||
|
||||
let stroke_color = ui.visuals().widgets.inactive.weak_bg_fill;
|
||||
painter.rect_stroke(
|
||||
copy_key_rect.shrink(1.0),
|
||||
copy_key_rounding,
|
||||
Stroke::new(1.0, stroke_color),
|
||||
);
|
||||
egui::Image::new(egui::include_image!(
|
||||
"../../../../../assets/icons/key_4x.png"
|
||||
))
|
||||
.paint_at(
|
||||
ui,
|
||||
painter.round_rect_to_pixels(egui::Rect::from_center_size(
|
||||
copy_key_rect.center(),
|
||||
egui::vec2(16.0, 16.0),
|
||||
)),
|
||||
);
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_profile_button() -> impl egui::Widget + 'static {
|
||||
|ui: &mut egui::Ui| -> egui::Response {
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(124.0, 32.0), Sense::click());
|
||||
let painter = ui.painter_at(rect);
|
||||
let rect = painter.round_rect_to_pixels(rect);
|
||||
|
||||
painter.rect_filled(
|
||||
rect,
|
||||
Rounding::same(8.0),
|
||||
if resp.hovered() {
|
||||
ui.visuals().widgets.active.bg_fill
|
||||
} else {
|
||||
ui.visuals().widgets.inactive.bg_fill
|
||||
},
|
||||
);
|
||||
painter.rect_stroke(
|
||||
rect.shrink(1.0),
|
||||
Rounding::same(8.0),
|
||||
if resp.hovered() {
|
||||
ui.visuals().widgets.active.bg_stroke
|
||||
} else {
|
||||
ui.visuals().widgets.inactive.bg_stroke
|
||||
},
|
||||
);
|
||||
|
||||
let edit_icon_size = vec2(16.0, 16.0);
|
||||
let galley = painter.layout(
|
||||
"Edit Profile".to_owned(),
|
||||
NotedeckTextStyle::Button.get_font_id(ui.ctx()),
|
||||
ui.visuals().text_color(),
|
||||
rect.width(),
|
||||
);
|
||||
|
||||
let space_between_icon_galley = 8.0;
|
||||
let half_icon_size = edit_icon_size.x / 2.0;
|
||||
let galley_rect = {
|
||||
let galley_rect = Rect::from_center_size(rect.center(), galley.rect.size());
|
||||
galley_rect.translate(vec2(half_icon_size + space_between_icon_galley / 2.0, 0.0))
|
||||
};
|
||||
|
||||
let edit_icon_rect = {
|
||||
let mut center = galley_rect.left_center();
|
||||
center.x -= half_icon_size + space_between_icon_galley;
|
||||
painter.round_rect_to_pixels(Rect::from_center_size(
|
||||
painter.round_pos_to_pixel_center(center),
|
||||
edit_icon_size,
|
||||
))
|
||||
};
|
||||
|
||||
painter.galley(galley_rect.left_top(), galley, Color32::WHITE);
|
||||
|
||||
egui::Image::new(egui::include_image!(
|
||||
"../../../../../assets/icons/edit_icon_4x_dark.png"
|
||||
))
|
||||
.paint_at(ui, edit_icon_rect);
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
let disp_resp = name.display_name.map(|disp_name| {
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new(disp_name).text_style(NotedeckTextStyle::Heading3.text_style()),
|
||||
)
|
||||
.selectable(false),
|
||||
)
|
||||
});
|
||||
|
||||
let (username_resp, nip05_resp) = ui
|
||||
.horizontal(|ui| {
|
||||
let username_resp = name.username.map(|username| {
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new(format!("@{}", username))
|
||||
.size(16.0)
|
||||
.color(colors::MID_GRAY),
|
||||
)
|
||||
.selectable(false),
|
||||
)
|
||||
});
|
||||
|
||||
let nip05_resp = name.nip05.map(|nip05| {
|
||||
ui.image(egui::include_image!(
|
||||
"../../../../../assets/icons/verified_4x.png"
|
||||
));
|
||||
ui.add(Label::new(
|
||||
RichText::new(nip05).size(16.0).color(colors::TEAL),
|
||||
))
|
||||
});
|
||||
|
||||
(username_resp, nip05_resp)
|
||||
})
|
||||
.inner;
|
||||
|
||||
let resp = match (disp_resp, username_resp, nip05_resp) {
|
||||
(Some(disp), Some(username), Some(nip05)) => disp.union(username).union(nip05),
|
||||
(Some(disp), Some(username), None) => disp.union(username),
|
||||
(Some(disp), None, None) => disp,
|
||||
(None, Some(username), Some(nip05)) => username.union(nip05),
|
||||
(None, Some(username), None) => username,
|
||||
_ => ui.add(Label::new(RichText::new(name.name()))),
|
||||
};
|
||||
|
||||
if add_placeholder_space {
|
||||
ui.add_space(16.0);
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str {
|
||||
unwrap_profile_url(profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())))
|
||||
}
|
||||
|
||||
pub fn unwrap_profile_url(maybe_url: Option<&str>) -> &str {
|
||||
if let Some(url) = maybe_url {
|
||||
url
|
||||
} else {
|
||||
ProfilePic::no_pfp_url()
|
||||
}
|
||||
}
|
||||
|
||||
fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
move |ui: &mut egui::Ui| {
|
||||
if let Some(about) = profile.record().profile().and_then(|p| p.about()) {
|
||||
let resp = ui.label(about);
|
||||
ui.add_space(8.0);
|
||||
resp
|
||||
} else {
|
||||
// need any Response so we dont need an Option
|
||||
ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn banner_texture(ui: &mut egui::Ui, banner_url: &str) -> Option<egui::load::SizedTexture> {
|
||||
// TODO: cache banner
|
||||
if !banner_url.is_empty() {
|
||||
let texture_load_res =
|
||||
egui::Image::new(banner_url).load_for_size(ui.ctx(), ui.available_size());
|
||||
if let Ok(texture_poll) = texture_load_res {
|
||||
match texture_poll {
|
||||
TexturePoll::Pending { .. } => {}
|
||||
TexturePoll::Ready { texture, .. } => return Some(texture),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn banner(ui: &mut egui::Ui, banner_url: Option<&str>, height: f32) -> egui::Response {
|
||||
ui.add_sized([ui.available_size().x, height], |ui: &mut egui::Ui| {
|
||||
banner_url
|
||||
.and_then(|url| banner_texture(ui, url))
|
||||
.map(|texture| {
|
||||
images::aspect_fill(
|
||||
ui,
|
||||
Sense::hover(),
|
||||
texture.id,
|
||||
texture.size.x / texture.size.y,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| ui.label(""))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::ui::ProfilePic;
|
||||
use crate::{colors, images, DisplayName};
|
||||
use egui::load::TexturePoll;
|
||||
use egui::{Frame, Label, RichText, Sense, Widget};
|
||||
use crate::NostrName;
|
||||
use egui::{Frame, Label, RichText, Widget};
|
||||
use egui_extras::Size;
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, ProfileRecord, Transaction};
|
||||
use nostrdb::ProfileRecord;
|
||||
|
||||
use notedeck::{ImageCache, NotedeckTextStyle, UserAccount};
|
||||
|
||||
use super::{about_section_widget, banner, display_name_widget, get_display_name, get_profile_url};
|
||||
|
||||
pub struct ProfilePreview<'a, 'cache> {
|
||||
profile: &'a ProfileRecord<'a>,
|
||||
cache: &'cache mut ImageCache,
|
||||
@@ -28,41 +28,6 @@ impl<'a, 'cache> ProfilePreview<'a, 'cache> {
|
||||
self.banner_height = size;
|
||||
}
|
||||
|
||||
fn banner_texture(
|
||||
ui: &mut egui::Ui,
|
||||
profile: &ProfileRecord<'_>,
|
||||
) -> Option<egui::load::SizedTexture> {
|
||||
// TODO: cache banner
|
||||
let banner = profile.record().profile().and_then(|p| p.banner());
|
||||
|
||||
if let Some(banner) = banner {
|
||||
let texture_load_res =
|
||||
egui::Image::new(banner).load_for_size(ui.ctx(), ui.available_size());
|
||||
if let Ok(texture_poll) = texture_load_res {
|
||||
match texture_poll {
|
||||
TexturePoll::Pending { .. } => {}
|
||||
TexturePoll::Ready { texture, .. } => return Some(texture),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn banner(ui: &mut egui::Ui, profile: &ProfileRecord<'_>) -> egui::Response {
|
||||
if let Some(texture) = Self::banner_texture(ui, profile) {
|
||||
images::aspect_fill(
|
||||
ui,
|
||||
Sense::hover(),
|
||||
texture.id,
|
||||
texture.size.x / texture.size.y,
|
||||
)
|
||||
} else {
|
||||
// TODO: default banner texture
|
||||
ui.label("")
|
||||
}
|
||||
}
|
||||
|
||||
fn body(self, ui: &mut egui::Ui) {
|
||||
let padding = 12.0;
|
||||
crate::ui::padding(padding, ui, |ui| {
|
||||
@@ -88,9 +53,11 @@ impl<'a, 'cache> ProfilePreview<'a, 'cache> {
|
||||
impl egui::Widget for ProfilePreview<'_, '_> {
|
||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_sized([ui.available_size().x, 80.0], |ui: &mut egui::Ui| {
|
||||
ProfilePreview::banner(ui, self.profile)
|
||||
});
|
||||
banner(
|
||||
ui,
|
||||
self.profile.record().profile().and_then(|p| p.banner()),
|
||||
80.0,
|
||||
);
|
||||
|
||||
self.body(ui);
|
||||
})
|
||||
@@ -183,22 +150,6 @@ mod previews {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_display_name<'a>(profile: Option<&ProfileRecord<'a>>) -> DisplayName<'a> {
|
||||
if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) {
|
||||
name
|
||||
} else {
|
||||
DisplayName::One("??")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str {
|
||||
if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) {
|
||||
url
|
||||
} else {
|
||||
ProfilePic::no_pfp_url()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_profile_url_owned(profile: Option<ProfileRecord<'_>>) -> &str {
|
||||
if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) {
|
||||
url
|
||||
@@ -223,106 +174,19 @@ pub fn get_account_url<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
fn display_name_widget(
|
||||
display_name: DisplayName<'_>,
|
||||
add_placeholder_space: bool,
|
||||
) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| match display_name {
|
||||
DisplayName::One(n) => {
|
||||
let name_response = ui.add(
|
||||
Label::new(RichText::new(n).text_style(NotedeckTextStyle::Heading3.text_style()))
|
||||
.selectable(false),
|
||||
);
|
||||
if add_placeholder_space {
|
||||
ui.add_space(16.0);
|
||||
}
|
||||
name_response
|
||||
}
|
||||
|
||||
DisplayName::Both {
|
||||
display_name,
|
||||
username,
|
||||
} => {
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new(display_name)
|
||||
.text_style(NotedeckTextStyle::Heading3.text_style()),
|
||||
)
|
||||
.selectable(false),
|
||||
);
|
||||
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new(format!("@{}", username))
|
||||
.size(12.0)
|
||||
.color(colors::MID_GRAY),
|
||||
)
|
||||
.selectable(false),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn one_line_display_name_widget<'a>(
|
||||
visuals: &egui::Visuals,
|
||||
display_name: DisplayName<'a>,
|
||||
display_name: NostrName<'a>,
|
||||
style: NotedeckTextStyle,
|
||||
) -> impl egui::Widget + 'a {
|
||||
let text_style = style.text_style();
|
||||
let color = visuals.noninteractive().fg_stroke.color;
|
||||
|
||||
move |ui: &mut egui::Ui| match display_name {
|
||||
DisplayName::One(n) => ui.label(RichText::new(n).text_style(text_style).color(color)),
|
||||
|
||||
DisplayName::Both {
|
||||
display_name,
|
||||
username: _,
|
||||
} => ui.label(
|
||||
RichText::new(display_name)
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
ui.label(
|
||||
RichText::new(display_name.name())
|
||||
.text_style(text_style)
|
||||
.color(color),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
move |ui: &mut egui::Ui| {
|
||||
if let Some(about) = profile.record().profile().and_then(|p| p.about()) {
|
||||
ui.label(about)
|
||||
} else {
|
||||
// need any Response so we dont need an Option
|
||||
ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_display_name_as_string<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str {
|
||||
let display_name = get_display_name(profile);
|
||||
match display_name {
|
||||
DisplayName::One(n) => n,
|
||||
DisplayName::Both { display_name, .. } => display_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_profile_displayname_string<'a>(txn: &'a Transaction, ndb: &Ndb, pk: &Pubkey) -> &'a str {
|
||||
let profile = ndb.get_profile_by_pubkey(txn, pk.bytes()).ok();
|
||||
get_display_name_as_string(profile.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_note_users_displayname_string<'a>(
|
||||
txn: &'a Transaction,
|
||||
ndb: &Ndb,
|
||||
id: &NoteId,
|
||||
) -> &'a str {
|
||||
let note = ndb.get_note_by_id(txn, id.bytes());
|
||||
let profile = if let Ok(note) = note {
|
||||
ndb.get_profile_by_pubkey(txn, note.pubkey()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
get_display_name_as_string(profile.as_ref())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use enostr::Pubkey;
|
||||
|
||||
use crate::deck_state::DeckState;
|
||||
use crate::login_manager::AcquireKeyState;
|
||||
use crate::profile_state::ProfileState;
|
||||
|
||||
/// Various state for views
|
||||
#[derive(Default)]
|
||||
@@ -10,6 +13,7 @@ pub struct ViewState {
|
||||
pub id_to_deck_state: HashMap<egui::Id, DeckState>,
|
||||
pub id_state_map: HashMap<egui::Id, AcquireKeyState>,
|
||||
pub id_string_map: HashMap<egui::Id, String>,
|
||||
pub pubkey_to_profile_state: HashMap<Pubkey, ProfileState>,
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
|
||||
Reference in New Issue
Block a user