Merge follow/unfollow from kernel
Jakub Gladysz (1):
ui: add follow button
kernelkind (14):
bump nostrdb
move polling responsibility to `AccountData`
`AccountData`: decouple query from constructor
add constructor for `AccountData`
add `Contacts`
use `Contacts` in `AccountData`
expose `AccountSubs`
Unify sub for contacts in accounts & timeline
move `styled_button_toggleable` to notedeck_ui
construct NoteBuilder from existing note
send kind 3 event
add actions for follow/unfollow
add UI for (un)follow
send contact list event on account creation
This commit is contained in:
@@ -5,6 +5,7 @@ use notedeck::{Accounts, AppContext, SingleUnkIdAction, UnknownIds};
|
||||
|
||||
use crate::app::get_active_columns_mut;
|
||||
use crate::decks::DecksCache;
|
||||
use crate::profile::send_new_contact_list;
|
||||
use crate::{
|
||||
login_manager::AcquireKeyState,
|
||||
route::Route,
|
||||
@@ -149,18 +150,14 @@ pub fn process_login_view_response(
|
||||
) -> AddAccountAction {
|
||||
let (r, pubkey) = match response {
|
||||
AccountLoginResponse::CreateNew => {
|
||||
let kp = FullKeypair::generate().to_keypair();
|
||||
let kp = FullKeypair::generate();
|
||||
let pubkey = kp.pubkey;
|
||||
let txn = Transaction::new(app_ctx.ndb).expect("txn");
|
||||
(app_ctx.accounts.add_account(app_ctx.ndb, &txn, kp), pubkey)
|
||||
send_new_contact_list(kp.to_filled(), app_ctx.ndb, app_ctx.pool);
|
||||
(app_ctx.accounts.add_account(kp.to_keypair()), pubkey)
|
||||
}
|
||||
AccountLoginResponse::LoginWith(keypair) => {
|
||||
let pubkey = keypair.pubkey;
|
||||
let txn = Transaction::new(app_ctx.ndb).expect("txn");
|
||||
(
|
||||
app_ctx.accounts.add_account(app_ctx.ndb, &txn, keypair),
|
||||
pubkey,
|
||||
)
|
||||
(app_ctx.accounts.add_account(keypair), pubkey)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ use crate::{
|
||||
storage,
|
||||
subscriptions::{SubKind, Subscriptions},
|
||||
support::Support,
|
||||
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
|
||||
timeline::{
|
||||
self, fetch_contact_list, kind::ListKind, thread::Threads, TimelineCache, TimelineKind,
|
||||
},
|
||||
ui::{self, DesktopSidePanel, SidePanelAction},
|
||||
view_state::ViewState,
|
||||
Result,
|
||||
};
|
||||
|
||||
use notedeck::{
|
||||
ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, UnknownIds,
|
||||
ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, UnknownIds,
|
||||
};
|
||||
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
||||
|
||||
@@ -116,13 +118,21 @@ fn try_process_event(
|
||||
.accounts
|
||||
.send_initial_filters(app_ctx.pool, &ev.relay);
|
||||
|
||||
let data = app_ctx.accounts.get_subs();
|
||||
damus.subscriptions.subs.insert(
|
||||
data.contacts.remote.clone(),
|
||||
SubKind::FetchingContactList(TimelineKind::List(ListKind::Contact(
|
||||
*app_ctx.accounts.selected_account_pubkey(),
|
||||
))),
|
||||
);
|
||||
|
||||
timeline::send_initial_timeline_filters(
|
||||
app_ctx.ndb,
|
||||
damus.since_optimize,
|
||||
&mut damus.timeline_cache,
|
||||
&mut damus.subscriptions,
|
||||
app_ctx.pool,
|
||||
&ev.relay,
|
||||
app_ctx.accounts,
|
||||
);
|
||||
}
|
||||
// TODO: handle reconnects
|
||||
@@ -248,44 +258,11 @@ fn handle_eose(
|
||||
}
|
||||
|
||||
SubKind::FetchingContactList(timeline_uid) => {
|
||||
let timeline = if let Some(tl) = timeline_cache.timelines.get_mut(timeline_uid) {
|
||||
tl
|
||||
} else {
|
||||
error!(
|
||||
"timeline uid:{} not found for FetchingContactList",
|
||||
timeline_uid
|
||||
);
|
||||
let Some(timeline) = timeline_cache.timelines.get_mut(timeline_uid) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let filter_state = timeline.filter.get_mut(relay_url);
|
||||
|
||||
// If this request was fetching a contact list, our filter
|
||||
// state should be "FetchingRemote". We look at the local
|
||||
// subscription for that filter state and get the subscription id
|
||||
let local_sub = if let FilterState::FetchingRemote(unisub) = filter_state {
|
||||
unisub.local
|
||||
} else {
|
||||
// TODO: we could have multiple contact list results, we need
|
||||
// to check to see if this one is newer and use that instead
|
||||
warn!(
|
||||
"Expected timeline to have FetchingRemote state but was {:?}",
|
||||
timeline.filter
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(
|
||||
"got contact list from {}, updating filter_state to got_remote",
|
||||
relay_url
|
||||
);
|
||||
|
||||
// We take the subscription id and pass it to the new state of
|
||||
// "GotRemote". This will let future frames know that it can try
|
||||
// to look for the contact list in nostrdb.
|
||||
timeline
|
||||
.filter
|
||||
.set_relay_state(relay_url.to_string(), FilterState::got_remote(local_sub));
|
||||
fetch_contact_list(relay_url, timeline, ctx.accounts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,7 +724,13 @@ fn timelines_view(
|
||||
let mut save_cols = false;
|
||||
if let Some(action) = side_panel_action {
|
||||
save_cols = save_cols
|
||||
|| action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx, ui.ctx());
|
||||
|| action.process(
|
||||
&mut app.timeline_cache,
|
||||
&mut app.decks_cache,
|
||||
&mut app.subscriptions,
|
||||
ctx,
|
||||
ui.ctx(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut app_action: Option<AppAction> = None;
|
||||
|
||||
@@ -7,9 +7,11 @@ use crate::{
|
||||
profile::{ProfileAction, SaveProfileChanges},
|
||||
profile_state::ProfileState,
|
||||
route::{Route, Router, SingletonRouter},
|
||||
subscriptions::{SubKind, Subscriptions},
|
||||
timeline::{
|
||||
kind::ListKind,
|
||||
route::{render_thread_route, render_timeline_route},
|
||||
TimelineCache,
|
||||
TimelineCache, TimelineKind,
|
||||
},
|
||||
ui::{
|
||||
self,
|
||||
@@ -72,18 +74,31 @@ impl SwitchingAction {
|
||||
&self,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
decks_cache: &mut DecksCache,
|
||||
subs: &mut Subscriptions,
|
||||
ctx: &mut AppContext<'_>,
|
||||
ui_ctx: &egui::Context,
|
||||
) -> bool {
|
||||
match &self {
|
||||
SwitchingAction::Accounts(account_action) => match account_action {
|
||||
AccountsAction::Switch(switch_action) => {
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
ctx.accounts.select_account(
|
||||
&switch_action.switch_to,
|
||||
ctx.ndb,
|
||||
&txn,
|
||||
ctx.pool,
|
||||
ui_ctx,
|
||||
);
|
||||
|
||||
let new_subs = ctx.accounts.get_subs();
|
||||
|
||||
subs.subs.insert(
|
||||
new_subs.contacts.remote.clone(),
|
||||
SubKind::FetchingContactList(TimelineKind::List(ListKind::Contact(
|
||||
*ctx.accounts.selected_account_pubkey(),
|
||||
))),
|
||||
);
|
||||
|
||||
// pop nav after switch
|
||||
get_active_columns_mut(ctx.accounts, decks_cache)
|
||||
.column_mut(switch_action.source_column)
|
||||
@@ -378,6 +393,7 @@ fn process_render_nav_action(
|
||||
if switching_action.process(
|
||||
&mut app.timeline_cache,
|
||||
&mut app.decks_cache,
|
||||
&mut app.subscriptions,
|
||||
ctx,
|
||||
ui.ctx(),
|
||||
) {
|
||||
@@ -390,6 +406,7 @@ fn process_render_nav_action(
|
||||
&mut app.view_state.pubkey_to_profile_state,
|
||||
ctx.ndb,
|
||||
ctx.pool,
|
||||
ctx.accounts,
|
||||
),
|
||||
RenderNavAction::WalletAction(wallet_action) => {
|
||||
wallet_action.process(ctx.accounts, ctx.global_wallet)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use enostr::{FullKeypair, Pubkey, RelayPool};
|
||||
use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder};
|
||||
use enostr::{FilledKeypair, FullKeypair, Pubkey, RelayPool};
|
||||
use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder, Transaction};
|
||||
|
||||
use notedeck::{Accounts, ContactState};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{nav::RouterAction, profile_state::ProfileState, route::Route};
|
||||
@@ -37,6 +38,8 @@ fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
|
||||
pub enum ProfileAction {
|
||||
Edit(FullKeypair),
|
||||
SaveChanges(SaveProfileChanges),
|
||||
Follow(Pubkey),
|
||||
Unfollow(Pubkey),
|
||||
}
|
||||
|
||||
impl ProfileAction {
|
||||
@@ -45,6 +48,7 @@ impl ProfileAction {
|
||||
state_map: &mut HashMap<Pubkey, ProfileState>,
|
||||
ndb: &Ndb,
|
||||
pool: &mut RelayPool,
|
||||
accounts: &Accounts,
|
||||
) -> Option<RouterAction> {
|
||||
match self {
|
||||
ProfileAction::Edit(kp) => Some(RouterAction::route_to(Route::EditProfile(kp.pubkey))),
|
||||
@@ -62,6 +66,157 @@ impl ProfileAction {
|
||||
|
||||
Some(RouterAction::GoBack)
|
||||
}
|
||||
ProfileAction::Follow(target_key) => {
|
||||
Self::send_follow_user_event(ndb, pool, accounts, target_key);
|
||||
None
|
||||
}
|
||||
ProfileAction::Unfollow(target_key) => {
|
||||
Self::send_unfollow_user_event(ndb, pool, accounts, target_key);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_follow_user_event(
|
||||
ndb: &Ndb,
|
||||
pool: &mut RelayPool,
|
||||
accounts: &Accounts,
|
||||
target_key: &Pubkey,
|
||||
) {
|
||||
send_kind_3_event(ndb, pool, accounts, FollowAction::Follow(target_key));
|
||||
}
|
||||
|
||||
fn send_unfollow_user_event(
|
||||
ndb: &Ndb,
|
||||
pool: &mut RelayPool,
|
||||
accounts: &Accounts,
|
||||
target_key: &Pubkey,
|
||||
) {
|
||||
send_kind_3_event(ndb, pool, accounts, FollowAction::Unfollow(target_key));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn builder_from_note<F>(note: Note<'_>, skip_tag: Option<F>) -> NoteBuilder<'_>
|
||||
where
|
||||
F: Fn(&nostrdb::Tag<'_>) -> bool,
|
||||
{
|
||||
let mut builder = NoteBuilder::new();
|
||||
|
||||
builder = builder.content(note.content());
|
||||
builder = builder.options(NoteBuildOptions::default());
|
||||
builder = builder.kind(note.kind());
|
||||
builder = builder.pubkey(note.pubkey());
|
||||
|
||||
for tag in note.tags() {
|
||||
if let Some(skip) = &skip_tag {
|
||||
if skip(&tag) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
builder = builder.start_tag();
|
||||
for tag_item in tag {
|
||||
builder = match tag_item.variant() {
|
||||
nostrdb::NdbStrVariant::Id(i) => builder.tag_id(i),
|
||||
nostrdb::NdbStrVariant::Str(s) => builder.tag_str(s),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
enum FollowAction<'a> {
|
||||
Follow(&'a Pubkey),
|
||||
Unfollow(&'a Pubkey),
|
||||
}
|
||||
|
||||
fn send_kind_3_event(ndb: &Ndb, pool: &mut RelayPool, accounts: &Accounts, action: FollowAction) {
|
||||
let Some(kp) = accounts.get_selected_account().key.to_full() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let txn = Transaction::new(ndb).expect("txn");
|
||||
|
||||
let ContactState::Received {
|
||||
contacts: _,
|
||||
note_key,
|
||||
} = accounts.get_selected_account().data.contacts.get_state()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let contact_note = match ndb.get_note_by_key(&txn, *note_key).ok() {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
tracing::error!("Somehow we are in state ContactState::Received but the contact note key doesn't exist");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if contact_note.kind() != 3 {
|
||||
tracing::error!("Something very wrong just occured. The key for the supposed contact note yielded a note which was not a contact...");
|
||||
return;
|
||||
}
|
||||
|
||||
let builder = match action {
|
||||
FollowAction::Follow(pubkey) => {
|
||||
builder_from_note(contact_note, None::<fn(&nostrdb::Tag<'_>) -> bool>)
|
||||
.start_tag()
|
||||
.tag_str("p")
|
||||
.tag_str(&pubkey.hex())
|
||||
}
|
||||
FollowAction::Unfollow(pubkey) => builder_from_note(
|
||||
contact_note,
|
||||
Some(|tag: &nostrdb::Tag<'_>| {
|
||||
if tag.count() < 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some("p") = tag.get_str(0) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(cur_val) = tag.get_id(1) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
cur_val == pubkey.bytes()
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
send_note_builder(builder, ndb, pool, kp);
|
||||
}
|
||||
|
||||
fn send_note_builder(builder: NoteBuilder, ndb: &Ndb, pool: &mut RelayPool, kp: FilledKeypair) {
|
||||
let note = builder
|
||||
.sign(&kp.secret_key.secret_bytes())
|
||||
.build()
|
||||
.expect("build note");
|
||||
|
||||
let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
|
||||
|
||||
let _ = ndb.process_event_with(
|
||||
raw_msg.as_str(),
|
||||
nostrdb::IngestMetadata::new().client(true),
|
||||
);
|
||||
info!("sending {}", raw_msg);
|
||||
pool.send(&enostr::ClientMessage::raw(raw_msg));
|
||||
}
|
||||
|
||||
pub fn send_new_contact_list(kp: FilledKeypair, ndb: &Ndb, pool: &mut RelayPool) {
|
||||
let builder = construct_new_contact_list(kp.pubkey);
|
||||
|
||||
send_note_builder(builder, ndb, pool, kp);
|
||||
}
|
||||
|
||||
fn construct_new_contact_list<'a>(pk: &'a Pubkey) -> NoteBuilder<'a> {
|
||||
NoteBuilder::new()
|
||||
.content("")
|
||||
.kind(3)
|
||||
.options(NoteBuildOptions::default())
|
||||
.start_tag()
|
||||
.tag_str("p")
|
||||
.tag_str(&pk.hex())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use notedeck::{
|
||||
filter, CachedNote, FilterError, FilterState, FilterStates, NoteCache, NoteRef, UnknownIds,
|
||||
filter, Accounts, CachedNote, FilterError, FilterState, FilterStates, NoteCache, NoteRef,
|
||||
UnknownIds,
|
||||
};
|
||||
|
||||
use egui_virtual_list::VirtualList;
|
||||
@@ -474,6 +475,7 @@ pub fn setup_new_timeline(
|
||||
pool: &mut RelayPool,
|
||||
note_cache: &mut NoteCache,
|
||||
since_optimize: bool,
|
||||
accounts: &Accounts,
|
||||
) {
|
||||
// if we're ready, setup local subs
|
||||
if is_timeline_ready(ndb, pool, note_cache, timeline) {
|
||||
@@ -483,7 +485,7 @@ pub fn setup_new_timeline(
|
||||
}
|
||||
|
||||
for relay in &mut pool.relays {
|
||||
send_initial_timeline_filter(ndb, since_optimize, subs, relay, timeline);
|
||||
send_initial_timeline_filter(since_optimize, subs, relay, timeline, accounts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,29 +494,29 @@ pub fn setup_new_timeline(
|
||||
/// situations where you are adding a new timeline, use
|
||||
/// setup_new_timeline.
|
||||
pub fn send_initial_timeline_filters(
|
||||
ndb: &Ndb,
|
||||
since_optimize: bool,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
subs: &mut Subscriptions,
|
||||
pool: &mut RelayPool,
|
||||
relay_id: &str,
|
||||
accounts: &Accounts,
|
||||
) -> Option<()> {
|
||||
info!("Sending initial filters to {}", relay_id);
|
||||
let relay = &mut pool.relays.iter_mut().find(|r| r.url() == relay_id)?;
|
||||
|
||||
for (_kind, timeline) in timeline_cache.timelines.iter_mut() {
|
||||
send_initial_timeline_filter(ndb, since_optimize, subs, relay, timeline);
|
||||
send_initial_timeline_filter(since_optimize, subs, relay, timeline, accounts);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn send_initial_timeline_filter(
|
||||
ndb: &Ndb,
|
||||
can_since_optimize: bool,
|
||||
subs: &mut Subscriptions,
|
||||
relay: &mut PoolRelay,
|
||||
timeline: &mut Timeline,
|
||||
accounts: &Accounts,
|
||||
) {
|
||||
let filter_state = timeline.filter.get_mut(relay.url());
|
||||
|
||||
@@ -572,34 +574,27 @@ pub fn send_initial_timeline_filter(
|
||||
}
|
||||
|
||||
// we need some data first
|
||||
FilterState::NeedsRemote(filter) => {
|
||||
fetch_contact_list(filter.to_owned(), ndb, subs, relay, timeline)
|
||||
}
|
||||
FilterState::NeedsRemote(_filter) => fetch_contact_list(relay.url(), timeline, accounts),
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_contact_list(
|
||||
filter: Vec<Filter>,
|
||||
ndb: &Ndb,
|
||||
subs: &mut Subscriptions,
|
||||
relay: &mut PoolRelay,
|
||||
timeline: &mut Timeline,
|
||||
) {
|
||||
let sub_kind = SubKind::FetchingContactList(timeline.kind.clone());
|
||||
let sub_id = subscriptions::new_sub_id();
|
||||
let local_sub = ndb.subscribe(&filter).expect("sub");
|
||||
pub fn fetch_contact_list(relay_url: &str, timeline: &mut Timeline, accounts: &Accounts) {
|
||||
let account_subs = accounts.get_subs();
|
||||
let local = account_subs.contacts.local;
|
||||
|
||||
timeline.filter.set_relay_state(
|
||||
relay.url().to_string(),
|
||||
FilterState::fetching_remote(sub_id.clone(), local_sub),
|
||||
);
|
||||
let filter_state = match accounts.get_selected_account().data.contacts.get_state() {
|
||||
notedeck::ContactState::Unreceived => {
|
||||
FilterState::fetching_remote(account_subs.contacts.remote.clone(), local)
|
||||
}
|
||||
notedeck::ContactState::Received {
|
||||
contacts: _,
|
||||
note_key: _,
|
||||
} => FilterState::GotRemote(local),
|
||||
};
|
||||
|
||||
subs.subs.insert(sub_id.clone(), sub_kind);
|
||||
|
||||
info!("fetching contact list from {}", relay.url());
|
||||
if let Err(err) = relay.subscribe(sub_id, filter) {
|
||||
error!("error subscribing: {err}");
|
||||
}
|
||||
timeline
|
||||
.filter
|
||||
.set_relay_state(relay_url.to_owned(), filter_state);
|
||||
}
|
||||
|
||||
fn setup_initial_timeline(
|
||||
|
||||
@@ -116,7 +116,7 @@ pub fn render_profile_route(
|
||||
note_context: &mut NoteContext,
|
||||
jobs: &mut JobsCache,
|
||||
) -> Option<RenderNavAction> {
|
||||
let action = ProfileView::new(
|
||||
let profile_view = ProfileView::new(
|
||||
pubkey,
|
||||
accounts,
|
||||
col,
|
||||
@@ -128,7 +128,7 @@ pub fn render_profile_route(
|
||||
)
|
||||
.ui(ui);
|
||||
|
||||
if let Some(action) = action {
|
||||
if let Some(action) = profile_view {
|
||||
match action {
|
||||
ui::profile::ProfileViewAction::EditProfile => accounts
|
||||
.get_full(pubkey)
|
||||
@@ -136,6 +136,12 @@ pub fn render_profile_route(
|
||||
ui::profile::ProfileViewAction::Note(note_action) => {
|
||||
Some(RenderNavAction::NoteAction(note_action))
|
||||
}
|
||||
ui::profile::ProfileViewAction::Follow(target_key) => Some(
|
||||
RenderNavAction::ProfileAction(ProfileAction::Follow(target_key)),
|
||||
),
|
||||
ui::profile::ProfileViewAction::Unfollow(target_key) => Some(
|
||||
RenderNavAction::ProfileAction(ProfileAction::Unfollow(target_key)),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -623,6 +623,7 @@ pub fn render_add_column_routes(
|
||||
ctx.pool,
|
||||
ctx.note_cache,
|
||||
app.since_optimize,
|
||||
ctx.accounts,
|
||||
);
|
||||
|
||||
app.columns_mut(ctx.accounts)
|
||||
@@ -664,6 +665,7 @@ pub fn render_add_column_routes(
|
||||
ctx.pool,
|
||||
ctx.note_cache,
|
||||
app.since_optimize,
|
||||
ctx.accounts,
|
||||
);
|
||||
|
||||
app.columns_mut(ctx.accounts)
|
||||
|
||||
@@ -9,9 +9,10 @@ use nostrdb::{Ndb, ProfileRecord, Transaction};
|
||||
use notedeck::{
|
||||
fonts::get_font_size, get_profile_url, name::get_display_name, Images, NotedeckTextStyle,
|
||||
};
|
||||
use notedeck_ui::{app_images, colors, profile::display_name_widget, AnimationHelper, ProfilePic};
|
||||
|
||||
use crate::ui::widgets::styled_button_toggleable;
|
||||
use notedeck_ui::{
|
||||
app_images, colors, profile::display_name_widget, widgets::styled_button_toggleable,
|
||||
AnimationHelper, ProfilePic,
|
||||
};
|
||||
|
||||
pub struct CustomZapView<'a> {
|
||||
images: &'a mut Images,
|
||||
|
||||
@@ -4,6 +4,7 @@ pub use edit::EditProfileView;
|
||||
use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{ProfileRecord, Transaction};
|
||||
use notedeck_ui::profile::follow_button;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
@@ -11,8 +12,8 @@ use crate::{
|
||||
ui::timeline::{tabs_ui, TimelineTabView},
|
||||
};
|
||||
use notedeck::{
|
||||
name::get_display_name, profile::get_profile_url, Accounts, MuteFun, NoteAction, NoteContext,
|
||||
NotedeckTextStyle,
|
||||
name::get_display_name, profile::get_profile_url, Accounts, IsFollowing, MuteFun, NoteAction,
|
||||
NoteContext, NotedeckTextStyle,
|
||||
};
|
||||
use notedeck_ui::{
|
||||
app_images,
|
||||
@@ -35,6 +36,8 @@ pub struct ProfileView<'a, 'd> {
|
||||
pub enum ProfileViewAction {
|
||||
EditProfile,
|
||||
Note(NoteAction),
|
||||
Unfollow(Pubkey),
|
||||
Follow(Pubkey),
|
||||
}
|
||||
|
||||
impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
@@ -79,8 +82,8 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
.ndb
|
||||
.get_profile_by_pubkey(&txn, self.pubkey.bytes())
|
||||
{
|
||||
if self.profile_body(ui, profile) {
|
||||
action = Some(ProfileViewAction::EditProfile);
|
||||
if let Some(profile_view_action) = self.profile_body(ui, profile) {
|
||||
action = Some(profile_view_action);
|
||||
}
|
||||
}
|
||||
let profile_timeline = self
|
||||
@@ -131,8 +134,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
output.inner
|
||||
}
|
||||
|
||||
fn profile_body(&mut self, ui: &mut egui::Ui, profile: ProfileRecord<'_>) -> bool {
|
||||
let mut action = false;
|
||||
fn profile_body(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
profile: ProfileRecord<'_>,
|
||||
) -> Option<ProfileViewAction> {
|
||||
let mut action = None;
|
||||
ui.vertical(|ui| {
|
||||
banner(
|
||||
ui,
|
||||
@@ -169,13 +176,49 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
ui.ctx().copy_text(to_copy)
|
||||
}
|
||||
|
||||
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.with_layout(Layout::right_to_left(egui::Align::RIGHT), |ui| {
|
||||
ui.add_space(24.0);
|
||||
|
||||
let target_key = self.pubkey;
|
||||
let selected = self.accounts.get_selected_account();
|
||||
|
||||
let profile_type = if selected.key.secret_key.is_none() {
|
||||
ProfileType::ReadOnly
|
||||
} else if &selected.key.pubkey == self.pubkey {
|
||||
ProfileType::MyProfile
|
||||
} else {
|
||||
ProfileType::Followable(selected.is_following(target_key))
|
||||
};
|
||||
|
||||
match profile_type {
|
||||
ProfileType::MyProfile => {
|
||||
if ui.add(edit_profile_button()).clicked() {
|
||||
action = Some(ProfileViewAction::EditProfile);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ProfileType::Followable(is_following) => {
|
||||
let follow_button = ui.add(follow_button(is_following));
|
||||
|
||||
if follow_button.clicked() {
|
||||
action = match is_following {
|
||||
IsFollowing::Unknown => {
|
||||
// don't do anything, we don't have contact list
|
||||
None
|
||||
}
|
||||
|
||||
IsFollowing::Yes => {
|
||||
Some(ProfileViewAction::Unfollow(target_key.to_owned()))
|
||||
}
|
||||
|
||||
IsFollowing::No => {
|
||||
Some(ProfileViewAction::Follow(target_key.to_owned()))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
ProfileType::ReadOnly => {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(18.0);
|
||||
@@ -215,6 +258,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
}
|
||||
}
|
||||
|
||||
enum ProfileType {
|
||||
MyProfile,
|
||||
ReadOnly,
|
||||
Followable(IsFollowing),
|
||||
}
|
||||
|
||||
fn handle_link(ui: &mut egui::Ui, website_url: &str) {
|
||||
let img = if ui.visuals().dark_mode {
|
||||
app_images::link_image()
|
||||
|
||||
@@ -154,6 +154,9 @@ fn timeline_ui(
|
||||
error!("tried to render timeline in column, but timeline was missing");
|
||||
// TODO (jb55): render error when timeline is missing?
|
||||
// this shouldn't happen...
|
||||
//
|
||||
// NOTE (jb55): it can easily happen if you add a timeline column without calling
|
||||
// add_new_timeline_column, since that sets up the initial subs, etc
|
||||
return None;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,49 +1,7 @@
|
||||
use egui::{Button, Widget};
|
||||
use notedeck::NotedeckTextStyle;
|
||||
use egui::Widget;
|
||||
use notedeck_ui::widgets::styled_button_toggleable;
|
||||
|
||||
/// Sized and styled to match the figma design
|
||||
pub fn styled_button(text: &str, fill_color: egui::Color32) -> impl Widget + '_ {
|
||||
styled_button_toggleable(text, fill_color, true)
|
||||
}
|
||||
|
||||
pub fn styled_button_toggleable(
|
||||
text: &str,
|
||||
fill_color: egui::Color32,
|
||||
enabled: bool,
|
||||
) -> impl Widget + '_ {
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
let painter = ui.painter();
|
||||
let text_color = if ui.visuals().dark_mode {
|
||||
egui::Color32::WHITE
|
||||
} else {
|
||||
egui::Color32::BLACK
|
||||
};
|
||||
|
||||
let galley = painter.layout(
|
||||
text.to_owned(),
|
||||
NotedeckTextStyle::Body.get_font_id(ui.ctx()),
|
||||
text_color,
|
||||
ui.available_width(),
|
||||
);
|
||||
|
||||
let size = galley.rect.expand2(egui::vec2(16.0, 8.0)).size();
|
||||
let mut button = Button::new(galley).corner_radius(8.0);
|
||||
|
||||
if !enabled {
|
||||
button = button
|
||||
.sense(egui::Sense::focusable_noninteractive())
|
||||
.fill(ui.visuals().noninteractive().bg_fill)
|
||||
.stroke(ui.visuals().noninteractive().bg_stroke);
|
||||
} else {
|
||||
button = button.fill(fill_color);
|
||||
}
|
||||
|
||||
let mut resp = ui.add_sized(size, button);
|
||||
|
||||
if !enabled {
|
||||
resp = resp.on_hover_cursor(egui::CursorIcon::NotAllowed);
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user