Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
97b6755504
|
|||
| c765b031e9 | |||
| 024cf3ef91 | |||
| 3a0da9a3e0 | |||
| 10b62a073b | |||
| ac212b96a6 | |||
| 637b05c1e2 | |||
| f436b49fec | |||
| 04ce29d1dd | |||
| ae1d5ab1c5 | |||
| 7caf77aa1c | |||
| 80ae489967 | |||
| 259c0b677a | |||
| 3b7f1f1b39 | |||
| f2258ab16b | |||
| 571435cf85 | |||
| 8f8ff42156 | |||
| 3a95ba05a8 | |||
| 73e44d1497 | |||
| 43b98fc6ed | |||
| 95ee275153 | |||
| 8bc54cc519 | |||
| 5282373434 | |||
| 14c59a6c94 | |||
| 09238baee0 | |||
| 594072cfb8 | |||
| 2882b1c2d9 | |||
| f4b8d235eb | |||
| cf48b29fd8 | |||
| 2a7c5eb983 | |||
| 72d696beb2 | |||
| dea695fa8e | |||
| fc1caf5eb4 | |||
| 5539e4ef82 | |||
| 408afbda50 | |||
| af4b896739 | |||
| d448caa369 |
Generated
+1
-1
@@ -1555,7 +1555,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui_nav"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/damus-io/egui-nav?rev=de6e2d51892478fdd516df166f866e64dedbae07#de6e2d51892478fdd516df166f866e64dedbae07"
|
||||
source = "git+https://github.com/damus-io/egui-nav?rev=e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9#e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ egui = { version = "0.31.1", features = ["serde"] }
|
||||
egui-wgpu = "0.31.1"
|
||||
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
||||
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "de6e2d51892478fdd516df166f866e64dedbae07" }
|
||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9" }
|
||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
||||
#egui_virtual_list = "0.6.0"
|
||||
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.33337 5.33337V3.46671C5.33337 2.71997 5.33337 2.3466 5.4787 2.06139C5.60653 1.8105 5.8105 1.60653 6.06139 1.4787C6.3466 1.33337 6.71997 1.33337 7.46671 1.33337H12.5334C13.2801 1.33337 13.6535 1.33337 13.9387 1.4787C14.1896 1.60653 14.3936 1.8105 14.5214 2.06139C14.6667 2.3466 14.6667 2.71997 14.6667 3.46671V8.53337C14.6667 9.28011 14.6667 9.65351 14.5214 9.93871C14.3936 10.1896 14.1896 10.3936 13.9387 10.5214C13.6535 10.6667 13.2801 10.6667 12.5334 10.6667H10.6667M3.46671 14.6667H8.53337C9.28011 14.6667 9.65351 14.6667 9.93871 14.5214C10.1896 14.3936 10.3936 14.1896 10.5214 13.9387C10.6667 13.6535 10.6667 13.2801 10.6667 12.5334V7.46671C10.6667 6.71997 10.6667 6.3466 10.5214 6.06139C10.3936 5.8105 10.1896 5.60653 9.93871 5.4787C9.65351 5.33337 9.28011 5.33337 8.53337 5.33337H3.46671C2.71997 5.33337 2.3466 5.33337 2.06139 5.4787C1.8105 5.60653 1.60653 5.8105 1.4787 6.06139C1.33337 6.3466 1.33337 6.71997 1.33337 7.46671V12.5334C1.33337 13.2801 1.33337 13.6535 1.4787 13.9387C1.60653 14.1896 1.8105 14.3936 2.06139 14.5214C2.3466 14.6667 2.71997 14.6667 3.46671 14.6667Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -46,6 +46,9 @@ Add_Hashtag_Column_ebf4 = Add Hashtag Column
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Add Last Notes Column
|
||||
|
||||
# Tooltip text for adding a new deck button
|
||||
Add_new_deck_f2fc = Add new deck
|
||||
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Add Notifications Column
|
||||
|
||||
@@ -136,6 +139,9 @@ Copy_Note_ID_6b45 = Copy Note ID
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copy Note JSON
|
||||
|
||||
# Tooltip text for copying npub to clipboard
|
||||
Copy_npub_to_clipboard_c105 = Copy npub to clipboard
|
||||
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copy Pubkey
|
||||
|
||||
@@ -208,6 +214,9 @@ Display_name_f9d9 = Display name
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
|
||||
|
||||
# Button to indicate that the user is done going through the onboarding process.
|
||||
Done_50dd = Done
|
||||
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Edit Deck
|
||||
|
||||
@@ -283,6 +292,9 @@ k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies
|
||||
|
||||
# label for keys setting section
|
||||
Keys_435f = Keys
|
||||
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Language:
|
||||
|
||||
@@ -310,6 +322,18 @@ Moves_this_column_to_another_position_0d4b = Moves this column to another positi
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = My Deck
|
||||
|
||||
# reaction from user to a note you were tagged in
|
||||
name__reacted_to_a_note_you_were_tagged_in_4b62 = {$name} reacted to a note you were tagged in
|
||||
|
||||
# reaction from user to your note
|
||||
name__reacted_to_your_note_ead9 = {$name} reacted to your note
|
||||
|
||||
# repost from user
|
||||
name__reposted_a_note_you_were_tagged_in_1379 = {$name} reposted a note you were tagged in
|
||||
|
||||
# repost from user
|
||||
name__reposted_your_note_1379 = {$name} reposted your note
|
||||
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = New to Nostr?
|
||||
|
||||
@@ -385,6 +409,9 @@ Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard_
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Profile picture
|
||||
|
||||
# label describing public key
|
||||
PUBLIC_ACCOUNT_ID_4394 = PUBLIC ACCOUNT ID
|
||||
|
||||
# Column title for quote composition
|
||||
Quote_475c = Quote
|
||||
|
||||
@@ -463,6 +490,9 @@ Search_notes_42a6 = Search notes...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Searching for '{$query}'
|
||||
|
||||
# label describing secret key
|
||||
SECRET_ACCOUNT_LOGIN_KEY_8440 = SECRET ACCOUNT LOGIN KEY
|
||||
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = See notes from your contacts
|
||||
|
||||
@@ -609,3 +639,35 @@ Got__count__results_for___query_85fb =
|
||||
[one] Got {$count} result for '{$query}'
|
||||
*[other] Got {$count} results for '{$query}'
|
||||
}
|
||||
|
||||
|
||||
# amount of reactions a note you were tagged in received
|
||||
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
|
||||
{ $count ->
|
||||
[one] {$name} and {$count} other reacted to a note you were tagged in
|
||||
*[other] {$name} and {$count} others reacted to a note you were tagged in
|
||||
}
|
||||
|
||||
|
||||
# describing the amount of reactions your note received
|
||||
name__and__count__others_reacted_to_your_note_0f6a =
|
||||
{ $count ->
|
||||
[one] {$name} and {$count} other reacted to your note
|
||||
*[other] {$name} and {$count} others reacted to your note
|
||||
}
|
||||
|
||||
|
||||
# describing the amount of reposts a note you were tagged in received
|
||||
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
|
||||
{ $count ->
|
||||
[one] {$name} and {$count} other reposted a note you were tagged in
|
||||
*[other] {$name} and {$count} others reposted a note you were tagged in
|
||||
}
|
||||
|
||||
|
||||
# describing the amount of reposts your note received
|
||||
name__and__count__others_reposted_your_note_70a0 =
|
||||
{ $count ->
|
||||
[one] {$name} and {$count} other reposted your note
|
||||
*[other] {$name} and {$count} others reposted your note
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ Add_Hashtag_Column_ebf4 = {"["}Àdd Hàshtàg Çólúmñ{"]"}
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"}
|
||||
|
||||
# Tooltip text for adding a new deck button
|
||||
Add_new_deck_f2fc = {"["}Àdd ñéw déçk{"]"}
|
||||
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
|
||||
|
||||
@@ -136,6 +139,9 @@ Copy_Note_ID_6b45 = {"["}Çópy Ñóté ÍD{"]"}
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
|
||||
|
||||
# Tooltip text for copying npub to clipboard
|
||||
Copy_npub_to_clipboard_c105 = {"["}Çópy ñpúb tó çlípbóàrd{"]"}
|
||||
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
|
||||
|
||||
@@ -208,6 +214,9 @@ Display_name_f9d9 = {"["}Dísplày ñàmé{"]"}
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
|
||||
|
||||
# Button to indicate that the user is done going through the onboarding process.
|
||||
Done_50dd = {"["}Dóñé{"]"}
|
||||
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
|
||||
|
||||
@@ -283,6 +292,9 @@ k_5K_f7e6 = {"["}5K{"]"}
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
|
||||
|
||||
# label for keys setting section
|
||||
Keys_435f = {"["}Kéys{"]"}
|
||||
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = {"["}Làñgúàgé:{"]"}
|
||||
|
||||
@@ -310,6 +322,18 @@ Moves_this_column_to_another_position_0d4b = {"["}Móvés thís çólúmñ tó
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = {"["}My Déçk{"]"}
|
||||
|
||||
# reaction from user to a note you were tagged in
|
||||
name__reacted_to_a_note_you_were_tagged_in_4b62 = {"["}{$name} réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||
|
||||
# reaction from user to your note
|
||||
name__reacted_to_your_note_ead9 = {"["}{$name} réàçtéd tó yóúr ñóté{"]"}
|
||||
|
||||
# repost from user
|
||||
name__reposted_a_note_you_were_tagged_in_1379 = {"["}{$name} répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||
|
||||
# repost from user
|
||||
name__reposted_your_note_1379 = {"["}{$name} répóstéd yóúr ñóté{"]"}
|
||||
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"}
|
||||
|
||||
@@ -385,6 +409,9 @@ Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard_
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
|
||||
|
||||
# label describing public key
|
||||
PUBLIC_ACCOUNT_ID_4394 = {"["}PÚBLÍÇ ÀÇÇÓÚÑT ÍD{"]"}
|
||||
|
||||
# Column title for quote composition
|
||||
Quote_475c = {"["}Qúóté{"]"}
|
||||
|
||||
@@ -463,6 +490,9 @@ Search_notes_42a6 = {"["}Séàrçh ñótés...{"]"}
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
|
||||
|
||||
# label describing secret key
|
||||
SECRET_ACCOUNT_LOGIN_KEY_8440 = {"["}SÉÇRÉT ÀÇÇÓÚÑT LÓGÍÑ KÉY{"]"}
|
||||
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"}
|
||||
|
||||
@@ -609,3 +639,35 @@ Got__count__results_for___query_85fb =
|
||||
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
|
||||
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
|
||||
}
|
||||
|
||||
|
||||
# amount of reactions a note you were tagged in received
|
||||
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
|
||||
{ $count ->
|
||||
[one] {"["}{$name} àñd {$count} óthér réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||
*[other] {"["}{$name} àñd {$count} óthérs réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||
}
|
||||
|
||||
|
||||
# describing the amount of reactions your note received
|
||||
name__and__count__others_reacted_to_your_note_0f6a =
|
||||
{ $count ->
|
||||
[one] {"["}{$name} àñd {$count} óthér réàçtéd tó yóúr ñóté{"]"}
|
||||
*[other] {"["}{$name} àñd {$count} óthérs réàçtéd tó yóúr ñóté{"]"}
|
||||
}
|
||||
|
||||
|
||||
# describing the amount of reposts a note you were tagged in received
|
||||
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
|
||||
{ $count ->
|
||||
[one] {"["}{$name} àñd {$count} óthér répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||
*[other] {"["}{$name} àñd {$count} óthérs répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||
}
|
||||
|
||||
|
||||
# describing the amount of reposts your note received
|
||||
name__and__count__others_reposted_your_note_70a0 =
|
||||
{ $count ->
|
||||
[one] {"["}{$name} àñd {$count} óthér répóstéd yóúr ñóté{"]"}
|
||||
*[other] {"["}{$name} àñd {$count} óthérs répóstéd yóúr ñóté{"]"}
|
||||
}
|
||||
|
||||
@@ -33,15 +33,26 @@ pub enum ZapError {
|
||||
#[error("invalid lud16")]
|
||||
InvalidLud16(String),
|
||||
#[error("invalid endpoint response")]
|
||||
EndpointError(String),
|
||||
EndpointError(EndpointError),
|
||||
#[error("bech encoding/decoding error")]
|
||||
Bech(String),
|
||||
#[error("serialization/deserialization problem")]
|
||||
Serialization(String),
|
||||
#[error("nwc error")]
|
||||
NWC(String),
|
||||
#[error("ndb error")]
|
||||
Ndb(String),
|
||||
}
|
||||
|
||||
impl ZapError {
|
||||
pub fn endpoint_error(error: String) -> ZapError {
|
||||
ZapError::EndpointError(EndpointError(error))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EndpointError(pub String);
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::Generic(s)
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use nwc::nostr::nips::nip47::PayInvoiceResponse;
|
||||
use poll_promise::Promise;
|
||||
use tokio::task::JoinError;
|
||||
use url::Url;
|
||||
|
||||
use crate::{get_wallet_for, Accounts, GlobalWallet, ZapError};
|
||||
|
||||
use super::{
|
||||
networking::{fetch_invoice_lnurl, fetch_invoice_lud16, FetchedInvoice, FetchingInvoice},
|
||||
zap::Zap,
|
||||
use crate::{
|
||||
get_wallet_for,
|
||||
zaps::{
|
||||
get_users_zap_address,
|
||||
networking::{fetch_invoice_promise, FetchedInvoiceResponse, LNUrlPayResponse, PayEntry},
|
||||
},
|
||||
Accounts, GlobalWallet, ZapError,
|
||||
};
|
||||
|
||||
use super::{networking::FetchingInvoice, zap::Zap};
|
||||
|
||||
type ZapId = u32;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -23,11 +30,31 @@ pub struct Zaps {
|
||||
zaps: std::collections::HashMap<ZapId, ZapState>,
|
||||
in_flight: Vec<ZapPromise>,
|
||||
events: Vec<EventResponse>,
|
||||
|
||||
pay_cache: PayCache,
|
||||
}
|
||||
|
||||
/// Cache to hold LNURL payRequest responses from the desired LNURL endpoint
|
||||
#[derive(Default)]
|
||||
pub struct PayCache {
|
||||
// endpoint URL to response
|
||||
pub pay_responses: HashMap<Url, LNUrlPayResponse>,
|
||||
}
|
||||
|
||||
impl PayCache {
|
||||
pub fn get_response(&self, url: &Url) -> Option<&LNUrlPayResponse> {
|
||||
self.pay_responses.get(url)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entry: PayEntry) {
|
||||
self.pay_responses.insert(entry.url, entry.response);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_event(
|
||||
id: ZapId,
|
||||
event: ZapEvent,
|
||||
cache: &PayCache,
|
||||
accounts: &mut Accounts,
|
||||
global_wallet: &mut GlobalWallet,
|
||||
ndb: &Ndb,
|
||||
@@ -37,7 +64,7 @@ fn process_event(
|
||||
ZapEvent::FetchInvoice {
|
||||
zap_ctx,
|
||||
sender_relays,
|
||||
} => process_new_zap_event(zap_ctx, accounts, ndb, txn, sender_relays),
|
||||
} => process_new_zap_event(cache, zap_ctx, accounts, ndb, txn, sender_relays),
|
||||
ZapEvent::SendNWC {
|
||||
zap_ctx,
|
||||
req_noteid,
|
||||
@@ -74,6 +101,7 @@ fn process_event(
|
||||
}
|
||||
|
||||
fn process_new_zap_event(
|
||||
cache: &PayCache,
|
||||
zap_ctx: ZapCtx,
|
||||
accounts: &Accounts,
|
||||
ndb: &Ndb,
|
||||
@@ -96,7 +124,8 @@ fn process_new_zap_event(
|
||||
};
|
||||
|
||||
let id = zap_ctx.id;
|
||||
let promise = send_note_zap(
|
||||
let m_promise = send_note_zap(
|
||||
cache,
|
||||
ndb,
|
||||
txn,
|
||||
note_target,
|
||||
@@ -106,55 +135,41 @@ fn process_new_zap_event(
|
||||
)
|
||||
.map(|promise| ZapPromise::FetchingInvoice {
|
||||
ctx: zap_ctx,
|
||||
promise,
|
||||
promise: Box::new(promise),
|
||||
});
|
||||
let Some(promise) = promise else {
|
||||
|
||||
let promise = match m_promise {
|
||||
Ok(promise) => promise,
|
||||
Err(e) => {
|
||||
return NextState::Event(EventResponse {
|
||||
id,
|
||||
event: Err(ZappingError::InvalidZapAddress),
|
||||
event: Err(ZappingError::InvoiceFetchFailed(e)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
NextState::Transition(promise)
|
||||
}
|
||||
|
||||
fn send_note_zap(
|
||||
cache: &PayCache,
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
note_target: NoteZapTargetOwned,
|
||||
msats: u64,
|
||||
nsec: &[u8; 32],
|
||||
relays: Vec<String>,
|
||||
) -> Option<FetchingInvoice> {
|
||||
let address = get_users_zap_endpoint(txn, ndb, ¬e_target.zap_recipient)?;
|
||||
) -> Result<FetchingInvoice, ZapError> {
|
||||
let address = get_users_zap_address(txn, ndb, ¬e_target.zap_recipient)?;
|
||||
|
||||
let promise = match address {
|
||||
ZapAddress::Lud16(s) => {
|
||||
fetch_invoice_lud16(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays)
|
||||
}
|
||||
ZapAddress::Lud06(s) => {
|
||||
fetch_invoice_lnurl(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays)
|
||||
}
|
||||
};
|
||||
Some(promise)
|
||||
}
|
||||
|
||||
enum ZapAddress {
|
||||
Lud16(String),
|
||||
Lud06(String),
|
||||
}
|
||||
|
||||
fn get_users_zap_endpoint(txn: &Transaction, ndb: &Ndb, receiver: &Pubkey) -> Option<ZapAddress> {
|
||||
let profile = ndb
|
||||
.get_profile_by_pubkey(txn, receiver.bytes())
|
||||
.ok()?
|
||||
.record()
|
||||
.profile()?;
|
||||
|
||||
profile
|
||||
.lud06()
|
||||
.map(|l| ZapAddress::Lud06(l.to_string()))
|
||||
.or(profile.lud16().map(|l| ZapAddress::Lud16(l.to_string())))
|
||||
fetch_invoice_promise(
|
||||
cache,
|
||||
address,
|
||||
msats,
|
||||
*nsec,
|
||||
ZapTargetOwned::Note(note_target),
|
||||
relays,
|
||||
)
|
||||
}
|
||||
|
||||
fn try_get_promise_response(
|
||||
@@ -169,7 +184,7 @@ fn try_get_promise_response(
|
||||
|
||||
match promise {
|
||||
ZapPromise::FetchingInvoice { ctx, promise } => {
|
||||
let result = promise.block_and_take();
|
||||
let result = Box::new(promise.block_and_take());
|
||||
|
||||
Some(PromiseResponse::FetchingInvoice { ctx, result })
|
||||
}
|
||||
@@ -272,6 +287,16 @@ impl Zaps {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let PromiseResponse::FetchingInvoice { ctx: _, result } = &resp {
|
||||
if let Ok(resp) = &**result {
|
||||
if let Some(entry) = &resp.pay_entry {
|
||||
let url = &entry.url;
|
||||
tracing::info!("inserting {url} in pay cache");
|
||||
self.pay_cache.insert(entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.events.push(resp.take_as_event_response());
|
||||
}
|
||||
|
||||
@@ -286,7 +311,15 @@ impl Zaps {
|
||||
};
|
||||
|
||||
let txn = nostrdb::Transaction::new(ndb).expect("txn");
|
||||
match process_event(event_resp.id, event, accounts, global_wallet, ndb, &txn) {
|
||||
match process_event(
|
||||
event_resp.id,
|
||||
event,
|
||||
&self.pay_cache,
|
||||
accounts,
|
||||
global_wallet,
|
||||
ndb,
|
||||
&txn,
|
||||
) {
|
||||
NextState::Event(event_resp) => {
|
||||
self.zaps
|
||||
.insert(event_resp.id, ZapState::Pending(event_resp.event));
|
||||
@@ -483,7 +516,7 @@ impl std::fmt::Display for ZappingError {
|
||||
enum ZapPromise {
|
||||
FetchingInvoice {
|
||||
ctx: ZapCtx,
|
||||
promise: Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>,
|
||||
promise: Box<Promise<Result<FetchedInvoiceResponse, JoinError>>>,
|
||||
},
|
||||
SendingNWCInvoice {
|
||||
ctx: SendingNWCInvoiceContext,
|
||||
@@ -494,7 +527,7 @@ enum ZapPromise {
|
||||
enum PromiseResponse {
|
||||
FetchingInvoice {
|
||||
ctx: ZapCtx,
|
||||
result: Result<Result<FetchedInvoice, ZapError>, JoinError>,
|
||||
result: Box<Result<FetchedInvoiceResponse, JoinError>>,
|
||||
},
|
||||
SendingNWCInvoice {
|
||||
ctx: SendingNWCInvoiceContext,
|
||||
@@ -507,8 +540,8 @@ impl PromiseResponse {
|
||||
match self {
|
||||
PromiseResponse::FetchingInvoice { ctx, result } => {
|
||||
let id = ctx.id;
|
||||
let event = match result {
|
||||
Ok(r) => match r {
|
||||
let event = match *result {
|
||||
Ok(r) => match r.invoice {
|
||||
Ok(invoice) => Ok(ZapEvent::SendNWC {
|
||||
zap_ctx: ctx,
|
||||
req_noteid: invoice.request_noteid,
|
||||
|
||||
@@ -11,3 +11,39 @@ pub use default_zap::{
|
||||
get_current_default_msats, DefaultZapError, DefaultZapMsats, PendingDefaultZapState,
|
||||
UserZapMsats,
|
||||
};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
|
||||
use crate::ZapError;
|
||||
|
||||
pub enum ZapAddress {
|
||||
Lud16(String),
|
||||
Lud06(String),
|
||||
}
|
||||
|
||||
pub fn get_users_zap_address(
|
||||
txn: &Transaction,
|
||||
ndb: &Ndb,
|
||||
receiver: &Pubkey,
|
||||
) -> Result<ZapAddress, ZapError> {
|
||||
let Some(profile) = ndb
|
||||
.get_profile_by_pubkey(txn, receiver.bytes())
|
||||
.map_err(|e| ZapError::Ndb(e.to_string()))?
|
||||
.record()
|
||||
.profile()
|
||||
else {
|
||||
return Err(ZapError::Ndb(format!("No profile for {receiver}")));
|
||||
};
|
||||
|
||||
let Some(address) = profile
|
||||
.lud06()
|
||||
.map(|l| ZapAddress::Lud06(l.to_string()))
|
||||
.or(profile.lud16().map(|l| ZapAddress::Lud16(l.to_string())))
|
||||
else {
|
||||
return Err(ZapError::Ndb(format!(
|
||||
"profile for {receiver} doesn't have lud06 or lud16"
|
||||
)));
|
||||
};
|
||||
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use crate::{zaps::ZapTargetOwned, ZapError};
|
||||
use enostr::NoteId;
|
||||
use crate::{
|
||||
error::EndpointError,
|
||||
zaps::{cache::PayCache, ZapAddress, ZapTargetOwned},
|
||||
ZapError,
|
||||
};
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use nostrdb::NoteBuilder;
|
||||
use poll_promise::Promise;
|
||||
use serde::Deserialize;
|
||||
@@ -11,15 +15,20 @@ pub struct FetchedInvoice {
|
||||
pub request_noteid: NoteId, // note id of kind 9734 request
|
||||
}
|
||||
|
||||
pub type FetchingInvoice = Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>;
|
||||
pub struct FetchedInvoiceResponse {
|
||||
pub invoice: Result<FetchedInvoice, ZapError>,
|
||||
pub pay_entry: Option<PayEntry>,
|
||||
}
|
||||
|
||||
async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayRequest, ZapError> {
|
||||
pub type FetchingInvoice = Promise<Result<FetchedInvoiceResponse, JoinError>>;
|
||||
|
||||
async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayResponseRaw, ZapError> {
|
||||
let (sender, promise) = Promise::new();
|
||||
|
||||
let on_done = move |response: Result<ehttp::Response, String>| {
|
||||
let handle = response.map_err(ZapError::EndpointError).and_then(|resp| {
|
||||
let handle = response.map_err(ZapError::endpoint_error).and_then(|resp| {
|
||||
if !resp.ok {
|
||||
return Err(ZapError::EndpointError(format!(
|
||||
return Err(ZapError::endpoint_error(format!(
|
||||
"bad http response: {}",
|
||||
resp.status_text
|
||||
)));
|
||||
@@ -36,20 +45,9 @@ async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayRequest, ZapError> {
|
||||
tokio::task::block_in_place(|| promise.block_and_take())
|
||||
}
|
||||
|
||||
async fn fetch_pay_req_from_lud16(lud16: &str) -> Result<LNUrlPayRequest, ZapError> {
|
||||
let url = match generate_endpoint_url(lud16) {
|
||||
Ok(url) => url,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
fetch_pay_req_async(&url).await
|
||||
}
|
||||
|
||||
static HRP_LNURL: bech32::Hrp = bech32::Hrp::parse_unchecked("lnurl");
|
||||
|
||||
fn lud16_to_lnurl(lud16: &str) -> Result<String, ZapError> {
|
||||
let endpoint_url = generate_endpoint_url(lud16)?;
|
||||
|
||||
fn endpoint_url_to_lnurl(endpoint_url: &Url) -> Result<String, ZapError> {
|
||||
let url_str = endpoint_url.to_string();
|
||||
let data = url_str.as_bytes();
|
||||
|
||||
@@ -100,7 +98,7 @@ fn make_kind_9734<'a>(
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LNUrlPayRequest {
|
||||
pub struct LNUrlPayResponseRaw {
|
||||
#[allow(dead_code)]
|
||||
#[serde(rename = "allowsNostr")]
|
||||
allow_nostr: bool,
|
||||
@@ -121,57 +119,117 @@ pub struct LNUrlPayRequest {
|
||||
max_sendable: u64,
|
||||
}
|
||||
|
||||
impl From<LNUrlPayResponseRaw> for LNUrlPayResponse {
|
||||
fn from(value: LNUrlPayResponseRaw) -> Self {
|
||||
let nostr_pubkey = Pubkey::from_hex(&value.nostr_pubkey)
|
||||
.map_err(|e: enostr::Error| EndpointError(e.to_string()));
|
||||
|
||||
let callback_url = Url::parse(&value.callback_url)
|
||||
.map_err(|e| EndpointError(format!("invalid callback url: {e}")));
|
||||
|
||||
Self {
|
||||
allow_nostr: value.allow_nostr,
|
||||
nostr_pubkey,
|
||||
callback_url,
|
||||
min_sendable: value.min_sendable,
|
||||
max_sendable: value.max_sendable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LNUrlPayResponse {
|
||||
pub allow_nostr: bool,
|
||||
pub nostr_pubkey: Result<Pubkey, EndpointError>,
|
||||
pub callback_url: Result<Url, EndpointError>,
|
||||
pub min_sendable: u64,
|
||||
pub max_sendable: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PayEntry {
|
||||
pub url: Url,
|
||||
pub response: LNUrlPayResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LNInvoice {
|
||||
#[serde(rename = "pr")]
|
||||
invoice: String,
|
||||
}
|
||||
|
||||
fn endpoint_query_for_invoice<'a>(
|
||||
endpoint_base_url: &'a mut Url,
|
||||
fn endpoint_query_for_invoice(
|
||||
endpoint_base_url: &Url,
|
||||
msats: u64,
|
||||
lnurl: &str,
|
||||
note: nostrdb::Note,
|
||||
) -> Result<&'a Url, ZapError> {
|
||||
) -> Result<Url, ZapError> {
|
||||
let mut new_url = endpoint_base_url.clone();
|
||||
let nostr = note
|
||||
.json()
|
||||
.map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?;
|
||||
|
||||
Ok(endpoint_base_url
|
||||
new_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("amount", &msats.to_string())
|
||||
.append_pair("lnurl", lnurl)
|
||||
.append_pair("nostr", &nostr)
|
||||
.finish())
|
||||
.finish();
|
||||
|
||||
Ok(new_url)
|
||||
}
|
||||
|
||||
pub fn fetch_invoice_lud16(
|
||||
lud16: String,
|
||||
pub fn fetch_invoice_promise(
|
||||
cache: &PayCache,
|
||||
zap_address: ZapAddress,
|
||||
msats: u64,
|
||||
sender_nsec: [u8; 32],
|
||||
target: ZapTargetOwned,
|
||||
relays: Vec<String>,
|
||||
) -> FetchingInvoice {
|
||||
Promise::spawn_async(tokio::spawn(async move {
|
||||
fetch_invoice_lud16_async(&lud16, msats, &sender_nsec, target, relays).await
|
||||
}))
|
||||
) -> Result<FetchingInvoice, ZapError> {
|
||||
let (url, lnurl) = match zap_address {
|
||||
ZapAddress::Lud16(lud16) => {
|
||||
let url = generate_endpoint_url(&lud16)?;
|
||||
let lnurl = endpoint_url_to_lnurl(&url)?;
|
||||
(url, lnurl)
|
||||
}
|
||||
|
||||
pub fn fetch_invoice_lnurl(
|
||||
lnurl: String,
|
||||
msats: u64,
|
||||
sender_nsec: [u8; 32],
|
||||
target: ZapTargetOwned,
|
||||
relays: Vec<String>,
|
||||
) -> FetchingInvoice {
|
||||
Promise::spawn_async(tokio::spawn(async move {
|
||||
let pay_req = match fetch_pay_req_from_lnurl_async(&lnurl).await {
|
||||
Ok(req) => req,
|
||||
Err(e) => return Err(e),
|
||||
ZapAddress::Lud06(lnurl) => (convert_lnurl_to_endpoint_url(&lnurl)?, lnurl),
|
||||
};
|
||||
|
||||
fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, &sender_nsec, relays, target).await
|
||||
}))
|
||||
match cache.get_response(&url) {
|
||||
Some(endpoint_resp) => {
|
||||
tracing::info!("Using existing endpoint response for {url}");
|
||||
let response = endpoint_resp.clone();
|
||||
Ok(Promise::spawn_async(tokio::spawn(async move {
|
||||
fetch_invoice_lnurl_async(
|
||||
&lnurl,
|
||||
PayEntry { url, response },
|
||||
msats,
|
||||
&sender_nsec,
|
||||
relays,
|
||||
target,
|
||||
)
|
||||
.await
|
||||
})))
|
||||
}
|
||||
None => Ok(Promise::spawn_async(tokio::spawn(async move {
|
||||
tracing::info!("querying ln endpoint: {url}");
|
||||
let pay_req = match fetch_pay_req_async(&url).await {
|
||||
Ok(p) => PayEntry {
|
||||
url,
|
||||
response: p.into(),
|
||||
},
|
||||
Err(e) => {
|
||||
return FetchedInvoiceResponse {
|
||||
invoice: Err(e),
|
||||
pay_entry: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetch_invoice_lnurl_async(&lnurl, pay_req, msats, &sender_nsec, relays, target).await
|
||||
}))),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
|
||||
@@ -181,68 +239,96 @@ fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
|
||||
String::from_utf8(data).map_err(|e| ZapError::Bech(format!("string conversion: {e}")))?;
|
||||
|
||||
Url::parse(&url_str)
|
||||
.map_err(|e| ZapError::EndpointError(format!("endpoint url from lnurl is invalid: {e}")))
|
||||
}
|
||||
|
||||
async fn fetch_pay_req_from_lnurl_async(lnurl: &str) -> Result<LNUrlPayRequest, ZapError> {
|
||||
let url = match convert_lnurl_to_endpoint_url(lnurl) {
|
||||
Ok(u) => u,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
fetch_pay_req_async(&url).await
|
||||
.map_err(|e| ZapError::endpoint_error(format!("endpoint url from lnurl is invalid: {e}")))
|
||||
}
|
||||
|
||||
async fn fetch_invoice_lnurl_async(
|
||||
lnurl: &str,
|
||||
pay_req: &LNUrlPayRequest,
|
||||
pay_entry: PayEntry,
|
||||
msats: u64,
|
||||
sender_nsec: &[u8; 32],
|
||||
relays: Vec<String>,
|
||||
target: ZapTargetOwned,
|
||||
) -> Result<FetchedInvoice, ZapError> {
|
||||
//let recipient = Pubkey::from_hex(&pay_req.nostr_pubkey)
|
||||
//.map_err(|e| ZapError::EndpointError(format!("invalid pubkey hex from endpoint: {e}")))?;
|
||||
) -> FetchedInvoiceResponse {
|
||||
if !pay_entry.response.allow_nostr {
|
||||
return FetchedInvoiceResponse {
|
||||
invoice: Err(ZapError::endpoint_error(
|
||||
"endpoint does not allow nostr".to_owned(),
|
||||
)),
|
||||
pay_entry: Some(pay_entry),
|
||||
};
|
||||
}
|
||||
|
||||
let mut base_url = Url::parse(&pay_req.callback_url)
|
||||
.map_err(|e| ZapError::EndpointError(format!("invalid callback url from endpoint: {e}")))?;
|
||||
if let Err(e) = &pay_entry.response.nostr_pubkey {
|
||||
return FetchedInvoiceResponse {
|
||||
invoice: Err(ZapError::EndpointError(e.clone())),
|
||||
pay_entry: Some(pay_entry),
|
||||
};
|
||||
};
|
||||
|
||||
let min_sendable = pay_entry.response.min_sendable;
|
||||
if msats < min_sendable {
|
||||
return FetchedInvoiceResponse {
|
||||
invoice: Err(ZapError::endpoint_error(format!(
|
||||
"zap amount {msats} is less than minimum sendable: {min_sendable} (in msats)"
|
||||
))),
|
||||
pay_entry: Some(pay_entry),
|
||||
};
|
||||
}
|
||||
|
||||
let max_sendable = pay_entry.response.max_sendable;
|
||||
if msats > max_sendable {
|
||||
return FetchedInvoiceResponse {
|
||||
invoice: Err(ZapError::endpoint_error(format!(
|
||||
"zap amount {msats} is greater than maximum sendable: {max_sendable} (in msats)"
|
||||
))),
|
||||
pay_entry: Some(pay_entry),
|
||||
};
|
||||
}
|
||||
|
||||
let base_url = match &pay_entry.response.callback_url {
|
||||
Ok(url) => url.clone(),
|
||||
Err(error) => {
|
||||
return FetchedInvoiceResponse {
|
||||
invoice: Err(ZapError::EndpointError(error.clone())),
|
||||
pay_entry: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let (query, noteid) = {
|
||||
let comment: &str = "";
|
||||
let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target);
|
||||
let noteid = NoteId::new(*note.id());
|
||||
let query = endpoint_query_for_invoice(&mut base_url, msats, lnurl, note)?;
|
||||
let query = match endpoint_query_for_invoice(&base_url, msats, lnurl, note) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
return FetchedInvoiceResponse {
|
||||
invoice: Err(e),
|
||||
pay_entry: Some(pay_entry),
|
||||
}
|
||||
}
|
||||
};
|
||||
(query, noteid)
|
||||
};
|
||||
|
||||
let res = fetch_invoice(query).await;
|
||||
res.map(|i| FetchedInvoice {
|
||||
invoice: i.invoice,
|
||||
let res = fetch_ln_invoice(&query).await;
|
||||
FetchedInvoiceResponse {
|
||||
invoice: res.map(|r| FetchedInvoice {
|
||||
invoice: r.invoice,
|
||||
request_noteid: noteid,
|
||||
})
|
||||
}),
|
||||
pay_entry: Some(pay_entry),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_invoice_lud16_async(
|
||||
lud16: &str,
|
||||
msats: u64,
|
||||
sender_nsec: &[u8; 32],
|
||||
target: ZapTargetOwned,
|
||||
relays: Vec<String>,
|
||||
) -> Result<FetchedInvoice, ZapError> {
|
||||
let pay_req = fetch_pay_req_from_lud16(lud16).await?;
|
||||
|
||||
let lnurl = lud16_to_lnurl(lud16)?;
|
||||
|
||||
fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, sender_nsec, relays, target).await
|
||||
}
|
||||
|
||||
async fn fetch_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
|
||||
async fn fetch_ln_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
|
||||
let request = ehttp::Request::get(req);
|
||||
let (sender, promise) = Promise::new();
|
||||
let on_done = move |response: Result<ehttp::Response, String>| {
|
||||
let handle = response.map_err(ZapError::EndpointError).and_then(|resp| {
|
||||
let handle = response.map_err(ZapError::endpoint_error).and_then(|resp| {
|
||||
if !resp.ok {
|
||||
return Err(ZapError::EndpointError(format!(
|
||||
return Err(ZapError::endpoint_error(format!(
|
||||
"invalid http response: {}",
|
||||
resp.status_text
|
||||
)));
|
||||
@@ -290,25 +376,32 @@ fn generate_endpoint_url(lud16: &str) -> Result<Url, ZapError> {
|
||||
if use_http { "" } else { "s" }
|
||||
);
|
||||
|
||||
Url::parse(&url_str).map_err(|e| ZapError::EndpointError(e.to_string()))
|
||||
Url::parse(&url_str).map_err(|e| ZapError::endpoint_error(e.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enostr::{FullKeypair, NoteId};
|
||||
|
||||
use crate::zaps::networking::convert_lnurl_to_endpoint_url;
|
||||
|
||||
use super::{
|
||||
fetch_invoice_lnurl, fetch_invoice_lud16, fetch_pay_req_from_lud16, lud16_to_lnurl,
|
||||
use crate::zaps::{
|
||||
cache::PayCache,
|
||||
networking::{
|
||||
convert_lnurl_to_endpoint_url, endpoint_url_to_lnurl, fetch_pay_req_async,
|
||||
generate_endpoint_url,
|
||||
},
|
||||
};
|
||||
|
||||
use super::fetch_invoice_promise;
|
||||
|
||||
#[ignore] // don't run this test automatically since it sends real http
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_pay_req() {
|
||||
let lud16 = "jb55@sendsats.lol";
|
||||
|
||||
let maybe_res = fetch_pay_req_from_lud16(lud16).await;
|
||||
let url = generate_endpoint_url(lud16);
|
||||
assert!(url.is_ok());
|
||||
|
||||
let maybe_res = fetch_pay_req_async(&url.unwrap()).await;
|
||||
|
||||
assert!(maybe_res.is_ok());
|
||||
|
||||
@@ -328,7 +421,10 @@ mod tests {
|
||||
fn test_lnurl() {
|
||||
let lud16 = "jb55@sendsats.lol";
|
||||
|
||||
let maybe_lnurl = lud16_to_lnurl(lud16);
|
||||
let url = generate_endpoint_url(lud16);
|
||||
assert!(url.is_ok());
|
||||
|
||||
let maybe_lnurl = endpoint_url_to_lnurl(&url.unwrap());
|
||||
assert!(maybe_lnurl.is_ok());
|
||||
|
||||
let lnurl = maybe_lnurl.unwrap();
|
||||
@@ -344,9 +440,11 @@ mod tests {
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
|
||||
|
||||
let kp = FullKeypair::generate();
|
||||
let mut cache = PayCache::default();
|
||||
let maybe_invoice = rt.block_on(async {
|
||||
fetch_invoice_lud16(
|
||||
"jb55@sendsats.lol".to_owned(),
|
||||
fetch_invoice_promise(
|
||||
&mut cache,
|
||||
crate::zaps::ZapAddress::Lud16("jb55@sendsats.lol".to_owned()),
|
||||
1000,
|
||||
FullKeypair::generate().secret_key.to_secret_bytes(),
|
||||
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
||||
@@ -355,14 +453,18 @@ mod tests {
|
||||
}),
|
||||
vec!["wss://relay.damus.io".to_owned()],
|
||||
)
|
||||
.block_and_take()
|
||||
.map(|p| p.block_and_take())
|
||||
});
|
||||
|
||||
assert!(maybe_invoice.is_ok());
|
||||
let inner = maybe_invoice.unwrap();
|
||||
assert!(inner.is_ok());
|
||||
let invoice = inner.unwrap();
|
||||
assert!(invoice.invoice.starts_with("lnbc"));
|
||||
let inner = inner.unwrap().invoice;
|
||||
assert!(inner.is_ok());
|
||||
|
||||
let inner = inner.unwrap();
|
||||
|
||||
assert!(inner.invoice.starts_with("lnbc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -385,9 +487,11 @@ mod tests {
|
||||
|
||||
let kp = FullKeypair::generate();
|
||||
|
||||
let mut cache = PayCache::default();
|
||||
let maybe_invoice = rt.block_on(async {
|
||||
fetch_invoice_lnurl(
|
||||
lnurl.to_owned(),
|
||||
fetch_invoice_promise(
|
||||
&mut cache,
|
||||
crate::zaps::ZapAddress::Lud06(lnurl.to_owned()),
|
||||
1000,
|
||||
kp.secret_key.to_secret_bytes(),
|
||||
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
||||
@@ -396,11 +500,17 @@ mod tests {
|
||||
}),
|
||||
[relay.to_owned()].to_vec(),
|
||||
)
|
||||
.block_and_take()
|
||||
.map(|p| p.block_and_take())
|
||||
});
|
||||
|
||||
assert!(maybe_invoice.is_ok());
|
||||
let inner = maybe_invoice.unwrap();
|
||||
assert!(inner.is_ok());
|
||||
let inner = inner.unwrap().invoice;
|
||||
assert!(inner.is_ok());
|
||||
|
||||
assert!(maybe_invoice.unwrap().unwrap().invoice.starts_with("lnbc"));
|
||||
let inner = inner.unwrap();
|
||||
|
||||
assert!(inner.invoice.starts_with("lnbc"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +625,7 @@ impl TimelineKind {
|
||||
pub fn notifications_filter(pk: &Pubkey) -> Filter {
|
||||
Filter::new()
|
||||
.pubkeys([pk.bytes()])
|
||||
.kinds([1, 7])
|
||||
.kinds([1, 7, 6])
|
||||
.limit(default_limit())
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ mod unit;
|
||||
|
||||
pub use cache::TimelineCache;
|
||||
pub use kind::{ColumnTitle, PubkeySource, ThreadSelection, TimelineKind};
|
||||
pub use note_units::{InsertionResponse, NoteUnits};
|
||||
pub use note_units::{CompositeType, InsertionResponse, NoteUnits};
|
||||
pub use timeline_units::{TimelineUnits, UnknownPks};
|
||||
pub use unit::{CompositeUnit, NoteUnit, ReactionUnit};
|
||||
pub use unit::{CompositeUnit, NoteUnit, ReactionUnit, RepostUnit};
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||||
pub enum ViewFilter {
|
||||
|
||||
@@ -17,7 +17,7 @@ type StorageIndex = usize;
|
||||
pub struct NoteUnits {
|
||||
reversed: bool,
|
||||
storage: Vec<NoteUnit>,
|
||||
lookup: HashMap<NoteKey, StorageIndex>, // `NoteKey` to index in `NoteUnits::storage`
|
||||
lookup: HashMap<UnitKey, StorageIndex>, // the key to index in `NoteUnits::storage`
|
||||
order: Vec<StorageIndex>, // the sorted order of the `NoteUnit`s in `NoteUnits::storage`
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl NoteUnits {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, k: &NoteKey) -> bool {
|
||||
pub fn contains_key(&self, k: &UnitKey) -> bool {
|
||||
self.lookup.contains_key(k)
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ impl NoteUnits {
|
||||
/// if `NoteUnitFragment::Composite` exists already, it will fold the fragment into the `CompositeUnit`
|
||||
/// otherwise, it will generate the `NoteUnit::CompositeUnit` from the `NoteUnitFragment::Composite`
|
||||
pub fn merge_fragments(&mut self, frags: Vec<NoteUnitFragment>) -> InsertManyResponse {
|
||||
let mut to_build: HashMap<NoteKey, CompositeUnit> = HashMap::new(); // new composites by key
|
||||
let mut to_build: HashMap<CompositeKey, CompositeUnit> = HashMap::new(); // new composites by key
|
||||
let mut singles_to_build: Vec<NoteRef> = Vec::new();
|
||||
let mut singles_seen: HashSet<NoteKey> = HashSet::new();
|
||||
|
||||
@@ -172,7 +172,7 @@ impl NoteUnits {
|
||||
match frag {
|
||||
NoteUnitFragment::Single(note_ref) => {
|
||||
let key = note_ref.key;
|
||||
if self.lookup.contains_key(&key) {
|
||||
if self.lookup.contains_key(&UnitKey::Single(key)) {
|
||||
continue;
|
||||
}
|
||||
if singles_seen.insert(key) {
|
||||
@@ -181,8 +181,9 @@ impl NoteUnits {
|
||||
}
|
||||
NoteUnitFragment::Composite(c_frag) => {
|
||||
let key = c_frag.get_underlying_noteref().key;
|
||||
let composite_type = c_frag.get_type();
|
||||
|
||||
if let Some(&storage_idx) = self.lookup.get(&key) {
|
||||
if let Some(&storage_idx) = self.lookup.get(&UnitKey::Composite(c_frag.key())) {
|
||||
if let Some(NoteUnit::Composite(c_unit)) = self.storage.get_mut(storage_idx)
|
||||
{
|
||||
if c_frag.get_latest_ref() < c_unit.get_latest_ref() {
|
||||
@@ -194,7 +195,10 @@ impl NoteUnits {
|
||||
}
|
||||
// aggregate for new composite
|
||||
use std::collections::hash_map::Entry;
|
||||
match to_build.entry(key) {
|
||||
match to_build.entry(CompositeKey {
|
||||
key,
|
||||
composite_type,
|
||||
}) {
|
||||
Entry::Occupied(mut o) => {
|
||||
c_frag.fold_into(o.get_mut());
|
||||
}
|
||||
@@ -234,6 +238,24 @@ impl NoteUnits {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||
pub struct CompositeKey {
|
||||
pub key: NoteKey,
|
||||
pub composite_type: CompositeType,
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||
pub enum CompositeType {
|
||||
Reaction,
|
||||
Repost,
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||
pub enum UnitKey {
|
||||
Single(NoteKey),
|
||||
Composite(CompositeKey),
|
||||
}
|
||||
|
||||
pub enum InsertManyResponse {
|
||||
Zero,
|
||||
Some {
|
||||
@@ -303,9 +325,9 @@ mod tests {
|
||||
use crate::timeline::{
|
||||
unit::{
|
||||
CompositeFragment, CompositeUnit, NoteUnit, NoteUnitFragment, Reaction,
|
||||
ReactionFragment, ReactionUnit,
|
||||
ReactionFragment, ReactionUnit, RepostFragment,
|
||||
},
|
||||
NoteUnits,
|
||||
NoteUnits, RepostUnit,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -329,7 +351,7 @@ mod tests {
|
||||
Pubkey::new(out)
|
||||
}
|
||||
|
||||
fn build_fragment(&mut self, reacted_to: NoteRef) -> NoteUnitFragment {
|
||||
fn build_reac_frag(&mut self, reacted_to: NoteRef) -> NoteUnitFragment {
|
||||
NoteUnitFragment::Composite(CompositeFragment::Reaction(ReactionFragment {
|
||||
noteref_reacted_to: reacted_to,
|
||||
reaction_note_ref: NoteRef {
|
||||
@@ -343,8 +365,27 @@ mod tests {
|
||||
}))
|
||||
}
|
||||
|
||||
fn fragment(&mut self, reacted_to: NoteRef) -> String {
|
||||
let frag = self.build_fragment(reacted_to);
|
||||
fn build_repost_frag(&mut self, reposting: NoteRef) -> NoteUnitFragment {
|
||||
NoteUnitFragment::Composite(CompositeFragment::Repost(RepostFragment {
|
||||
reposted_noteref: reposting,
|
||||
repost_noteref: self.new_noteref(),
|
||||
reposter: self.random_sender(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn insert_repost(&mut self, reposting: NoteRef) -> String {
|
||||
let repost = self.build_repost_frag(reposting);
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
self.frags.insert(id.clone(), repost.clone());
|
||||
|
||||
self.units.merge_fragments(vec![repost]);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
fn insert_reac_frag(&mut self, reacted_to: NoteRef) -> String {
|
||||
let frag = self.build_reac_frag(reacted_to);
|
||||
let id = Uuid::new_v4().to_string();
|
||||
self.frags.insert(id.clone(), frag.clone());
|
||||
|
||||
@@ -353,9 +394,9 @@ mod tests {
|
||||
id
|
||||
}
|
||||
|
||||
fn fragments_pair(&mut self, reacted_to: NoteRef) -> (String, String) {
|
||||
let frag1 = self.build_fragment(reacted_to);
|
||||
let frag2 = self.build_fragment(reacted_to);
|
||||
fn insert_reac_frag_pair(&mut self, reacted_to: NoteRef) -> (String, String) {
|
||||
let frag1 = self.build_reac_frag(reacted_to);
|
||||
let frag2 = self.build_reac_frag(reacted_to);
|
||||
|
||||
self.units
|
||||
.merge_fragments(vec![frag1.clone(), frag2.clone()]);
|
||||
@@ -368,7 +409,7 @@ mod tests {
|
||||
(id1, id2)
|
||||
}
|
||||
|
||||
fn generate_reaction_note(&mut self) -> NoteRef {
|
||||
fn new_noteref(&mut self) -> NoteRef {
|
||||
NoteRef {
|
||||
key: NoteKey::new(self.counter()),
|
||||
created_at: self.counter(),
|
||||
@@ -420,6 +461,36 @@ mod tests {
|
||||
}))
|
||||
}
|
||||
|
||||
fn expected_reposts(&mut self, ids: Vec<&String>) -> NoteUnit {
|
||||
let mut reposts = BTreeMap::new();
|
||||
let mut reposted_id = None;
|
||||
let mut senders = HashSet::new();
|
||||
for id in ids {
|
||||
let NoteUnitFragment::Composite(CompositeFragment::Repost(repost)) =
|
||||
self.frags.get(id).unwrap()
|
||||
else {
|
||||
panic!("got something other than repost");
|
||||
};
|
||||
|
||||
if let Some(prev_reposted_id) = reposted_id {
|
||||
if prev_reposted_id != repost.reposted_noteref {
|
||||
panic!("internal error");
|
||||
}
|
||||
}
|
||||
|
||||
reposted_id = Some(repost.reposted_noteref);
|
||||
|
||||
reposts.insert(repost.repost_noteref, repost.reposter);
|
||||
senders.insert(repost.reposter);
|
||||
}
|
||||
|
||||
NoteUnit::Composite(CompositeUnit::Repost(RepostUnit {
|
||||
note_reposted: reposted_id.unwrap(),
|
||||
reposts,
|
||||
senders,
|
||||
}))
|
||||
}
|
||||
|
||||
fn expected_single(&mut self, id: &String) -> NoteUnit {
|
||||
let Some(NoteUnitFragment::Single(note_ref)) = self.frags.get(id) else {
|
||||
panic!("fail");
|
||||
@@ -438,6 +509,7 @@ mod tests {
|
||||
match expect {
|
||||
Expect::Single(id) => self.expected_single(id),
|
||||
Expect::Reaction(items) => self.expected_reactions(items),
|
||||
Expect::Repost(items) => self.expected_reposts(items),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -446,17 +518,18 @@ mod tests {
|
||||
enum Expect<'a> {
|
||||
Single(&'a String),
|
||||
Reaction(Vec<&'a String>),
|
||||
Repost(Vec<&'a String>),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
fn test_reactions1() {
|
||||
let mut builder = UnitBuilder::default();
|
||||
let reaction_note = builder.generate_reaction_note();
|
||||
let reaction_note = builder.new_noteref();
|
||||
|
||||
let single0 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single0));
|
||||
|
||||
let reac1 = builder.fragment(reaction_note);
|
||||
let reac1 = builder.insert_reac_frag(reaction_note);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1]));
|
||||
builder.aeq(1, Expect::Single(&single0));
|
||||
|
||||
@@ -465,7 +538,7 @@ mod tests {
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac1]));
|
||||
builder.aeq(2, Expect::Single(&single0));
|
||||
|
||||
let reac2 = builder.fragment(reaction_note);
|
||||
let reac2 = builder.insert_reac_frag(reaction_note);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac2, &reac1]));
|
||||
builder.aeq(1, Expect::Single(&single1));
|
||||
builder.aeq(2, Expect::Single(&single0));
|
||||
@@ -476,7 +549,7 @@ mod tests {
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
builder.aeq(3, Expect::Single(&single0));
|
||||
|
||||
let reac3 = builder.fragment(reaction_note);
|
||||
let reac3 = builder.insert_reac_frag(reaction_note);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1, &reac2, &reac3]));
|
||||
builder.aeq(1, Expect::Single(&single2));
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
@@ -484,19 +557,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test2() {
|
||||
fn test_reactions2() {
|
||||
let mut builder = UnitBuilder::default();
|
||||
let reaction_note1 = builder.generate_reaction_note();
|
||||
let reaction_note2 = builder.generate_reaction_note();
|
||||
let reaction_note1 = builder.new_noteref();
|
||||
let reaction_note2 = builder.new_noteref();
|
||||
|
||||
let single0 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single0));
|
||||
|
||||
let reac1_1 = builder.fragment(reaction_note1);
|
||||
let reac1_1 = builder.insert_reac_frag(reaction_note1);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1_1]));
|
||||
builder.aeq(1, Expect::Single(&single0));
|
||||
|
||||
let reac2_1 = builder.fragment(reaction_note2);
|
||||
let reac2_1 = builder.insert_reac_frag(reaction_note2);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac2_1]));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac1_1]));
|
||||
builder.aeq(2, Expect::Single(&single0));
|
||||
@@ -507,7 +580,7 @@ mod tests {
|
||||
builder.aeq(2, Expect::Reaction(vec![&reac1_1]));
|
||||
builder.aeq(3, Expect::Single(&single0));
|
||||
|
||||
let reac1_2 = builder.fragment(reaction_note1);
|
||||
let reac1_2 = builder.insert_reac_frag(reaction_note1);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1_2, &reac1_1]));
|
||||
builder.aeq(1, Expect::Single(&single1));
|
||||
builder.aeq(2, Expect::Reaction(vec![&reac2_1]));
|
||||
@@ -520,14 +593,14 @@ mod tests {
|
||||
builder.aeq(3, Expect::Reaction(vec![&reac2_1]));
|
||||
builder.aeq(4, Expect::Single(&single0));
|
||||
|
||||
let reac1_3 = builder.fragment(reaction_note1);
|
||||
let reac1_3 = builder.insert_reac_frag(reaction_note1);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1_2, &reac1_1, &reac1_3]));
|
||||
builder.aeq(1, Expect::Single(&single2));
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
builder.aeq(3, Expect::Reaction(vec![&reac2_1]));
|
||||
builder.aeq(4, Expect::Single(&single0));
|
||||
|
||||
let reac2_2 = builder.fragment(reaction_note2);
|
||||
let reac2_2 = builder.insert_reac_frag(reaction_note2);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac2_1, &reac2_2]));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac1_2, &reac1_1, &reac1_3]));
|
||||
builder.aeq(2, Expect::Single(&single2));
|
||||
@@ -536,18 +609,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test3() {
|
||||
fn test_reactions3() {
|
||||
let mut builder = UnitBuilder::default();
|
||||
let reaction_note1 = builder.generate_reaction_note();
|
||||
let reaction_note1 = builder.new_noteref();
|
||||
|
||||
let single1 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single1));
|
||||
|
||||
let reac0 = builder.fragment(reaction_note1);
|
||||
let reac0 = builder.insert_reac_frag(reaction_note1);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac0]));
|
||||
builder.aeq(1, Expect::Single(&single1));
|
||||
|
||||
let (reac1, reac2) = builder.fragments_pair(reaction_note1);
|
||||
let (reac1, reac2) = builder.insert_reac_frag_pair(reaction_note1);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac0, &reac1, &reac2]));
|
||||
builder.aeq(1, Expect::Single(&single1));
|
||||
|
||||
@@ -556,4 +629,40 @@ mod tests {
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac0, &reac1, &reac2]));
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repost() {
|
||||
let mut builder = UnitBuilder::default();
|
||||
let repost_note = builder.new_noteref();
|
||||
|
||||
let single1 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single1));
|
||||
|
||||
let repost1 = builder.insert_repost(repost_note);
|
||||
builder.aeq(0, Expect::Repost(vec![&repost1]));
|
||||
builder.aeq(1, Expect::Single(&single1));
|
||||
|
||||
let single2 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single2));
|
||||
builder.aeq(1, Expect::Repost(vec![&repost1]));
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
|
||||
let reac1 = builder.insert_reac_frag(repost_note);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1]));
|
||||
builder.aeq(1, Expect::Single(&single2));
|
||||
builder.aeq(2, Expect::Repost(vec![&repost1]));
|
||||
builder.aeq(3, Expect::Single(&single1));
|
||||
|
||||
let repost2 = builder.insert_repost(repost_note);
|
||||
builder.aeq(0, Expect::Repost(vec![&repost1, &repost2]));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac1]));
|
||||
builder.aeq(2, Expect::Single(&single2));
|
||||
builder.aeq(3, Expect::Single(&single1));
|
||||
|
||||
let reac2 = builder.insert_reac_frag(repost_note);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1, &reac2]));
|
||||
builder.aeq(1, Expect::Repost(vec![&repost1, &repost2]));
|
||||
builder.aeq(2, Expect::Single(&single2));
|
||||
builder.aeq(3, Expect::Single(&single1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ use notedeck::{NoteCache, NoteRef, UnknownIds};
|
||||
use crate::{
|
||||
actionbar::{process_thread_notes, NewThreadNotes},
|
||||
multi_subscriber::ThreadSubs,
|
||||
timeline::{note_units::NoteUnits, unit::NoteUnit, InsertionResponse},
|
||||
timeline::{
|
||||
note_units::{NoteUnits, UnitKey},
|
||||
unit::NoteUnit,
|
||||
InsertionResponse,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ThreadSelection;
|
||||
@@ -417,6 +421,6 @@ impl SingleNoteUnits {
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, k: &NoteKey) -> bool {
|
||||
self.units.contains_key(k)
|
||||
self.units.contains_key(&UnitKey::Single(*k))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ use std::collections::HashSet;
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Note, NoteKey, Transaction};
|
||||
use notedeck::NoteRef;
|
||||
use notedeck_ui::note::get_reposted_note;
|
||||
|
||||
use crate::timeline::{
|
||||
note_units::{InsertManyResponse, NoteUnits},
|
||||
unit::{CompositeFragment, NoteUnit, NoteUnitFragment, Reaction, ReactionFragment},
|
||||
unit::{
|
||||
CompositeFragment, NoteUnit, NoteUnitFragment, Reaction, ReactionFragment, RepostFragment,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -99,6 +102,15 @@ pub struct NotePayload<'a> {
|
||||
pub key: NoteKey,
|
||||
}
|
||||
|
||||
impl<'a> NotePayload<'a> {
|
||||
pub fn noteref(&self) -> NoteRef {
|
||||
NoteRef {
|
||||
key: self.key,
|
||||
created_at: self.note.created_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_fragment<'a>(
|
||||
payload: &'a NotePayload,
|
||||
ndb: &Ndb,
|
||||
@@ -116,6 +128,7 @@ fn to_fragment<'a>(
|
||||
fragment: NoteUnitFragment::Composite(CompositeFragment::Reaction(r.fragment)),
|
||||
unknown_pk: Some(r.pk),
|
||||
}),
|
||||
6 => to_repost(payload, ndb, txn).map(RepostResponse::into),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -147,10 +160,7 @@ fn to_reaction<'a>(
|
||||
|
||||
let reacted_to_noteid = note_reacted_to?;
|
||||
|
||||
let reaction_note_ref = NoteRef {
|
||||
key: payload.key,
|
||||
created_at: payload.note.created_at(),
|
||||
};
|
||||
let reaction_note_ref = payload.noteref();
|
||||
|
||||
let reacted_to_note = ndb.get_note_by_id(txn, reacted_to_noteid).ok()?;
|
||||
|
||||
@@ -174,5 +184,59 @@ fn to_reaction<'a>(
|
||||
|
||||
pub struct ReactionResponse<'a> {
|
||||
fragment: ReactionFragment,
|
||||
pk: &'a [u8; 32],
|
||||
pk: &'a [u8; 32], // reaction sender
|
||||
}
|
||||
|
||||
pub struct RepostResponse<'a> {
|
||||
fragment: RepostFragment,
|
||||
reposter_pk: &'a [u8; 32],
|
||||
}
|
||||
|
||||
impl<'a> From<RepostResponse<'a>> for NoteUnitFragmentResponse<'a> {
|
||||
fn from(value: RepostResponse<'a>) -> Self {
|
||||
Self {
|
||||
fragment: NoteUnitFragment::Composite(CompositeFragment::Repost(value.fragment)),
|
||||
unknown_pk: Some(value.reposter_pk),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_repost<'a>(
|
||||
payload: &'a NotePayload,
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
) -> Option<RepostResponse<'a>> {
|
||||
let reposted_note = match get_reposted_note(ndb, txn, &payload.note) {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
tracing::error!(
|
||||
"Could not get reposted note for note id {}",
|
||||
enostr::NoteId::new(*payload.note.id()).hex()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let reposted_key = match reposted_note.key() {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
tracing::error!(
|
||||
"Could not get key of reposted note {}",
|
||||
enostr::NoteId::new(*reposted_note.id()).hex()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(RepostResponse {
|
||||
fragment: RepostFragment {
|
||||
reposted_noteref: NoteRef {
|
||||
key: reposted_key,
|
||||
created_at: reposted_note.created_at(),
|
||||
},
|
||||
repost_noteref: payload.noteref(),
|
||||
reposter: Pubkey::new(*payload.note.pubkey()),
|
||||
},
|
||||
reposter_pk: payload.note.pubkey(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::NoteKey;
|
||||
use notedeck::NoteRef;
|
||||
|
||||
use crate::timeline::note_units::{CompositeKey, CompositeType, UnitKey};
|
||||
|
||||
/// A `NoteUnit` represents a cohesive piece of data derived from notes
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NoteUnit {
|
||||
@@ -12,10 +13,10 @@ pub enum NoteUnit {
|
||||
}
|
||||
|
||||
impl NoteUnit {
|
||||
pub fn key(&self) -> NoteKey {
|
||||
pub fn key(&self) -> UnitKey {
|
||||
match self {
|
||||
NoteUnit::Single(note_ref) => note_ref.key,
|
||||
NoteUnit::Composite(clustered_entry) => clustered_entry.key(),
|
||||
NoteUnit::Single(note_ref) => UnitKey::Single(note_ref.key),
|
||||
NoteUnit::Composite(clustered_entry) => UnitKey::Composite(clustered_entry.key()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +25,7 @@ impl NoteUnit {
|
||||
NoteUnit::Single(note_ref) => note_ref,
|
||||
NoteUnit::Composite(clustered) => match clustered {
|
||||
CompositeUnit::Reaction(reaction_entry) => &reaction_entry.note_reacted_to,
|
||||
CompositeUnit::Repost(repost_unit) => &repost_unit.note_reposted,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -60,12 +62,14 @@ impl PartialOrd for NoteUnit {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompositeUnit {
|
||||
Reaction(ReactionUnit),
|
||||
Repost(RepostUnit),
|
||||
}
|
||||
|
||||
impl CompositeUnit {
|
||||
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||
match self {
|
||||
CompositeUnit::Reaction(reaction_unit) => reaction_unit.get_latest_ref(),
|
||||
CompositeUnit::Repost(repost_unit) => repost_unit.get_latest_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,14 +78,23 @@ impl PartialEq for CompositeUnit {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Reaction(l0), Self::Reaction(r0)) => l0 == r0,
|
||||
(Self::Repost(l0), Self::Repost(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompositeUnit {
|
||||
pub fn key(&self) -> NoteKey {
|
||||
pub fn key(&self) -> CompositeKey {
|
||||
match self {
|
||||
CompositeUnit::Reaction(reaction_entry) => reaction_entry.note_reacted_to.key,
|
||||
CompositeUnit::Reaction(reaction_entry) => CompositeKey {
|
||||
key: reaction_entry.note_reacted_to.key,
|
||||
composite_type: CompositeType::Reaction,
|
||||
},
|
||||
CompositeUnit::Repost(repost_unit) => CompositeKey {
|
||||
key: repost_unit.note_reposted.key,
|
||||
composite_type: CompositeType::Repost,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +105,9 @@ impl From<CompositeFragment> for CompositeUnit {
|
||||
CompositeFragment::Reaction(reaction_fragment) => {
|
||||
CompositeUnit::Reaction(reaction_fragment.into())
|
||||
}
|
||||
CompositeFragment::Repost(repost_fragment) => {
|
||||
CompositeUnit::Repost(repost_fragment.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +145,38 @@ impl From<ReactionFragment> for ReactionUnit {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct RepostUnit {
|
||||
pub note_reposted: NoteRef,
|
||||
pub reposts: BTreeMap<NoteRef, Pubkey>, // repost note to sender
|
||||
pub senders: HashSet<Pubkey>,
|
||||
}
|
||||
|
||||
impl RepostUnit {
|
||||
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||
self.reposts
|
||||
.first_key_value()
|
||||
.map(|(r, _)| r)
|
||||
.unwrap_or(&self.note_reposted)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RepostFragment> for RepostUnit {
|
||||
fn from(value: RepostFragment) -> Self {
|
||||
let mut reposts = BTreeMap::new();
|
||||
reposts.insert(value.repost_noteref, value.reposter);
|
||||
|
||||
let mut senders = HashSet::new();
|
||||
senders.insert(value.reposter);
|
||||
|
||||
Self {
|
||||
note_reposted: value.reposted_noteref,
|
||||
reposts,
|
||||
senders,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum NoteUnitFragment {
|
||||
Single(NoteRef),
|
||||
@@ -138,32 +186,62 @@ pub enum NoteUnitFragment {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompositeFragment {
|
||||
Reaction(ReactionFragment),
|
||||
Repost(RepostFragment),
|
||||
}
|
||||
|
||||
impl CompositeFragment {
|
||||
pub fn fold_into(self, unit: &mut CompositeUnit) {
|
||||
match self {
|
||||
CompositeFragment::Reaction(reaction_fragment) => reaction_fragment.fold_into(unit),
|
||||
CompositeFragment::Reaction(reaction_fragment) => {
|
||||
let CompositeUnit::Reaction(reaction_unit) = unit else {
|
||||
tracing::error!("Attempting to fold a reaction fragment into a unit which isn't ReactionUnit. Doing nothing, this should never occur");
|
||||
return;
|
||||
};
|
||||
|
||||
reaction_fragment.fold_into(reaction_unit);
|
||||
}
|
||||
CompositeFragment::Repost(repost_fragment) => {
|
||||
let CompositeUnit::Repost(repost_unit) = unit else {
|
||||
tracing::error!("Attempting to fold a repost fragment into a unit which isn't RepostUnit. Doing nothing, this should never occur");
|
||||
return;
|
||||
};
|
||||
|
||||
repost_fragment.fold_into(repost_unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(&self) -> NoteKey {
|
||||
pub fn key(&self) -> CompositeKey {
|
||||
match self {
|
||||
CompositeFragment::Reaction(reaction_fragment) => {
|
||||
reaction_fragment.reaction_note_ref.key
|
||||
}
|
||||
CompositeFragment::Reaction(reaction) => CompositeKey {
|
||||
key: reaction.noteref_reacted_to.key,
|
||||
composite_type: CompositeType::Reaction,
|
||||
},
|
||||
CompositeFragment::Repost(repost) => CompositeKey {
|
||||
key: repost.reposted_noteref.key,
|
||||
composite_type: CompositeType::Repost,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_underlying_noteref(&self) -> &NoteRef {
|
||||
match self {
|
||||
CompositeFragment::Reaction(reaction_fragment) => &reaction_fragment.noteref_reacted_to,
|
||||
CompositeFragment::Repost(repost_fragment) => &repost_fragment.reposted_noteref,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||
match self {
|
||||
CompositeFragment::Reaction(reaction_fragment) => &reaction_fragment.reaction_note_ref,
|
||||
CompositeFragment::Repost(repost_fragment) => &repost_fragment.repost_noteref,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_type(&self) -> CompositeType {
|
||||
match self {
|
||||
CompositeFragment::Reaction(_) => CompositeType::Reaction,
|
||||
CompositeFragment::Repost(_) => CompositeType::Repost,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,23 +256,18 @@ pub struct ReactionFragment {
|
||||
|
||||
impl ReactionFragment {
|
||||
/// Add all the contents of Self into `CompositeUnit`
|
||||
pub fn fold_into(self, unit: &mut CompositeUnit) {
|
||||
match unit {
|
||||
CompositeUnit::Reaction(reaction_unit) => {
|
||||
if self.noteref_reacted_to != reaction_unit.note_reacted_to {
|
||||
pub fn fold_into(self, unit: &mut ReactionUnit) {
|
||||
if self.noteref_reacted_to != unit.note_reacted_to {
|
||||
tracing::error!("Attempting to fold a reaction fragment into a ReactionUnit which as a different note reacted to: {:?} != {:?}. This should never occur", self.noteref_reacted_to, unit.note_reacted_to);
|
||||
return;
|
||||
}
|
||||
|
||||
if reaction_unit.senders.contains(&self.reaction.sender) {
|
||||
if unit.senders.contains(&self.reaction.sender) {
|
||||
return;
|
||||
}
|
||||
|
||||
reaction_unit.senders.insert(self.reaction.sender);
|
||||
reaction_unit
|
||||
.reactions
|
||||
.insert(self.reaction_note_ref, self.reaction);
|
||||
}
|
||||
}
|
||||
unit.senders.insert(self.reaction.sender);
|
||||
unit.reactions.insert(self.reaction_note_ref, self.reaction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,3 +276,27 @@ pub struct Reaction {
|
||||
pub reaction: String, // can't use char because some emojis are 'grapheme clusters'
|
||||
pub sender: Pubkey,
|
||||
}
|
||||
|
||||
/// Represents a singular repost
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RepostFragment {
|
||||
pub reposted_noteref: NoteRef,
|
||||
pub repost_noteref: NoteRef,
|
||||
pub reposter: Pubkey,
|
||||
}
|
||||
|
||||
impl RepostFragment {
|
||||
pub fn fold_into(self, unit: &mut RepostUnit) {
|
||||
if self.reposted_noteref != unit.note_reposted {
|
||||
tracing::error!("Attempting to fold a repost fragment into a RepostUnit which has a different note reposted: {:?} != {:?}. This should never occur", self.reposted_noteref, unit.note_reposted);
|
||||
return;
|
||||
}
|
||||
|
||||
if unit.senders.contains(&self.reposter) {
|
||||
return;
|
||||
}
|
||||
|
||||
unit.senders.insert(self.reposter);
|
||||
unit.reposts.insert(self.repost_noteref, self.reposter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ fn login_textedit<'a>(
|
||||
text_edit
|
||||
}
|
||||
|
||||
fn eye_button(ui: &mut egui::Ui, is_visible: bool) -> egui::Response {
|
||||
pub fn eye_button(ui: &mut egui::Ui, is_visible: bool) -> egui::Response {
|
||||
let is_dark_mode = ui.visuals().dark_mode;
|
||||
let icon = if is_visible && is_dark_mode {
|
||||
app_images::eye_dark_image()
|
||||
|
||||
@@ -292,10 +292,6 @@ fn show_amount(
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
});
|
||||
|
||||
// let user_changed = cur_input != Some(user_input.clone());
|
||||
ui.memory_mut(|m| m.request_focus(user_input_id));
|
||||
// ui.data_mut(|d| d.insert_temp(id, user_input));
|
||||
}
|
||||
|
||||
const SELECTION_BUTTONS: [ZapSelectionButton; 8] = [
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::mem;
|
||||
|
||||
use egui::{Layout, ScrollArea};
|
||||
use nostrdb::Ndb;
|
||||
use notedeck::{Images, JobPool, JobsCache, Localization};
|
||||
use notedeck::{tr, Images, JobPool, JobsCache, Localization};
|
||||
use notedeck_ui::{
|
||||
colors,
|
||||
nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetAction, Nip51SetWidgetFlags},
|
||||
@@ -107,7 +107,7 @@ impl<'a> FollowPackOnboardingView<'a> {
|
||||
|
||||
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
|
||||
ui.add_space(4.0);
|
||||
if ui.add(styled_button("Done", colors::PINK)).clicked() {
|
||||
if ui.add(styled_button(tr!(self.loc, "Done", "Button to indicate that the user is done going through the onboarding process.").as_str(), colors::PINK)).clicked() {
|
||||
action = Some(OnboardingResponse::FollowPacks(
|
||||
FollowPacksResponse::UserSelectedPacks(mem::take(self.ui_state)),
|
||||
));
|
||||
|
||||
@@ -163,7 +163,10 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
.border(ProfilePic::border_stroke(ui)),
|
||||
);
|
||||
|
||||
if ui.add(copy_key_widget(&pfp_rect)).clicked() {
|
||||
if ui
|
||||
.add(copy_key_widget(&pfp_rect, self.note_context.i18n))
|
||||
.clicked()
|
||||
{
|
||||
let to_copy = if let Some(bech) = self.pubkey.npub() {
|
||||
bech
|
||||
} else {
|
||||
@@ -297,7 +300,10 @@ fn handle_lud16(ui: &mut egui::Ui, lud16: &str) {
|
||||
.on_hover_text(lud16);
|
||||
}
|
||||
|
||||
fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ {
|
||||
fn copy_key_widget<'a>(
|
||||
pfp_rect: &'a egui::Rect,
|
||||
i18n: &'a mut Localization,
|
||||
) -> impl egui::Widget + 'a {
|
||||
|ui: &mut egui::Ui| -> egui::Response {
|
||||
let painter = ui.painter();
|
||||
#[allow(deprecated)]
|
||||
@@ -311,7 +317,11 @@ fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ {
|
||||
ui.id().with("custom_painter"),
|
||||
Sense::click(),
|
||||
)
|
||||
.on_hover_text("Copy npub to clipboard");
|
||||
.on_hover_text(tr!(
|
||||
i18n,
|
||||
"Copy npub to clipboard",
|
||||
"Tooltip text for copying npub to clipboard"
|
||||
));
|
||||
|
||||
let copy_key_rounding = CornerRadius::same(100);
|
||||
let fill_color = if resp.hovered() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use egui::{
|
||||
vec2, Button, Color32, ComboBox, FontId, Frame, Margin, RichText, ScrollArea, ThemePreference,
|
||||
vec2, Button, Color32, ComboBox, CornerRadius, FontId, Frame, Layout, Margin, RichText,
|
||||
ScrollArea, TextEdit, ThemePreference,
|
||||
};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use enostr::NoteId;
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::{
|
||||
@@ -9,9 +11,12 @@ use notedeck::{
|
||||
Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings,
|
||||
SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE,
|
||||
};
|
||||
use notedeck_ui::{NoteOptions, NoteView};
|
||||
use notedeck_ui::{
|
||||
app_images::{copy_to_clipboard_dark_image, copy_to_clipboard_image},
|
||||
AnimationHelper, NoteOptions, NoteView,
|
||||
};
|
||||
|
||||
use crate::{nav::RouterAction, Damus, Route};
|
||||
use crate::{nav::RouterAction, ui::account_login_view::eye_button, Damus, Route};
|
||||
|
||||
const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw";
|
||||
|
||||
@@ -470,6 +475,149 @@ impl<'a> SettingsView<'a> {
|
||||
action
|
||||
}
|
||||
|
||||
fn keys_section(&mut self, ui: &mut egui::Ui) {
|
||||
let title = tr!(
|
||||
self.note_context.i18n,
|
||||
"Keys",
|
||||
"label for keys setting section"
|
||||
);
|
||||
|
||||
settings_group(ui, title, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(
|
||||
richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"PUBLIC ACCOUNT ID",
|
||||
"label describing public key"
|
||||
))
|
||||
.color(ui.visuals().gray_out(ui.visuals().text_color())),
|
||||
);
|
||||
});
|
||||
|
||||
let copy_img = if ui.visuals().dark_mode {
|
||||
copy_to_clipboard_image()
|
||||
} else {
|
||||
copy_to_clipboard_dark_image()
|
||||
};
|
||||
let copy_max_size = vec2(16.0, 16.0);
|
||||
|
||||
if let Some(npub) = self.note_context.accounts.selected_account_pubkey().npub() {
|
||||
item_frame(ui).show(ui, |ui| {
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::exact(24.0))
|
||||
.cell_layout(Layout::left_to_right(egui::Align::Center))
|
||||
.vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(16.0))
|
||||
.cell_layout(Layout::left_to_right(egui::Align::Center))
|
||||
.horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(richtext_small(&npub));
|
||||
});
|
||||
});
|
||||
|
||||
strip.cell(|ui| {
|
||||
let helper = AnimationHelper::new(
|
||||
ui,
|
||||
"copy-to-clipboard-npub",
|
||||
copy_max_size,
|
||||
);
|
||||
|
||||
copy_img.paint_at(ui, helper.scaled_rect());
|
||||
|
||||
if helper.take_animation_response().clicked() {
|
||||
ui.ctx().copy_text(npub);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let Some(filled) = self.note_context.accounts.selected_filled() else {
|
||||
return;
|
||||
};
|
||||
let Some(mut nsec) = bech32::encode::<bech32::Bech32>(
|
||||
bech32::Hrp::parse_unchecked("nsec"),
|
||||
&filled.secret_key.secret_bytes(),
|
||||
)
|
||||
.ok() else {
|
||||
return;
|
||||
};
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(
|
||||
richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"SECRET ACCOUNT LOGIN KEY",
|
||||
"label describing secret key"
|
||||
))
|
||||
.color(ui.visuals().gray_out(ui.visuals().text_color())),
|
||||
);
|
||||
});
|
||||
|
||||
let is_password_id = ui.id().with("is-password");
|
||||
let is_password = ui
|
||||
.ctx()
|
||||
.data_mut(|d| d.get_temp(is_password_id))
|
||||
.unwrap_or(true);
|
||||
|
||||
item_frame(ui).show(ui, |ui| {
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::exact(24.0))
|
||||
.cell_layout(Layout::left_to_right(egui::Align::Center))
|
||||
.vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(48.0))
|
||||
.cell_layout(Layout::left_to_right(egui::Align::Center))
|
||||
.horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
if is_password {
|
||||
ui.add(
|
||||
TextEdit::singleline(&mut nsec)
|
||||
.password(is_password)
|
||||
.interactive(false)
|
||||
.frame(false),
|
||||
);
|
||||
} else {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(richtext_small(&nsec));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
strip.cell(|ui| {
|
||||
let helper = AnimationHelper::new(
|
||||
ui,
|
||||
"copy-to-clipboard-nsec",
|
||||
copy_max_size,
|
||||
);
|
||||
|
||||
copy_img.paint_at(ui, helper.scaled_rect());
|
||||
|
||||
if helper.take_animation_response().clicked() {
|
||||
ui.ctx().copy_text(nsec);
|
||||
}
|
||||
|
||||
if eye_button(ui, is_password).clicked() {
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.insert_temp(is_password_id, !is_password)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||
let mut action = None;
|
||||
|
||||
@@ -509,6 +657,10 @@ impl<'a> SettingsView<'a> {
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
self.keys_section(ui);
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
if let Some(new_action) = self.other_options_section(ui) {
|
||||
action = Some(new_action);
|
||||
}
|
||||
@@ -542,3 +694,10 @@ pub fn format_size(size_bytes: u64) -> String {
|
||||
format!("{:.2} GB", size / GB)
|
||||
}
|
||||
}
|
||||
|
||||
fn item_frame(ui: &egui::Ui) -> egui::Frame {
|
||||
Frame::new()
|
||||
.inner_margin(Margin::same(8))
|
||||
.corner_radius(CornerRadius::same(8))
|
||||
.fill(ui.visuals().panel_fill)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ impl<'a> DesktopSidePanel<'a> {
|
||||
.color(ui.visuals().noninteractive().fg_stroke.color),
|
||||
));
|
||||
ui.add_space(8.0);
|
||||
let add_deck_resp = ui.add(add_deck_button());
|
||||
let add_deck_resp = ui.add(add_deck_button(self.i18n));
|
||||
|
||||
let decks_inner = ScrollArea::vertical()
|
||||
.max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0)))
|
||||
@@ -383,7 +383,7 @@ pub fn search_button() -> impl Widget {
|
||||
|
||||
// TODO: convert to responsive button when expanded side panel impl is finished
|
||||
|
||||
fn add_deck_button() -> impl Widget {
|
||||
fn add_deck_button<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
|
||||
|ui: &mut egui::Ui| -> egui::Response {
|
||||
let img_size = 40.0;
|
||||
|
||||
@@ -403,7 +403,11 @@ fn add_deck_button() -> impl Widget {
|
||||
helper
|
||||
.take_animation_response()
|
||||
.on_hover_cursor(CursorIcon::PointingHand)
|
||||
.on_hover_text("Add new deck")
|
||||
.on_hover_text(tr!(
|
||||
i18n,
|
||||
"Add new deck",
|
||||
"Tooltip text for adding a new deck button"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,15 @@ use enostr::Pubkey;
|
||||
use nostrdb::{ProfileRecord, Transaction};
|
||||
use notedeck::name::get_display_name;
|
||||
use notedeck::ui::is_narrow;
|
||||
use notedeck::{JobsCache, Muted, NoteRef};
|
||||
use notedeck_ui::app_images::like_image;
|
||||
use notedeck::{tr_plural, JobsCache, Muted, NoteRef};
|
||||
use notedeck_ui::app_images::{like_image, repost_image};
|
||||
use notedeck_ui::ProfilePic;
|
||||
use std::f32::consts::PI;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::timeline::{
|
||||
CompositeUnit, NoteUnit, ReactionUnit, TimelineCache, TimelineKind, TimelineTab, ViewFilter,
|
||||
CompositeType, CompositeUnit, NoteUnit, ReactionUnit, RepostUnit, TimelineCache, TimelineKind,
|
||||
TimelineTab, ViewFilter,
|
||||
};
|
||||
use notedeck::{
|
||||
note::root_note_id_from_selected_id, tr, Localization, NoteAction, NoteContext, ScrollInfo,
|
||||
@@ -459,11 +460,147 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
||||
self.txn,
|
||||
reaction_unit,
|
||||
),
|
||||
CompositeUnit::Repost(repost_unit) => render_repost_cluster(
|
||||
ui,
|
||||
self.note_context,
|
||||
self.note_options,
|
||||
self.jobs,
|
||||
mute,
|
||||
self.txn,
|
||||
repost_unit,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ReferencedNoteType {
|
||||
Tagged,
|
||||
Yours,
|
||||
}
|
||||
|
||||
impl CompositeType {
|
||||
fn image(&self, darkmode: bool) -> egui::Image<'static> {
|
||||
match self {
|
||||
CompositeType::Reaction => like_image(),
|
||||
CompositeType::Repost => repost_image(darkmode),
|
||||
}
|
||||
}
|
||||
|
||||
fn description(
|
||||
&self,
|
||||
loc: &mut Localization,
|
||||
first_name: &str,
|
||||
total_count: usize,
|
||||
referenced_type: ReferencedNoteType,
|
||||
) -> String {
|
||||
let count = total_count - 1;
|
||||
|
||||
match self {
|
||||
CompositeType::Reaction => {
|
||||
reaction_description(loc, first_name, count, referenced_type)
|
||||
}
|
||||
CompositeType::Repost => repost_description(loc, first_name, count, referenced_type),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reaction_description(
|
||||
loc: &mut Localization,
|
||||
first_name: &str,
|
||||
count: usize,
|
||||
referenced_type: ReferencedNoteType,
|
||||
) -> String {
|
||||
match referenced_type {
|
||||
ReferencedNoteType::Tagged => {
|
||||
if count == 0 {
|
||||
tr!(
|
||||
loc,
|
||||
"{name} reacted to a note you were tagged in",
|
||||
"reaction from user to a note you were tagged in",
|
||||
name = first_name
|
||||
)
|
||||
} else {
|
||||
tr_plural!(
|
||||
loc,
|
||||
"{name} and {count} other reacted to a note you were tagged in",
|
||||
"{name} and {count} others reacted to a note you were tagged in",
|
||||
"amount of reactions a note you were tagged in received",
|
||||
count,
|
||||
name = first_name
|
||||
)
|
||||
}
|
||||
}
|
||||
ReferencedNoteType::Yours => {
|
||||
if count == 0 {
|
||||
tr!(
|
||||
loc,
|
||||
"{name} reacted to your note",
|
||||
"reaction from user to your note",
|
||||
name = first_name
|
||||
)
|
||||
} else {
|
||||
tr_plural!(
|
||||
loc,
|
||||
"{name} and {count} other reacted to your note",
|
||||
"{name} and {count} others reacted to your note",
|
||||
"describing the amount of reactions your note received",
|
||||
count,
|
||||
name = first_name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn repost_description(
|
||||
loc: &mut Localization,
|
||||
first_name: &str,
|
||||
count: usize,
|
||||
referenced_type: ReferencedNoteType,
|
||||
) -> String {
|
||||
match referenced_type {
|
||||
ReferencedNoteType::Tagged => {
|
||||
if count == 0 {
|
||||
tr!(
|
||||
loc,
|
||||
"{name} reposted a note you were tagged in",
|
||||
"repost from user",
|
||||
name = first_name
|
||||
)
|
||||
} else {
|
||||
tr_plural!(
|
||||
loc,
|
||||
"{name} and {count} other reposted a note you were tagged in",
|
||||
"{name} and {count} others reposted a note you were tagged in",
|
||||
"describing the amount of reposts a note you were tagged in received",
|
||||
count,
|
||||
name = first_name
|
||||
)
|
||||
}
|
||||
}
|
||||
ReferencedNoteType::Yours => {
|
||||
if count == 0 {
|
||||
tr!(
|
||||
loc,
|
||||
"{name} reposted your note",
|
||||
"repost from user",
|
||||
name = first_name
|
||||
)
|
||||
} else {
|
||||
tr_plural!(
|
||||
loc,
|
||||
"{name} and {count} other reposted your note",
|
||||
"{name} and {count} others reposted your note",
|
||||
"describing the amount of reposts your note received",
|
||||
count,
|
||||
name = first_name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_note(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
@@ -534,10 +671,30 @@ fn render_reaction_cluster(
|
||||
})
|
||||
.collect();
|
||||
|
||||
render_composite_entry(
|
||||
ui,
|
||||
note_context,
|
||||
note_options,
|
||||
jobs,
|
||||
reacted_to_note,
|
||||
profiles_to_show,
|
||||
CompositeType::Reaction,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_composite_entry(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
note_options: NoteOptions,
|
||||
jobs: &mut JobsCache,
|
||||
underlying_note: nostrdb::Note<'_>,
|
||||
profiles_to_show: Vec<ProfileEntry>,
|
||||
composite_type: CompositeType,
|
||||
) -> RenderEntryResponse {
|
||||
let first_name = get_display_name(profiles_to_show.iter().find_map(|opt| opt.record.as_ref()))
|
||||
.name()
|
||||
.to_string();
|
||||
let num_profiles_other = profiles_to_show.len() - 1;
|
||||
let num_profiles = profiles_to_show.len();
|
||||
|
||||
let mut action = None;
|
||||
egui::Frame::new()
|
||||
@@ -549,7 +706,10 @@ fn render_reaction_cluster(
|
||||
|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.add_sized(vec2(28.0, 28.0), like_image());
|
||||
ui.add_sized(
|
||||
vec2(28.0, 28.0),
|
||||
composite_type.image(ui.visuals().dark_mode),
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
@@ -577,17 +737,17 @@ fn render_reaction_cluster(
|
||||
},
|
||||
);
|
||||
|
||||
let note_type_desc = if note_context
|
||||
let referenced_type = if note_context
|
||||
.accounts
|
||||
.get_selected_account()
|
||||
.key
|
||||
.pubkey
|
||||
.bytes()
|
||||
!= reacted_to_note.pubkey()
|
||||
!= underlying_note.pubkey()
|
||||
{
|
||||
"note you were tagged in"
|
||||
ReferencedNoteType::Tagged
|
||||
} else {
|
||||
"your note"
|
||||
ReferencedNoteType::Yours
|
||||
};
|
||||
|
||||
ui.add_space(2.0);
|
||||
@@ -595,13 +755,12 @@ fn render_reaction_cluster(
|
||||
ui.add_space(52.0);
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if num_profiles_other > 0 {
|
||||
ui.label(format!(
|
||||
"{first_name} and {num_profiles_other} others reacted to {note_type_desc}",
|
||||
));
|
||||
} else {
|
||||
ui.label(format!("{first_name} reacted to {note_type_desc}"));
|
||||
}
|
||||
ui.label(composite_type.description(
|
||||
note_context.i18n,
|
||||
&first_name,
|
||||
num_profiles,
|
||||
referenced_type,
|
||||
))
|
||||
});
|
||||
});
|
||||
|
||||
@@ -612,7 +771,7 @@ fn render_reaction_cluster(
|
||||
let options = note_options
|
||||
.difference(NoteOptions::ActionBar | NoteOptions::OptionsButton)
|
||||
.union(NoteOptions::NotificationPreview);
|
||||
let resp = NoteView::new(note_context, &reacted_to_note, options, jobs).show(ui);
|
||||
let resp = NoteView::new(note_context, &underlying_note, options, jobs).show(ui);
|
||||
|
||||
if let Some(note_action) = resp.action {
|
||||
action = Some(note_action);
|
||||
@@ -624,6 +783,44 @@ fn render_reaction_cluster(
|
||||
RenderEntryResponse::Success(action)
|
||||
}
|
||||
|
||||
fn render_repost_cluster(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
note_options: NoteOptions,
|
||||
jobs: &mut JobsCache,
|
||||
mute: &std::sync::Arc<Muted>,
|
||||
txn: &Transaction,
|
||||
repost: &RepostUnit,
|
||||
) -> RenderEntryResponse {
|
||||
let reposted_key = repost.note_reposted.key;
|
||||
let reposted_note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, reposted_key) {
|
||||
note
|
||||
} else {
|
||||
warn!("failed to query note {:?}", reposted_key);
|
||||
return RenderEntryResponse::Unsuccessful;
|
||||
};
|
||||
|
||||
let profiles_to_show: Vec<ProfileEntry> = repost
|
||||
.reposts
|
||||
.values()
|
||||
.filter(|r| !mute.is_pk_muted(r.bytes()))
|
||||
.map(|p| ProfileEntry {
|
||||
record: note_context.ndb.get_profile_by_pubkey(txn, p.bytes()).ok(),
|
||||
pk: p,
|
||||
})
|
||||
.collect();
|
||||
|
||||
render_composite_entry(
|
||||
ui,
|
||||
note_context,
|
||||
note_options,
|
||||
jobs,
|
||||
reposted_note,
|
||||
profiles_to_show,
|
||||
CompositeType::Repost,
|
||||
)
|
||||
}
|
||||
|
||||
enum RenderEntryResponse {
|
||||
Unsuccessful,
|
||||
Success(Option<NoteAction>),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{Pos2, Rect, Response, Sense};
|
||||
use egui::{vec2, Pos2, Rect, Response, Sense};
|
||||
|
||||
pub fn hover_expand(
|
||||
ui: &mut egui::Ui,
|
||||
@@ -116,6 +116,16 @@ impl AnimationHelper {
|
||||
self.rect
|
||||
}
|
||||
|
||||
pub fn scaled_rect(&self) -> egui::Rect {
|
||||
let min_height = self.rect.height() * (1.0 / self.expansion_multiple);
|
||||
let min_width = self.rect.width() * (1.0 / self.expansion_multiple);
|
||||
|
||||
egui::Rect::from_center_size(
|
||||
self.center,
|
||||
vec2(self.scale_1d_pos(min_width), self.scale_1d_pos(min_height)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn center(&self) -> Pos2 {
|
||||
self.rect.center()
|
||||
}
|
||||
|
||||
@@ -183,6 +183,14 @@ pub fn repost_light_image() -> Image<'static> {
|
||||
Image::new(include_image!("../../../assets/icons/repost_light_4x.png"))
|
||||
}
|
||||
|
||||
pub fn repost_image(dark_mode: bool) -> Image<'static> {
|
||||
if dark_mode {
|
||||
repost_dark_image()
|
||||
} else {
|
||||
repost_light_image()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reply_dark_image() -> Image<'static> {
|
||||
Image::new(include_image!("../../../assets/icons/reply.png"))
|
||||
}
|
||||
@@ -244,3 +252,13 @@ pub fn zap_light_image() -> Image<'static> {
|
||||
pub fn like_image() -> Image<'static> {
|
||||
Image::new(include_image!("../../../assets/icons/like_icon_4x.png"))
|
||||
}
|
||||
|
||||
pub fn copy_to_clipboard_image() -> Image<'static> {
|
||||
Image::new(include_image!(
|
||||
"../../../assets/icons/copy-to-clipboard.svg"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn copy_to_clipboard_dark_image() -> Image<'static> {
|
||||
copy_to_clipboard_image().tint(Color32::BLACK)
|
||||
}
|
||||
|
||||
@@ -677,7 +677,7 @@ fn get_zapper<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option<Note<'a>> {
|
||||
pub fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option<Note<'a>> {
|
||||
if note.kind() != 6 {
|
||||
return None;
|
||||
}
|
||||
@@ -832,33 +832,14 @@ struct Zapper<'a> {
|
||||
cur_acc: KeypairUnowned<'a>,
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn render_note_actionbar(
|
||||
fn zap_actionbar_button(
|
||||
ui: &mut egui::Ui,
|
||||
zapper: Option<Zapper<'_>>,
|
||||
note_id: &[u8; 32],
|
||||
note_pubkey: &[u8; 32],
|
||||
note_key: NoteKey,
|
||||
zapper: Option<Zapper<'_>>,
|
||||
i18n: &mut Localization,
|
||||
) -> Option<NoteAction> {
|
||||
ui.set_min_height(26.0);
|
||||
ui.spacing_mut().item_spacing.x = 24.0;
|
||||
|
||||
let reply_resp =
|
||||
reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
let quote_resp =
|
||||
quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
let to_noteid = |id: &[u8; 32]| NoteId::new(*id);
|
||||
if reply_resp.clicked() {
|
||||
return Some(NoteAction::Reply(to_noteid(note_id)));
|
||||
}
|
||||
|
||||
if quote_resp.clicked() {
|
||||
return Some(NoteAction::Quote(to_noteid(note_id)));
|
||||
}
|
||||
|
||||
let mut action: Option<NoteAction> = None;
|
||||
let Zapper { zaps, cur_acc } = zapper?;
|
||||
|
||||
let zap_target = ZapTarget::Note(NoteZapTarget {
|
||||
@@ -869,39 +850,75 @@ fn render_note_actionbar(
|
||||
let zap_state = zaps.any_zap_state_for(cur_acc.pubkey.bytes(), zap_target);
|
||||
|
||||
let target = NoteZapTargetOwned {
|
||||
note_id: to_noteid(note_id),
|
||||
note_id: NoteId::new(*note_id),
|
||||
zap_recipient: Pubkey::new(*note_pubkey),
|
||||
};
|
||||
|
||||
if zap_state.is_err() {
|
||||
return Some(NoteAction::Zap(ZapAction::ClearError(target)));
|
||||
}
|
||||
|
||||
let zap_resp = {
|
||||
cur_acc.secret_key.as_ref()?;
|
||||
|
||||
match zap_state {
|
||||
Ok(any_zap_state) => ui.add(zap_button(i18n, any_zap_state, note_id)),
|
||||
Ok(any_zap_state) => {
|
||||
let zap_resp = ui.add(zap_button(i18n, any_zap_state, note_id));
|
||||
|
||||
if zap_resp.secondary_clicked() {
|
||||
action = Some(NoteAction::Zap(ZapAction::CustomizeAmount(target.clone())));
|
||||
}
|
||||
|
||||
if zap_resp.clicked() {
|
||||
action = Some(NoteAction::Zap(ZapAction::Send(ZapTargetAmount {
|
||||
target,
|
||||
specified_msats: None,
|
||||
})));
|
||||
}
|
||||
|
||||
zap_resp
|
||||
}
|
||||
Err(err) => {
|
||||
let (rect, _) = ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
|
||||
ui.add(x_button(rect)).on_hover_text(err.to_string())
|
||||
let x_button = ui.add(x_button(rect)).on_hover_text(err.to_string());
|
||||
|
||||
if x_button.clicked() {
|
||||
action = Some(NoteAction::Zap(ZapAction::ClearError(target.clone())));
|
||||
}
|
||||
x_button
|
||||
}
|
||||
}
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
if zap_resp.secondary_clicked() {
|
||||
return Some(NoteAction::Zap(ZapAction::CustomizeAmount(target)));
|
||||
action
|
||||
}
|
||||
|
||||
if !zap_resp.clicked() {
|
||||
return None;
|
||||
#[profiling::function]
|
||||
fn render_note_actionbar(
|
||||
ui: &mut egui::Ui,
|
||||
zapper: Option<Zapper<'_>>,
|
||||
note_id: &[u8; 32],
|
||||
note_pubkey: &[u8; 32],
|
||||
note_key: NoteKey,
|
||||
i18n: &mut Localization,
|
||||
) -> Option<NoteAction> {
|
||||
let mut action = None;
|
||||
|
||||
ui.set_min_height(26.0);
|
||||
ui.spacing_mut().item_spacing.x = 24.0;
|
||||
|
||||
let reply_resp =
|
||||
reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
let quote_resp =
|
||||
quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
|
||||
if reply_resp.clicked() {
|
||||
action = Some(NoteAction::Reply(NoteId::new(*note_id)));
|
||||
}
|
||||
|
||||
Some(NoteAction::Zap(ZapAction::Send(ZapTargetAmount {
|
||||
target,
|
||||
specified_msats: None,
|
||||
})))
|
||||
if quote_resp.clicked() {
|
||||
action = Some(NoteAction::Quote(NoteId::new(*note_id)));
|
||||
}
|
||||
|
||||
action = zap_actionbar_button(ui, note_id, note_pubkey, zapper, i18n).or(action);
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
|
||||
Reference in New Issue
Block a user