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:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -3267,8 +3267,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostrdb"
|
name = "nostrdb"
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
source = "git+https://github.com/damus-io/nostrdb-rs?rev=3e87e504090b8cc153474e584a1ecd4618441099#3e87e504090b8cc153474e584a1ecd4618441099"
|
source = "git+https://github.com/damus-io/nostrdb-rs?rev=ee7287a897fc229fa2ef060e2358a7ba258a4a6d#ee7287a897fc229fa2ef060e2358a7ba258a4a6d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen",
|
"bindgen",
|
||||||
"cc",
|
"cc",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ log = "0.4.17"
|
|||||||
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
|
nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] }
|
||||||
nwc = "0.39.0"
|
nwc = "0.39.0"
|
||||||
mio = { version = "1.0.3", features = ["os-poll", "net"] }
|
mio = { version = "1.0.3", features = ["os-poll", "net"] }
|
||||||
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "3e87e504090b8cc153474e584a1ecd4618441099" }
|
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "ee7287a897fc229fa2ef060e2358a7ba258a4a6d" }
|
||||||
#nostrdb = "0.6.1"
|
#nostrdb = "0.6.1"
|
||||||
notedeck = { path = "crates/notedeck" }
|
notedeck = { path = "crates/notedeck" }
|
||||||
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use tracing::debug;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::account::cache::AccountCache;
|
use crate::account::cache::AccountCache;
|
||||||
|
use crate::account::contacts::Contacts;
|
||||||
use crate::account::mute::AccountMutedData;
|
use crate::account::mute::AccountMutedData;
|
||||||
use crate::account::relay::{
|
use crate::account::relay::{
|
||||||
modify_advertised_relays, update_relay_configuration, AccountRelayData, RelayAction,
|
modify_advertised_relays, update_relay_configuration, AccountRelayData, RelayAction,
|
||||||
@@ -42,10 +42,7 @@ impl Accounts {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let (mut cache, unknown_id) = AccountCache::new(UserAccount::new(
|
let (mut cache, unknown_id) = AccountCache::new(UserAccount::new(
|
||||||
Keypair::only_pubkey(fallback),
|
Keypair::only_pubkey(fallback),
|
||||||
AccountData {
|
AccountData::new(fallback.bytes()),
|
||||||
relay: AccountRelayData::new(ndb, txn, fallback.bytes()),
|
|
||||||
muted: AccountMutedData::new(ndb, txn, fallback.bytes()),
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
|
|
||||||
unknown_id.process_action(unknown_ids, ndb, txn);
|
unknown_id.process_action(unknown_ids, ndb, txn);
|
||||||
@@ -56,7 +53,7 @@ impl Accounts {
|
|||||||
match reader.get_accounts() {
|
match reader.get_accounts() {
|
||||||
Ok(accounts) => {
|
Ok(accounts) => {
|
||||||
for account in accounts {
|
for account in accounts {
|
||||||
add_account_from_storage(&mut cache, ndb, txn, account).process_action(
|
add_account_from_storage(&mut cache, account).process_action(
|
||||||
unknown_ids,
|
unknown_ids,
|
||||||
ndb,
|
ndb,
|
||||||
txn,
|
txn,
|
||||||
@@ -76,8 +73,10 @@ impl Accounts {
|
|||||||
|
|
||||||
let relay_defaults = RelayDefaults::new(forced_relays);
|
let relay_defaults = RelayDefaults::new(forced_relays);
|
||||||
|
|
||||||
let selected = cache.selected();
|
let selected = cache.selected_mut();
|
||||||
let selected_data = &selected.data;
|
let selected_data = &mut selected.data;
|
||||||
|
|
||||||
|
selected_data.query(ndb, txn);
|
||||||
|
|
||||||
let subs = {
|
let subs = {
|
||||||
AccountSubs::new(
|
AccountSubs::new(
|
||||||
@@ -117,12 +116,7 @@ impl Accounts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"]
|
#[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"]
|
||||||
pub fn add_account(
|
pub fn add_account(&mut self, kp: Keypair) -> Option<AddAccountResponse> {
|
||||||
&mut self,
|
|
||||||
ndb: &Ndb,
|
|
||||||
txn: &Transaction,
|
|
||||||
kp: Keypair,
|
|
||||||
) -> Option<AddAccountResponse> {
|
|
||||||
let acc = if let Some(acc) = self.cache.get_mut(&kp.pubkey) {
|
let acc = if let Some(acc) = self.cache.get_mut(&kp.pubkey) {
|
||||||
if kp.secret_key.is_none() || acc.key.secret_key.is_some() {
|
if kp.secret_key.is_none() || acc.key.secret_key.is_some() {
|
||||||
tracing::info!("Already have account, not adding");
|
tracing::info!("Already have account, not adding");
|
||||||
@@ -132,10 +126,7 @@ impl Accounts {
|
|||||||
acc.key = kp.clone();
|
acc.key = kp.clone();
|
||||||
AccType::Acc(&*acc)
|
AccType::Acc(&*acc)
|
||||||
} else {
|
} else {
|
||||||
let new_account_data = AccountData {
|
let new_account_data = AccountData::new(kp.pubkey.bytes());
|
||||||
relay: AccountRelayData::new(ndb, txn, kp.pubkey.bytes()),
|
|
||||||
muted: AccountMutedData::new(ndb, txn, kp.pubkey.bytes()),
|
|
||||||
};
|
|
||||||
AccType::Entry(
|
AccType::Entry(
|
||||||
self.cache
|
self.cache
|
||||||
.add(UserAccount::new(kp.clone(), new_account_data)),
|
.add(UserAccount::new(kp.clone(), new_account_data)),
|
||||||
@@ -213,6 +204,7 @@ impl Accounts {
|
|||||||
&mut self,
|
&mut self,
|
||||||
pk_to_select: &Pubkey,
|
pk_to_select: &Pubkey,
|
||||||
ndb: &mut Ndb,
|
ndb: &mut Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
) {
|
) {
|
||||||
@@ -226,6 +218,7 @@ impl Accounts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.get_selected_account_mut().data.query(ndb, txn);
|
||||||
self.subs.swap_to(
|
self.subs.swap_to(
|
||||||
ndb,
|
ndb,
|
||||||
pool,
|
pool,
|
||||||
@@ -261,55 +254,42 @@ impl Accounts {
|
|||||||
),
|
),
|
||||||
relay_url,
|
relay_url,
|
||||||
);
|
);
|
||||||
}
|
pool.send_to(
|
||||||
|
&ClientMessage::req(
|
||||||
fn poll_for_updates(&mut self, ndb: &Ndb) -> bool {
|
self.subs.contacts.remote.clone(),
|
||||||
let mut changed = false;
|
vec![data.contacts.filter.clone()],
|
||||||
let relay_sub = self.subs.relay.local;
|
),
|
||||||
let mute_sub = self.subs.mute.local;
|
relay_url,
|
||||||
let acc = self.get_selected_account_mut();
|
|
||||||
|
|
||||||
let nks = ndb.poll_for_notes(relay_sub, 1);
|
|
||||||
if !nks.is_empty() {
|
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
|
||||||
let relays = AccountRelayData::harvest_nip65_relays(ndb, &txn, &nks);
|
|
||||||
debug!(
|
|
||||||
"pubkey {}: updated relays {:?}",
|
|
||||||
acc.key.pubkey.hex(),
|
|
||||||
relays
|
|
||||||
);
|
);
|
||||||
acc.data.relay.advertised = relays.into_iter().collect();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nks = ndb.poll_for_notes(mute_sub, 1);
|
|
||||||
if !nks.is_empty() {
|
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
|
||||||
let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks);
|
|
||||||
debug!("pubkey {}: updated muted {:?}", acc.key.pubkey.hex(), muted);
|
|
||||||
acc.data.muted.muted = Arc::new(muted);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
changed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, ndb: &mut Ndb, pool: &mut RelayPool, ctx: &egui::Context) {
|
pub fn update(&mut self, ndb: &mut Ndb, pool: &mut RelayPool, ctx: &egui::Context) {
|
||||||
// IMPORTANT - This function is called in the UI update loop,
|
// IMPORTANT - This function is called in the UI update loop,
|
||||||
// make sure it is fast when idle
|
// make sure it is fast when idle
|
||||||
|
|
||||||
|
let Some(update) = self
|
||||||
|
.cache
|
||||||
|
.selected_mut()
|
||||||
|
.data
|
||||||
|
.poll_for_updates(ndb, &self.subs)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match update {
|
||||||
// If needed, update the relay configuration
|
// If needed, update the relay configuration
|
||||||
if self.poll_for_updates(ndb) {
|
AccountDataUpdate::Relay => {
|
||||||
let acc = self.cache.selected();
|
let acc = self.cache.selected();
|
||||||
update_relay_configuration(
|
update_relay_configuration(
|
||||||
pool,
|
pool,
|
||||||
&self.relay_defaults,
|
&self.relay_defaults,
|
||||||
&acc.key.pubkey,
|
&acc.key.pubkey,
|
||||||
&acc.data,
|
&acc.data.relay,
|
||||||
create_wakeup(ctx),
|
create_wakeup(ctx),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_full<'a>(&'a self, pubkey: &Pubkey) -> Option<FilledKeypair<'a>> {
|
pub fn get_full<'a>(&'a self, pubkey: &Pubkey) -> Option<FilledKeypair<'a>> {
|
||||||
self.cache.get(pubkey).and_then(|r| r.key.to_full())
|
self.cache.get(pubkey).and_then(|r| r.key.to_full())
|
||||||
@@ -328,10 +308,14 @@ impl Accounts {
|
|||||||
pool,
|
pool,
|
||||||
&self.relay_defaults,
|
&self.relay_defaults,
|
||||||
&acc.key.pubkey,
|
&acc.key.pubkey,
|
||||||
&acc.data,
|
&acc.data.relay,
|
||||||
create_wakeup(ctx),
|
create_wakeup(ctx),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_subs(&self) -> &AccountSubs {
|
||||||
|
&self.subs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AccType<'a> {
|
enum AccType<'a> {
|
||||||
@@ -357,11 +341,9 @@ fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'stat
|
|||||||
|
|
||||||
fn add_account_from_storage(
|
fn add_account_from_storage(
|
||||||
cache: &mut AccountCache,
|
cache: &mut AccountCache,
|
||||||
ndb: &Ndb,
|
|
||||||
txn: &Transaction,
|
|
||||||
user_account_serializable: UserAccountSerializable,
|
user_account_serializable: UserAccountSerializable,
|
||||||
) -> SingleUnkIdAction {
|
) -> SingleUnkIdAction {
|
||||||
let Some(acc) = get_acc_from_storage(ndb, txn, user_account_serializable) else {
|
let Some(acc) = get_acc_from_storage(user_account_serializable) else {
|
||||||
return SingleUnkIdAction::NoAction;
|
return SingleUnkIdAction::NoAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -371,16 +353,9 @@ fn add_account_from_storage(
|
|||||||
SingleUnkIdAction::pubkey(pk)
|
SingleUnkIdAction::pubkey(pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_acc_from_storage(
|
fn get_acc_from_storage(user_account_serializable: UserAccountSerializable) -> Option<UserAccount> {
|
||||||
ndb: &Ndb,
|
|
||||||
txn: &Transaction,
|
|
||||||
user_account_serializable: UserAccountSerializable,
|
|
||||||
) -> Option<UserAccount> {
|
|
||||||
let keypair = user_account_serializable.key;
|
let keypair = user_account_serializable.key;
|
||||||
let new_account_data = AccountData {
|
let new_account_data = AccountData::new(keypair.pubkey.bytes());
|
||||||
relay: AccountRelayData::new(ndb, txn, keypair.pubkey.bytes()),
|
|
||||||
muted: AccountMutedData::new(ndb, txn, keypair.pubkey.bytes()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut wallet = None;
|
let mut wallet = None;
|
||||||
if let Some(wallet_s) = user_account_serializable.wallet {
|
if let Some(wallet_s) = user_account_serializable.wallet {
|
||||||
@@ -403,6 +378,46 @@ fn get_acc_from_storage(
|
|||||||
pub struct AccountData {
|
pub struct AccountData {
|
||||||
pub(crate) relay: AccountRelayData,
|
pub(crate) relay: AccountRelayData,
|
||||||
pub(crate) muted: AccountMutedData,
|
pub(crate) muted: AccountMutedData,
|
||||||
|
pub contacts: Contacts,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountData {
|
||||||
|
pub fn new(pubkey: &[u8; 32]) -> Self {
|
||||||
|
Self {
|
||||||
|
relay: AccountRelayData::new(pubkey),
|
||||||
|
muted: AccountMutedData::new(pubkey),
|
||||||
|
contacts: Contacts::new(pubkey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn poll_for_updates(
|
||||||
|
&mut self,
|
||||||
|
ndb: &Ndb,
|
||||||
|
subs: &AccountSubs,
|
||||||
|
) -> Option<AccountDataUpdate> {
|
||||||
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
|
let mut resp = None;
|
||||||
|
if self.relay.poll_for_updates(ndb, &txn, subs.relay.local) {
|
||||||
|
resp = Some(AccountDataUpdate::Relay);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.muted.poll_for_updates(ndb, &txn, subs.mute.local);
|
||||||
|
self.contacts
|
||||||
|
.poll_for_updates(ndb, &txn, subs.contacts.local);
|
||||||
|
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Note: query should be called as close to the subscription as possible
|
||||||
|
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||||
|
self.relay.query(ndb, txn);
|
||||||
|
self.muted.query(ndb, txn);
|
||||||
|
self.contacts.query(ndb, txn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) enum AccountDataUpdate {
|
||||||
|
Relay,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AddAccountResponse {
|
pub struct AddAccountResponse {
|
||||||
@@ -410,13 +425,14 @@ pub struct AddAccountResponse {
|
|||||||
pub unk_id_action: SingleUnkIdAction,
|
pub unk_id_action: SingleUnkIdAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AccountSubs {
|
pub struct AccountSubs {
|
||||||
relay: UnifiedSubscription,
|
relay: UnifiedSubscription,
|
||||||
mute: UnifiedSubscription,
|
mute: UnifiedSubscription,
|
||||||
|
pub contacts: UnifiedSubscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountSubs {
|
impl AccountSubs {
|
||||||
pub fn new(
|
pub(super) fn new(
|
||||||
ndb: &mut Ndb,
|
ndb: &mut Ndb,
|
||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
relay_defaults: &RelayDefaults,
|
relay_defaults: &RelayDefaults,
|
||||||
@@ -426,12 +442,17 @@ impl AccountSubs {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let relay = subscribe(ndb, pool, &data.relay.filter);
|
let relay = subscribe(ndb, pool, &data.relay.filter);
|
||||||
let mute = subscribe(ndb, pool, &data.muted.filter);
|
let mute = subscribe(ndb, pool, &data.muted.filter);
|
||||||
update_relay_configuration(pool, relay_defaults, pk, data, wakeup);
|
let contacts = subscribe(ndb, pool, &data.contacts.filter);
|
||||||
|
update_relay_configuration(pool, relay_defaults, pk, &data.relay, wakeup);
|
||||||
|
|
||||||
Self { relay, mute }
|
Self {
|
||||||
|
relay,
|
||||||
|
mute,
|
||||||
|
contacts,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn swap_to(
|
pub(super) fn swap_to(
|
||||||
&mut self,
|
&mut self,
|
||||||
ndb: &mut Ndb,
|
ndb: &mut Ndb,
|
||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
@@ -442,6 +463,7 @@ impl AccountSubs {
|
|||||||
) {
|
) {
|
||||||
unsubscribe(ndb, pool, &self.relay);
|
unsubscribe(ndb, pool, &self.relay);
|
||||||
unsubscribe(ndb, pool, &self.mute);
|
unsubscribe(ndb, pool, &self.mute);
|
||||||
|
unsubscribe(ndb, pool, &self.contacts);
|
||||||
|
|
||||||
*self = AccountSubs::new(ndb, pool, relay_defaults, pk, new_selection_data, wakeup);
|
*self = AccountSubs::new(ndb, pool, relay_defaults, pk, new_selection_data, wakeup);
|
||||||
}
|
}
|
||||||
|
|||||||
145
crates/notedeck/src/account/contacts.rs
Normal file
145
crates/notedeck/src/account/contacts.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction};
|
||||||
|
|
||||||
|
pub struct Contacts {
|
||||||
|
pub filter: Filter,
|
||||||
|
pub(super) state: ContactState,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ContactState {
|
||||||
|
Unreceived,
|
||||||
|
Received {
|
||||||
|
contacts: HashSet<Pubkey>,
|
||||||
|
note_key: NoteKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
|
||||||
|
pub enum IsFollowing {
|
||||||
|
/// We don't have the contact list, so we don't know
|
||||||
|
Unknown,
|
||||||
|
|
||||||
|
/// We are follow
|
||||||
|
Yes,
|
||||||
|
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Contacts {
|
||||||
|
pub fn new(pubkey: &[u8; 32]) -> Self {
|
||||||
|
let filter = Filter::new().authors([pubkey]).kinds([3]).limit(1).build();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
filter,
|
||||||
|
state: ContactState::Unreceived,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||||
|
let binding = ndb
|
||||||
|
.query(txn, &[self.filter.clone()], 1)
|
||||||
|
.expect("query user relays results");
|
||||||
|
|
||||||
|
let Some(res) = binding.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
update_state(&mut self.state, &res.note, res.note_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_following(&self, other: &Pubkey) -> IsFollowing {
|
||||||
|
match &self.state {
|
||||||
|
ContactState::Unreceived => IsFollowing::Unknown,
|
||||||
|
ContactState::Received {
|
||||||
|
contacts,
|
||||||
|
note_key: _,
|
||||||
|
} => {
|
||||||
|
if contacts.contains(other) {
|
||||||
|
IsFollowing::Yes
|
||||||
|
} else {
|
||||||
|
IsFollowing::No
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) {
|
||||||
|
let nks = ndb.poll_for_notes(sub, 1);
|
||||||
|
|
||||||
|
let Some(key) = nks.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let note = match ndb.get_note_by_key(txn, *key) {
|
||||||
|
Ok(note) => note,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Could not find note at key {:?}: {e}", key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
update_state(&mut self.state, ¬e, *key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_state(&self) -> &ContactState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) {
|
||||||
|
match state {
|
||||||
|
ContactState::Unreceived => {
|
||||||
|
*state = ContactState::Received {
|
||||||
|
contacts: get_contacts_owned(note),
|
||||||
|
note_key: key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ContactState::Received { contacts, note_key } => {
|
||||||
|
update_contacts(contacts, note);
|
||||||
|
*note_key = key;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_contacts<'a>(note: &Note<'a>) -> HashSet<&'a [u8; 32]> {
|
||||||
|
let mut contacts = HashSet::with_capacity(note.tags().count().into());
|
||||||
|
|
||||||
|
for tag in note.tags() {
|
||||||
|
if tag.count() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some("p") = tag.get_str(0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(cur_id) = tag.get_id(1) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
contacts.insert(cur_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_contacts_owned(note: &Note<'_>) -> HashSet<Pubkey> {
|
||||||
|
get_contacts(note)
|
||||||
|
.iter()
|
||||||
|
.map(|p| Pubkey::new(**p))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_contacts(cur: &mut HashSet<Pubkey>, new: &Note<'_>) {
|
||||||
|
let new_contacts = get_contacts(new);
|
||||||
|
|
||||||
|
cur.retain(|pk| new_contacts.contains(pk.bytes()));
|
||||||
|
|
||||||
|
new_contacts.iter().for_each(|c| {
|
||||||
|
if !cur.contains(*c) {
|
||||||
|
cur.insert(Pubkey::new(**c));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod accounts;
|
pub mod accounts;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod contacts;
|
||||||
pub mod mute;
|
pub mod mute;
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use nostrdb::{Filter, Ndb, NoteKey, Transaction};
|
use nostrdb::{Filter, Ndb, NoteKey, Subscription, Transaction};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::Muted;
|
use crate::Muted;
|
||||||
@@ -11,7 +11,7 @@ pub(crate) struct AccountMutedData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AccountMutedData {
|
impl AccountMutedData {
|
||||||
pub fn new(ndb: &Ndb, txn: &Transaction, pubkey: &[u8; 32]) -> Self {
|
pub fn new(pubkey: &[u8; 32]) -> Self {
|
||||||
// Construct a filter for the user's NIP-51 muted list
|
// Construct a filter for the user's NIP-51 muted list
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.authors([pubkey])
|
.authors([pubkey])
|
||||||
@@ -19,21 +19,28 @@ impl AccountMutedData {
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
AccountMutedData {
|
||||||
|
filter,
|
||||||
|
muted: Arc::new(Muted::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||||
// Query the ndb immediately to see if the user's muted list is already there
|
// Query the ndb immediately to see if the user's muted list is already there
|
||||||
let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32;
|
let lim = self
|
||||||
|
.filter
|
||||||
|
.limit()
|
||||||
|
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||||
let nks = ndb
|
let nks = ndb
|
||||||
.query(txn, &[filter.clone()], lim)
|
.query(txn, &[self.filter.clone()], lim)
|
||||||
.expect("query user muted results")
|
.expect("query user muted results")
|
||||||
.iter()
|
.iter()
|
||||||
.map(|qr| qr.note_key)
|
.map(|qr| qr.note_key)
|
||||||
.collect::<Vec<NoteKey>>();
|
.collect::<Vec<NoteKey>>();
|
||||||
let muted = Self::harvest_nip51_muted(ndb, txn, &nks);
|
let muted = Self::harvest_nip51_muted(ndb, txn, &nks);
|
||||||
debug!("pubkey {}: initial muted {:?}", hex::encode(pubkey), muted);
|
debug!("initial muted {:?}", muted);
|
||||||
|
|
||||||
AccountMutedData {
|
self.muted = Arc::new(muted);
|
||||||
filter,
|
|
||||||
muted: Arc::new(muted),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted {
|
pub(crate) fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted {
|
||||||
@@ -76,4 +83,16 @@ impl AccountMutedData {
|
|||||||
}
|
}
|
||||||
muted
|
muted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) {
|
||||||
|
let nks = ndb.poll_for_notes(sub, 1);
|
||||||
|
|
||||||
|
if nks.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let muted = AccountMutedData::harvest_nip51_muted(ndb, txn, &nks);
|
||||||
|
debug!("updated muted {:?}", muted);
|
||||||
|
self.muted = Arc::new(muted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use enostr::{Keypair, Pubkey, RelayPool};
|
use enostr::{Keypair, Pubkey, RelayPool};
|
||||||
use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Transaction};
|
use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction};
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ pub(crate) struct AccountRelayData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AccountRelayData {
|
impl AccountRelayData {
|
||||||
pub fn new(ndb: &Ndb, txn: &Transaction, pubkey: &[u8; 32]) -> Self {
|
pub fn new(pubkey: &[u8; 32]) -> Self {
|
||||||
// Construct a filter for the user's NIP-65 relay list
|
// Construct a filter for the user's NIP-65 relay list
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.authors([pubkey])
|
.authors([pubkey])
|
||||||
@@ -22,26 +22,29 @@ impl AccountRelayData {
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
AccountRelayData {
|
||||||
|
filter,
|
||||||
|
local: BTreeSet::new(),
|
||||||
|
advertised: BTreeSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||||
// Query the ndb immediately to see if the user list is already there
|
// Query the ndb immediately to see if the user list is already there
|
||||||
let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32;
|
let lim = self
|
||||||
|
.filter
|
||||||
|
.limit()
|
||||||
|
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||||
let nks = ndb
|
let nks = ndb
|
||||||
.query(txn, &[filter.clone()], lim)
|
.query(txn, &[self.filter.clone()], lim)
|
||||||
.expect("query user relays results")
|
.expect("query user relays results")
|
||||||
.iter()
|
.iter()
|
||||||
.map(|qr| qr.note_key)
|
.map(|qr| qr.note_key)
|
||||||
.collect::<Vec<NoteKey>>();
|
.collect::<Vec<NoteKey>>();
|
||||||
let relays = Self::harvest_nip65_relays(ndb, txn, &nks);
|
let relays = Self::harvest_nip65_relays(ndb, txn, &nks);
|
||||||
debug!(
|
debug!("initial relays {:?}", relays);
|
||||||
"pubkey {}: initial relays {:?}",
|
|
||||||
hex::encode(pubkey),
|
|
||||||
relays
|
|
||||||
);
|
|
||||||
|
|
||||||
AccountRelayData {
|
self.advertised = relays.into_iter().collect()
|
||||||
filter,
|
|
||||||
local: BTreeSet::new(),
|
|
||||||
advertised: relays.into_iter().collect(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// standardize the format (ie, trailing slashes) to avoid dups
|
// standardize the format (ie, trailing slashes) to avoid dups
|
||||||
@@ -106,6 +109,20 @@ impl AccountRelayData {
|
|||||||
let note = builder.sign(seckey).build().expect("note build");
|
let note = builder.sign(seckey).build().expect("note build");
|
||||||
pool.send(&enostr::ClientMessage::event(¬e).expect("note client message"));
|
pool.send(&enostr::ClientMessage::event(¬e).expect("note client message"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) -> bool {
|
||||||
|
let nks = ndb.poll_for_notes(sub, 1);
|
||||||
|
|
||||||
|
if nks.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let relays = AccountRelayData::harvest_nip65_relays(ndb, txn, &nks);
|
||||||
|
debug!("updated relays {:?}", relays);
|
||||||
|
self.advertised = relays.into_iter().collect();
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct RelayDefaults {
|
pub(crate) struct RelayDefaults {
|
||||||
@@ -142,7 +159,7 @@ pub(super) fn update_relay_configuration(
|
|||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
relay_defaults: &RelayDefaults,
|
relay_defaults: &RelayDefaults,
|
||||||
pk: &Pubkey,
|
pk: &Pubkey,
|
||||||
data: &AccountData,
|
data: &AccountRelayData,
|
||||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||||
) {
|
) {
|
||||||
debug!(
|
debug!(
|
||||||
@@ -155,8 +172,8 @@ pub(super) fn update_relay_configuration(
|
|||||||
|
|
||||||
// Compose the desired relay lists from the selected account
|
// Compose the desired relay lists from the selected account
|
||||||
if desired_relays.is_empty() {
|
if desired_relays.is_empty() {
|
||||||
desired_relays.extend(data.relay.local.iter().cloned());
|
desired_relays.extend(data.local.iter().cloned());
|
||||||
desired_relays.extend(data.relay.advertised.iter().cloned());
|
desired_relays.extend(data.advertised.iter().cloned());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no relays are specified at this point use the bootstrap list
|
// If no relays are specified at this point use the bootstrap list
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ impl Notedeck {
|
|||||||
{
|
{
|
||||||
for key in &parsed_args.keys {
|
for key in &parsed_args.keys {
|
||||||
info!("adding account: {}", &key.pubkey);
|
info!("adding account: {}", &key.pubkey);
|
||||||
if let Some(resp) = accounts.add_account(&ndb, &txn, key.clone()) {
|
if let Some(resp) = accounts.add_account(key.clone()) {
|
||||||
resp.unk_id_action
|
resp.unk_id_action
|
||||||
.process_action(&mut unknown_ids, &ndb, &txn);
|
.process_action(&mut unknown_ids, &ndb, &txn);
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ impl Notedeck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(first) = parsed_args.keys.first() {
|
if let Some(first) = parsed_args.keys.first() {
|
||||||
accounts.select_account(&first.pubkey, &mut ndb, &mut pool, ctx);
|
accounts.select_account(&first.pubkey, &mut ndb, &txn, &mut pool, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let img_cache = Images::new(img_cache_dir);
|
let img_cache = Images::new(img_cache_dir);
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ mod user_account;
|
|||||||
mod wallet;
|
mod wallet;
|
||||||
mod zaps;
|
mod zaps;
|
||||||
|
|
||||||
pub use account::accounts::{AccountData, Accounts};
|
pub use account::accounts::{AccountData, AccountSubs, Accounts};
|
||||||
|
pub use account::contacts::{ContactState, IsFollowing};
|
||||||
pub use account::relay::RelayAction;
|
pub use account::relay::RelayAction;
|
||||||
pub use account::FALLBACK_PUBKEY;
|
pub use account::FALLBACK_PUBKEY;
|
||||||
pub use app::{App, AppAction, Notedeck};
|
pub use app::{App, AppAction, Notedeck};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use enostr::{Keypair, KeypairUnowned};
|
use enostr::{Keypair, KeypairUnowned, Pubkey};
|
||||||
use tokenator::{ParseError, TokenParser, TokenSerializable};
|
use tokenator::{ParseError, TokenParser, TokenSerializable};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
wallet::{WalletSerializable, ZapWallet},
|
wallet::{WalletSerializable, ZapWallet},
|
||||||
AccountData,
|
AccountData, IsFollowing,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct UserAccount {
|
pub struct UserAccount {
|
||||||
@@ -32,6 +32,10 @@ impl UserAccount {
|
|||||||
self.wallet = Some(wallet);
|
self.wallet = Some(wallet);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_following(&self, pk: &Pubkey) -> IsFollowing {
|
||||||
|
self.data.contacts.is_following(pk)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserAccountSerializable {
|
pub struct UserAccountSerializable {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use notedeck::{Accounts, AppContext, SingleUnkIdAction, UnknownIds};
|
|||||||
|
|
||||||
use crate::app::get_active_columns_mut;
|
use crate::app::get_active_columns_mut;
|
||||||
use crate::decks::DecksCache;
|
use crate::decks::DecksCache;
|
||||||
|
use crate::profile::send_new_contact_list;
|
||||||
use crate::{
|
use crate::{
|
||||||
login_manager::AcquireKeyState,
|
login_manager::AcquireKeyState,
|
||||||
route::Route,
|
route::Route,
|
||||||
@@ -149,18 +150,14 @@ pub fn process_login_view_response(
|
|||||||
) -> AddAccountAction {
|
) -> AddAccountAction {
|
||||||
let (r, pubkey) = match response {
|
let (r, pubkey) = match response {
|
||||||
AccountLoginResponse::CreateNew => {
|
AccountLoginResponse::CreateNew => {
|
||||||
let kp = FullKeypair::generate().to_keypair();
|
let kp = FullKeypair::generate();
|
||||||
let pubkey = kp.pubkey;
|
let pubkey = kp.pubkey;
|
||||||
let txn = Transaction::new(app_ctx.ndb).expect("txn");
|
send_new_contact_list(kp.to_filled(), app_ctx.ndb, app_ctx.pool);
|
||||||
(app_ctx.accounts.add_account(app_ctx.ndb, &txn, kp), pubkey)
|
(app_ctx.accounts.add_account(kp.to_keypair()), pubkey)
|
||||||
}
|
}
|
||||||
AccountLoginResponse::LoginWith(keypair) => {
|
AccountLoginResponse::LoginWith(keypair) => {
|
||||||
let pubkey = keypair.pubkey;
|
let pubkey = keypair.pubkey;
|
||||||
let txn = Transaction::new(app_ctx.ndb).expect("txn");
|
(app_ctx.accounts.add_account(keypair), pubkey)
|
||||||
(
|
|
||||||
app_ctx.accounts.add_account(app_ctx.ndb, &txn, keypair),
|
|
||||||
pubkey,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ use crate::{
|
|||||||
storage,
|
storage,
|
||||||
subscriptions::{SubKind, Subscriptions},
|
subscriptions::{SubKind, Subscriptions},
|
||||||
support::Support,
|
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},
|
ui::{self, DesktopSidePanel, SidePanelAction},
|
||||||
view_state::ViewState,
|
view_state::ViewState,
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use notedeck::{
|
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};
|
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
||||||
|
|
||||||
@@ -116,13 +118,21 @@ fn try_process_event(
|
|||||||
.accounts
|
.accounts
|
||||||
.send_initial_filters(app_ctx.pool, &ev.relay);
|
.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(
|
timeline::send_initial_timeline_filters(
|
||||||
app_ctx.ndb,
|
|
||||||
damus.since_optimize,
|
damus.since_optimize,
|
||||||
&mut damus.timeline_cache,
|
&mut damus.timeline_cache,
|
||||||
&mut damus.subscriptions,
|
&mut damus.subscriptions,
|
||||||
app_ctx.pool,
|
app_ctx.pool,
|
||||||
&ev.relay,
|
&ev.relay,
|
||||||
|
app_ctx.accounts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// TODO: handle reconnects
|
// TODO: handle reconnects
|
||||||
@@ -248,44 +258,11 @@ fn handle_eose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SubKind::FetchingContactList(timeline_uid) => {
|
SubKind::FetchingContactList(timeline_uid) => {
|
||||||
let timeline = if let Some(tl) = timeline_cache.timelines.get_mut(timeline_uid) {
|
let Some(timeline) = timeline_cache.timelines.get_mut(timeline_uid) else {
|
||||||
tl
|
|
||||||
} else {
|
|
||||||
error!(
|
|
||||||
"timeline uid:{} not found for FetchingContactList",
|
|
||||||
timeline_uid
|
|
||||||
);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter_state = timeline.filter.get_mut(relay_url);
|
fetch_contact_list(relay_url, timeline, ctx.accounts);
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,7 +724,13 @@ fn timelines_view(
|
|||||||
let mut save_cols = false;
|
let mut save_cols = false;
|
||||||
if let Some(action) = side_panel_action {
|
if let Some(action) = side_panel_action {
|
||||||
save_cols = save_cols
|
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;
|
let mut app_action: Option<AppAction> = None;
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ use crate::{
|
|||||||
profile::{ProfileAction, SaveProfileChanges},
|
profile::{ProfileAction, SaveProfileChanges},
|
||||||
profile_state::ProfileState,
|
profile_state::ProfileState,
|
||||||
route::{Route, Router, SingletonRouter},
|
route::{Route, Router, SingletonRouter},
|
||||||
|
subscriptions::{SubKind, Subscriptions},
|
||||||
timeline::{
|
timeline::{
|
||||||
|
kind::ListKind,
|
||||||
route::{render_thread_route, render_timeline_route},
|
route::{render_thread_route, render_timeline_route},
|
||||||
TimelineCache,
|
TimelineCache, TimelineKind,
|
||||||
},
|
},
|
||||||
ui::{
|
ui::{
|
||||||
self,
|
self,
|
||||||
@@ -72,18 +74,31 @@ impl SwitchingAction {
|
|||||||
&self,
|
&self,
|
||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
decks_cache: &mut DecksCache,
|
decks_cache: &mut DecksCache,
|
||||||
|
subs: &mut Subscriptions,
|
||||||
ctx: &mut AppContext<'_>,
|
ctx: &mut AppContext<'_>,
|
||||||
ui_ctx: &egui::Context,
|
ui_ctx: &egui::Context,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match &self {
|
match &self {
|
||||||
SwitchingAction::Accounts(account_action) => match account_action {
|
SwitchingAction::Accounts(account_action) => match account_action {
|
||||||
AccountsAction::Switch(switch_action) => {
|
AccountsAction::Switch(switch_action) => {
|
||||||
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||||
ctx.accounts.select_account(
|
ctx.accounts.select_account(
|
||||||
&switch_action.switch_to,
|
&switch_action.switch_to,
|
||||||
ctx.ndb,
|
ctx.ndb,
|
||||||
|
&txn,
|
||||||
ctx.pool,
|
ctx.pool,
|
||||||
ui_ctx,
|
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
|
// pop nav after switch
|
||||||
get_active_columns_mut(ctx.accounts, decks_cache)
|
get_active_columns_mut(ctx.accounts, decks_cache)
|
||||||
.column_mut(switch_action.source_column)
|
.column_mut(switch_action.source_column)
|
||||||
@@ -378,6 +393,7 @@ fn process_render_nav_action(
|
|||||||
if switching_action.process(
|
if switching_action.process(
|
||||||
&mut app.timeline_cache,
|
&mut app.timeline_cache,
|
||||||
&mut app.decks_cache,
|
&mut app.decks_cache,
|
||||||
|
&mut app.subscriptions,
|
||||||
ctx,
|
ctx,
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
) {
|
) {
|
||||||
@@ -390,6 +406,7 @@ fn process_render_nav_action(
|
|||||||
&mut app.view_state.pubkey_to_profile_state,
|
&mut app.view_state.pubkey_to_profile_state,
|
||||||
ctx.ndb,
|
ctx.ndb,
|
||||||
ctx.pool,
|
ctx.pool,
|
||||||
|
ctx.accounts,
|
||||||
),
|
),
|
||||||
RenderNavAction::WalletAction(wallet_action) => {
|
RenderNavAction::WalletAction(wallet_action) => {
|
||||||
wallet_action.process(ctx.accounts, ctx.global_wallet)
|
wallet_action.process(ctx.accounts, ctx.global_wallet)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use enostr::{FullKeypair, Pubkey, RelayPool};
|
use enostr::{FilledKeypair, FullKeypair, Pubkey, RelayPool};
|
||||||
use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder};
|
use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder, Transaction};
|
||||||
|
|
||||||
|
use notedeck::{Accounts, ContactState};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{nav::RouterAction, profile_state::ProfileState, route::Route};
|
use crate::{nav::RouterAction, profile_state::ProfileState, route::Route};
|
||||||
@@ -37,6 +38,8 @@ fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
|
|||||||
pub enum ProfileAction {
|
pub enum ProfileAction {
|
||||||
Edit(FullKeypair),
|
Edit(FullKeypair),
|
||||||
SaveChanges(SaveProfileChanges),
|
SaveChanges(SaveProfileChanges),
|
||||||
|
Follow(Pubkey),
|
||||||
|
Unfollow(Pubkey),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileAction {
|
impl ProfileAction {
|
||||||
@@ -45,6 +48,7 @@ impl ProfileAction {
|
|||||||
state_map: &mut HashMap<Pubkey, ProfileState>,
|
state_map: &mut HashMap<Pubkey, ProfileState>,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
|
accounts: &Accounts,
|
||||||
) -> Option<RouterAction> {
|
) -> Option<RouterAction> {
|
||||||
match self {
|
match self {
|
||||||
ProfileAction::Edit(kp) => Some(RouterAction::route_to(Route::EditProfile(kp.pubkey))),
|
ProfileAction::Edit(kp) => Some(RouterAction::route_to(Route::EditProfile(kp.pubkey))),
|
||||||
@@ -62,6 +66,157 @@ impl ProfileAction {
|
|||||||
|
|
||||||
Some(RouterAction::GoBack)
|
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::{
|
use notedeck::{
|
||||||
filter, CachedNote, FilterError, FilterState, FilterStates, NoteCache, NoteRef, UnknownIds,
|
filter, Accounts, CachedNote, FilterError, FilterState, FilterStates, NoteCache, NoteRef,
|
||||||
|
UnknownIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
use egui_virtual_list::VirtualList;
|
use egui_virtual_list::VirtualList;
|
||||||
@@ -474,6 +475,7 @@ pub fn setup_new_timeline(
|
|||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
since_optimize: bool,
|
since_optimize: bool,
|
||||||
|
accounts: &Accounts,
|
||||||
) {
|
) {
|
||||||
// if we're ready, setup local subs
|
// if we're ready, setup local subs
|
||||||
if is_timeline_ready(ndb, pool, note_cache, timeline) {
|
if is_timeline_ready(ndb, pool, note_cache, timeline) {
|
||||||
@@ -483,7 +485,7 @@ pub fn setup_new_timeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for relay in &mut pool.relays {
|
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
|
/// situations where you are adding a new timeline, use
|
||||||
/// setup_new_timeline.
|
/// setup_new_timeline.
|
||||||
pub fn send_initial_timeline_filters(
|
pub fn send_initial_timeline_filters(
|
||||||
ndb: &Ndb,
|
|
||||||
since_optimize: bool,
|
since_optimize: bool,
|
||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
subs: &mut Subscriptions,
|
subs: &mut Subscriptions,
|
||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
relay_id: &str,
|
relay_id: &str,
|
||||||
|
accounts: &Accounts,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
info!("Sending initial filters to {}", relay_id);
|
info!("Sending initial filters to {}", relay_id);
|
||||||
let relay = &mut pool.relays.iter_mut().find(|r| r.url() == relay_id)?;
|
let relay = &mut pool.relays.iter_mut().find(|r| r.url() == relay_id)?;
|
||||||
|
|
||||||
for (_kind, timeline) in timeline_cache.timelines.iter_mut() {
|
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(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_initial_timeline_filter(
|
pub fn send_initial_timeline_filter(
|
||||||
ndb: &Ndb,
|
|
||||||
can_since_optimize: bool,
|
can_since_optimize: bool,
|
||||||
subs: &mut Subscriptions,
|
subs: &mut Subscriptions,
|
||||||
relay: &mut PoolRelay,
|
relay: &mut PoolRelay,
|
||||||
timeline: &mut Timeline,
|
timeline: &mut Timeline,
|
||||||
|
accounts: &Accounts,
|
||||||
) {
|
) {
|
||||||
let filter_state = timeline.filter.get_mut(relay.url());
|
let filter_state = timeline.filter.get_mut(relay.url());
|
||||||
|
|
||||||
@@ -572,34 +574,27 @@ pub fn send_initial_timeline_filter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we need some data first
|
// we need some data first
|
||||||
FilterState::NeedsRemote(filter) => {
|
FilterState::NeedsRemote(_filter) => fetch_contact_list(relay.url(), timeline, accounts),
|
||||||
fetch_contact_list(filter.to_owned(), ndb, subs, relay, timeline)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_contact_list(
|
pub fn fetch_contact_list(relay_url: &str, timeline: &mut Timeline, accounts: &Accounts) {
|
||||||
filter: Vec<Filter>,
|
let account_subs = accounts.get_subs();
|
||||||
ndb: &Ndb,
|
let local = account_subs.contacts.local;
|
||||||
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");
|
|
||||||
|
|
||||||
timeline.filter.set_relay_state(
|
let filter_state = match accounts.get_selected_account().data.contacts.get_state() {
|
||||||
relay.url().to_string(),
|
notedeck::ContactState::Unreceived => {
|
||||||
FilterState::fetching_remote(sub_id.clone(), local_sub),
|
FilterState::fetching_remote(account_subs.contacts.remote.clone(), 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}");
|
|
||||||
}
|
}
|
||||||
|
notedeck::ContactState::Received {
|
||||||
|
contacts: _,
|
||||||
|
note_key: _,
|
||||||
|
} => FilterState::GotRemote(local),
|
||||||
|
};
|
||||||
|
|
||||||
|
timeline
|
||||||
|
.filter
|
||||||
|
.set_relay_state(relay_url.to_owned(), filter_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_initial_timeline(
|
fn setup_initial_timeline(
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ pub fn render_profile_route(
|
|||||||
note_context: &mut NoteContext,
|
note_context: &mut NoteContext,
|
||||||
jobs: &mut JobsCache,
|
jobs: &mut JobsCache,
|
||||||
) -> Option<RenderNavAction> {
|
) -> Option<RenderNavAction> {
|
||||||
let action = ProfileView::new(
|
let profile_view = ProfileView::new(
|
||||||
pubkey,
|
pubkey,
|
||||||
accounts,
|
accounts,
|
||||||
col,
|
col,
|
||||||
@@ -128,7 +128,7 @@ pub fn render_profile_route(
|
|||||||
)
|
)
|
||||||
.ui(ui);
|
.ui(ui);
|
||||||
|
|
||||||
if let Some(action) = action {
|
if let Some(action) = profile_view {
|
||||||
match action {
|
match action {
|
||||||
ui::profile::ProfileViewAction::EditProfile => accounts
|
ui::profile::ProfileViewAction::EditProfile => accounts
|
||||||
.get_full(pubkey)
|
.get_full(pubkey)
|
||||||
@@ -136,6 +136,12 @@ pub fn render_profile_route(
|
|||||||
ui::profile::ProfileViewAction::Note(note_action) => {
|
ui::profile::ProfileViewAction::Note(note_action) => {
|
||||||
Some(RenderNavAction::NoteAction(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 {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -623,6 +623,7 @@ pub fn render_add_column_routes(
|
|||||||
ctx.pool,
|
ctx.pool,
|
||||||
ctx.note_cache,
|
ctx.note_cache,
|
||||||
app.since_optimize,
|
app.since_optimize,
|
||||||
|
ctx.accounts,
|
||||||
);
|
);
|
||||||
|
|
||||||
app.columns_mut(ctx.accounts)
|
app.columns_mut(ctx.accounts)
|
||||||
@@ -664,6 +665,7 @@ pub fn render_add_column_routes(
|
|||||||
ctx.pool,
|
ctx.pool,
|
||||||
ctx.note_cache,
|
ctx.note_cache,
|
||||||
app.since_optimize,
|
app.since_optimize,
|
||||||
|
ctx.accounts,
|
||||||
);
|
);
|
||||||
|
|
||||||
app.columns_mut(ctx.accounts)
|
app.columns_mut(ctx.accounts)
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ use nostrdb::{Ndb, ProfileRecord, Transaction};
|
|||||||
use notedeck::{
|
use notedeck::{
|
||||||
fonts::get_font_size, get_profile_url, name::get_display_name, Images, NotedeckTextStyle,
|
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 notedeck_ui::{
|
||||||
|
app_images, colors, profile::display_name_widget, widgets::styled_button_toggleable,
|
||||||
use crate::ui::widgets::styled_button_toggleable;
|
AnimationHelper, ProfilePic,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct CustomZapView<'a> {
|
pub struct CustomZapView<'a> {
|
||||||
images: &'a mut Images,
|
images: &'a mut Images,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub use edit::EditProfileView;
|
|||||||
use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke};
|
use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke};
|
||||||
use enostr::Pubkey;
|
use enostr::Pubkey;
|
||||||
use nostrdb::{ProfileRecord, Transaction};
|
use nostrdb::{ProfileRecord, Transaction};
|
||||||
|
use notedeck_ui::profile::follow_button;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -11,8 +12,8 @@ use crate::{
|
|||||||
ui::timeline::{tabs_ui, TimelineTabView},
|
ui::timeline::{tabs_ui, TimelineTabView},
|
||||||
};
|
};
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
name::get_display_name, profile::get_profile_url, Accounts, MuteFun, NoteAction, NoteContext,
|
name::get_display_name, profile::get_profile_url, Accounts, IsFollowing, MuteFun, NoteAction,
|
||||||
NotedeckTextStyle,
|
NoteContext, NotedeckTextStyle,
|
||||||
};
|
};
|
||||||
use notedeck_ui::{
|
use notedeck_ui::{
|
||||||
app_images,
|
app_images,
|
||||||
@@ -35,6 +36,8 @@ pub struct ProfileView<'a, 'd> {
|
|||||||
pub enum ProfileViewAction {
|
pub enum ProfileViewAction {
|
||||||
EditProfile,
|
EditProfile,
|
||||||
Note(NoteAction),
|
Note(NoteAction),
|
||||||
|
Unfollow(Pubkey),
|
||||||
|
Follow(Pubkey),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'd> ProfileView<'a, 'd> {
|
impl<'a, 'd> ProfileView<'a, 'd> {
|
||||||
@@ -79,8 +82,8 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
.ndb
|
.ndb
|
||||||
.get_profile_by_pubkey(&txn, self.pubkey.bytes())
|
.get_profile_by_pubkey(&txn, self.pubkey.bytes())
|
||||||
{
|
{
|
||||||
if self.profile_body(ui, profile) {
|
if let Some(profile_view_action) = self.profile_body(ui, profile) {
|
||||||
action = Some(ProfileViewAction::EditProfile);
|
action = Some(profile_view_action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let profile_timeline = self
|
let profile_timeline = self
|
||||||
@@ -131,8 +134,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
output.inner
|
output.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
fn profile_body(&mut self, ui: &mut egui::Ui, profile: ProfileRecord<'_>) -> bool {
|
fn profile_body(
|
||||||
let mut action = false;
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
profile: ProfileRecord<'_>,
|
||||||
|
) -> Option<ProfileViewAction> {
|
||||||
|
let mut action = None;
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
banner(
|
banner(
|
||||||
ui,
|
ui,
|
||||||
@@ -169,13 +176,49 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
ui.ctx().copy_text(to_copy)
|
ui.ctx().copy_text(to_copy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.accounts.contains_full_kp(self.pubkey) {
|
ui.with_layout(Layout::right_to_left(egui::Align::RIGHT), |ui| {
|
||||||
ui.with_layout(Layout::right_to_left(egui::Align::Max), |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() {
|
if ui.add(edit_profile_button()).clicked() {
|
||||||
action = true;
|
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);
|
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) {
|
fn handle_link(ui: &mut egui::Ui, website_url: &str) {
|
||||||
let img = if ui.visuals().dark_mode {
|
let img = if ui.visuals().dark_mode {
|
||||||
app_images::link_image()
|
app_images::link_image()
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ fn timeline_ui(
|
|||||||
error!("tried to render timeline in column, but timeline was missing");
|
error!("tried to render timeline in column, but timeline was missing");
|
||||||
// TODO (jb55): render error when timeline is missing?
|
// TODO (jb55): render error when timeline is missing?
|
||||||
// this shouldn't happen...
|
// 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;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,7 @@
|
|||||||
use egui::{Button, Widget};
|
use egui::Widget;
|
||||||
use notedeck::NotedeckTextStyle;
|
use notedeck_ui::widgets::styled_button_toggleable;
|
||||||
|
|
||||||
/// Sized and styled to match the figma design
|
/// Sized and styled to match the figma design
|
||||||
pub fn styled_button(text: &str, fill_color: egui::Color32) -> impl Widget + '_ {
|
pub fn styled_button(text: &str, fill_color: egui::Color32) -> impl Widget + '_ {
|
||||||
styled_button_toggleable(text, fill_color, true)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ pub use picture::ProfilePic;
|
|||||||
pub use preview::ProfilePreview;
|
pub use preview::ProfilePreview;
|
||||||
|
|
||||||
use egui::{load::TexturePoll, Label, RichText};
|
use egui::{load::TexturePoll, Label, RichText};
|
||||||
use notedeck::{NostrName, NotedeckTextStyle};
|
use notedeck::{IsFollowing, NostrName, NotedeckTextStyle};
|
||||||
|
|
||||||
use crate::app_images;
|
use crate::{app_images, colors, widgets::styled_button_toggleable};
|
||||||
|
|
||||||
pub fn display_name_widget<'a>(
|
pub fn display_name_widget<'a>(
|
||||||
name: &'a NostrName<'a>,
|
name: &'a NostrName<'a>,
|
||||||
@@ -115,3 +115,16 @@ pub fn banner(ui: &mut egui::Ui, banner_url: Option<&str>, height: f32) -> egui:
|
|||||||
.unwrap_or_else(|| ui.label(""))
|
.unwrap_or_else(|| ui.label(""))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn follow_button(following: IsFollowing) -> impl egui::Widget + 'static {
|
||||||
|
move |ui: &mut egui::Ui| -> egui::Response {
|
||||||
|
let (bg_color, text) = match following {
|
||||||
|
IsFollowing::Unknown => (ui.visuals().noninteractive().bg_fill, "Unknown"),
|
||||||
|
IsFollowing::Yes => (ui.visuals().widgets.inactive.bg_fill, "Unfollow"),
|
||||||
|
IsFollowing::No => (colors::PINK, "Follow"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let enabled = following != IsFollowing::Unknown;
|
||||||
|
ui.add(styled_button_toggleable(text, bg_color, enabled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
|
use crate::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
|
||||||
use egui::{emath::GuiRounding, Pos2, Stroke};
|
use egui::{emath::GuiRounding, Pos2, Stroke};
|
||||||
|
use notedeck::NotedeckTextStyle;
|
||||||
|
|
||||||
pub fn x_button(rect: egui::Rect) -> impl egui::Widget {
|
pub fn x_button(rect: egui::Rect) -> impl egui::Widget {
|
||||||
move |ui: &mut egui::Ui| -> egui::Response {
|
move |ui: &mut egui::Ui| -> egui::Response {
|
||||||
@@ -33,3 +34,46 @@ pub fn x_button(rect: egui::Rect) -> impl egui::Widget {
|
|||||||
helper.take_animation_response()
|
helper.take_animation_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Button styled in the Notedeck theme
|
||||||
|
pub fn styled_button_toggleable(
|
||||||
|
text: &str,
|
||||||
|
fill_color: egui::Color32,
|
||||||
|
enabled: bool,
|
||||||
|
) -> impl egui::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::Button.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 = egui::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