Compare commits
36 Commits
2025-09-04
...
translatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
e3e7d54142
|
|||
|
|
a6d91c43e4 | ||
|
|
19fe3703d9 | ||
|
|
ca67977b82 | ||
|
|
559e9577fc | ||
|
|
563fbb9c4b | ||
|
|
391900d393 | ||
|
|
11700d6217 | ||
|
|
50293a6f34 | ||
|
|
d6182ed7c3 | ||
|
|
a0e9c8b434 | ||
|
|
4ac2e59983 | ||
|
|
3f1a194983 | ||
|
|
a8eaea6509 | ||
|
|
9278c90802 | ||
|
|
02a90eccd1 | ||
|
|
c0fcf53ff6 | ||
|
|
f889b54ed9 | ||
|
|
7b4c96df91 | ||
|
|
eb44637601 | ||
|
|
ea14713b58 | ||
|
|
a5e7880e25 | ||
|
|
409ca68567 | ||
|
|
6cf193b7e3 | ||
|
|
5bb17cd810 | ||
|
|
ba359c95c2 | ||
|
|
e0ed122951 | ||
|
|
e1ad2e231f | ||
|
|
91028929b2 | ||
|
|
2eef34fa1c | ||
|
|
b8eecf0c9a | ||
|
|
1b9e77a1ff | ||
|
|
28634301b8 | ||
|
|
ce0d3e8e88 | ||
|
|
0b4545d598 | ||
|
|
6db03364fd |
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -193,7 +193,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.1",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
@@ -246,7 +246,7 @@ dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
@@ -3542,6 +3542,7 @@ dependencies = [
|
||||
"profiling",
|
||||
"puffin",
|
||||
"puffin_egui",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"secp256k1 0.30.0",
|
||||
"serde",
|
||||
@@ -3683,7 +3684,7 @@ dependencies = [
|
||||
"nostrdb",
|
||||
"notedeck",
|
||||
"notedeck_ui",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -4603,7 +4604,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.3",
|
||||
"lru-slab",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls",
|
||||
@@ -4657,9 +4658,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
@@ -6278,7 +6279,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
|
||||
@@ -19,6 +19,7 @@ chrono = "0.4.40"
|
||||
base32 = "0.4.0"
|
||||
base64 = "0.22.1"
|
||||
rmpv = "1.3.0"
|
||||
rand = "0.9.2"
|
||||
bech32 = { version = "0.11", default-features = false }
|
||||
bitflags = "2.5.0"
|
||||
dirs = "5.0.1"
|
||||
|
||||
@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Agregar columna de notificaciones exter
|
||||
Add_Hashtag_Column_ebf4 = Agregar columna de hashtags
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
|
||||
# Tooltip text for adding a new deck button
|
||||
Add_new_deck_f2fc = Agregar nuevo deck
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Agregar columna de notificaciones
|
||||
# Button label to add a relay
|
||||
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar enlace
|
||||
Copy_Note_ID_6b45 = Copiar ID de nota
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copiar JSON de nota
|
||||
# Tooltip text for copying npub to clipboard
|
||||
Copy_npub_to_clipboard_c105 = Copiar npub al portapapeles
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copiar pubkey
|
||||
# Copy the text content of the note to clipboard
|
||||
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar billetera
|
||||
Display_name_f9d9 = Nombre para mostrar
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
|
||||
# Button to indicate that the user is done going through the onboarding process.
|
||||
Done_50dd = Listo
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Editar deck
|
||||
# Button label to edit a deck
|
||||
@@ -162,7 +168,7 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Buscar usuario
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Font size:
|
||||
Font_size_dd73 = Tamaño de la fuente:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Title for Home column
|
||||
@@ -191,6 +197,8 @@ k_50K_c2dc = 50.000
|
||||
k_5K_f7e6 = 5.000
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
||||
# label for keys setting section
|
||||
Keys_435f = Claves
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Idioma:
|
||||
# Title for last note per user column
|
||||
@@ -209,6 +217,14 @@ Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que n
|
||||
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = Mi deck
|
||||
# reaction from user to a note you were tagged in
|
||||
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } reaccionó a una nota en la que te etiquetaron
|
||||
# reaction from user to your note
|
||||
name__reacted_to_your_note_ead9 = { $name } reaccionó a tu nota
|
||||
# repost from user
|
||||
name__reposted_a_note_you_were_tagged_in_1379 = { $name } volvió a publicar una nota en la que te etiquetaron
|
||||
# repost from user
|
||||
name__reposted_your_note_1379 = { $name } volvió a publicar tu nota
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
|
||||
# NIP-05 identity field label
|
||||
@@ -236,7 +252,9 @@ Notifications_ef56 = Notificaciones
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = ahora
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = On
|
||||
On_f412 = Activado
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = Incorporación
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Abrir correo electrónico
|
||||
# Instruction to open email client
|
||||
@@ -257,6 +275,8 @@ Post_now_8a49 = Publicar ahora
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Imagen de perfil
|
||||
# label describing public key
|
||||
PUBLIC_ACCOUNT_ID_4394 = ID DE CUENTA PÚBLICA
|
||||
# Column title for quote composition
|
||||
Quote_475c = Citar
|
||||
# Error message when quote note cannot be found
|
||||
@@ -290,7 +310,7 @@ Repost_this_note_8e56 = Volver a publicar esta nota
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Publicadas de nuevo
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Reset
|
||||
Reset_4e60 = Restablecer
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Restablecer
|
||||
# Heading for support section
|
||||
@@ -309,10 +329,14 @@ Search_c573 = Búsqueda
|
||||
Search_notes_42a6 = Buscar notas...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Buscando '{ $query }'
|
||||
# label describing secret key
|
||||
SECRET_ACCOUNT_LOGIN_KEY_8440 = CLAVE DE INICIO DE SESIÓN DE CUENTA SECRETA
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = Seleccionar todo
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Column title for app settings
|
||||
@@ -326,7 +350,7 @@ Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Sort replies newest first:
|
||||
Sort_replies_newest_first_b6c3 = Ordenar las respuestas más recientes primero:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
||||
# Description for hashtags column
|
||||
@@ -352,7 +376,7 @@ Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
||||
# Support email address
|
||||
Support_email_44d9 = Support email:
|
||||
Support_email_44d9 = Correo electrónico de ayuda:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||
# Hover text for light mode toggle button
|
||||
@@ -405,6 +429,30 @@ Zoom_Level_29a8 = Nivel de zoom:
|
||||
# Search results count
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[uno] Obtuvo { $count } resultado para '{ $query }'
|
||||
*[otro] Obtuvo { $count } resultados para '{ $query }'
|
||||
[uno] Se obtuvo { $count } resultado para '{ $query }'
|
||||
*[otro] Se obtuvieron { $count } resultados para '{ $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 } y { $count } persona más reaccionaron a una nota en la que te etiquetaron
|
||||
*[other] { $name } y { $count } personas más reaccionaron a una nota en la que te etiquetaron
|
||||
}
|
||||
# describing the amount of reactions your note received
|
||||
name__and__count__others_reacted_to_your_note_0f6a =
|
||||
{ $count ->
|
||||
[one] { $name } y { $count } persona más reaccionaron a tu nota
|
||||
*[other] { $name } y { $count } personas más reaccionaron a tu nota
|
||||
}
|
||||
# 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 } y { $count } persona más volvieron a publicar una nota en la que te etiquetaron
|
||||
*[other] { $name } y { $count } personas más volvieron a publicar una nota en la que te etiquetaron
|
||||
}
|
||||
# describing the amount of reposts your note received
|
||||
name__and__count__others_reposted_your_note_70a0 =
|
||||
{ $count ->
|
||||
[one] { $name } y { $count } persona más volvieron a publicar tu nota
|
||||
*[other] { $name } y { $count } personas más volvieron a publicar tu nota
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Añadir columna de notificaciones exter
|
||||
Add_Hashtag_Column_ebf4 = Añadir columna de hashtags
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas
|
||||
# Tooltip text for adding a new deck button
|
||||
Add_new_deck_f2fc = Añadir nuevo deck
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Añadir columna de notificaciones
|
||||
# Button label to add a relay
|
||||
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar enlace
|
||||
Copy_Note_ID_6b45 = Copiar ID de nota
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copiar JSON de nota
|
||||
# Tooltip text for copying npub to clipboard
|
||||
Copy_npub_to_clipboard_c105 = Copiar npub al portapapeles
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copiar pubkey
|
||||
# Copy the text content of the note to clipboard
|
||||
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar monedero
|
||||
Display_name_f9d9 = Nombre para mostrar
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
|
||||
# Button to indicate that the user is done going through the onboarding process.
|
||||
Done_50dd = Listo
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Editar deck
|
||||
# Button label to edit a deck
|
||||
@@ -162,7 +168,7 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
||||
# Label for find user button
|
||||
Find_User_bd12 = Buscar usuario
|
||||
# Label for font size, Appearance settings section
|
||||
Font_size_dd73 = Font size:
|
||||
Font_size_dd73 = Tamaño de la fuente:
|
||||
# Title for hashtags column
|
||||
Hashtags_f8e0 = Hashtags
|
||||
# Title for Home column
|
||||
@@ -191,6 +197,8 @@ k_50K_c2dc = 50.000
|
||||
k_5K_f7e6 = 5.000
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
||||
# label for keys setting section
|
||||
Keys_435f = Claves
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Idioma:
|
||||
# Title for last note per user column
|
||||
@@ -209,6 +217,14 @@ Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que n
|
||||
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = Mi deck
|
||||
# reaction from user to a note you were tagged in
|
||||
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } ha reaccionado a una nota en la que te han etiquetado
|
||||
# reaction from user to your note
|
||||
name__reacted_to_your_note_ead9 = { $name } ha reaccionado a tu nota
|
||||
# repost from user
|
||||
name__reposted_a_note_you_were_tagged_in_1379 = { $name } ha vuelto a publicar una nota en la que te han etiquetado
|
||||
# repost from user
|
||||
name__reposted_your_note_1379 = { $name } ha vuelto a publicar tu nota
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
|
||||
# NIP-05 identity field label
|
||||
@@ -236,7 +252,9 @@ Notifications_ef56 = Notificaciones
|
||||
# Relative time for very recent events (less than 3 seconds)
|
||||
now_2181 = ahora
|
||||
# Setting to turn on sorting replies so that the newest are shown first
|
||||
On_f412 = On
|
||||
On_f412 = Activado
|
||||
# Column title for finding users to follow
|
||||
Onboarding_4a25 = Incorporación
|
||||
# Button label to open email client
|
||||
Open_Email_25e9 = Abrir correo electrónico
|
||||
# Instruction to open email client
|
||||
@@ -257,6 +275,8 @@ Post_now_8a49 = Publicar ahora
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Imagen de perfil
|
||||
# label describing public key
|
||||
PUBLIC_ACCOUNT_ID_4394 = ID DE CUENTA PÚBLICA
|
||||
# Column title for quote composition
|
||||
Quote_475c = Citar
|
||||
# Error message when quote note cannot be found
|
||||
@@ -290,7 +310,7 @@ Repost_this_note_8e56 = Volver a publicar esta nota
|
||||
# Label for reposted notes
|
||||
Reposted_61c8 = Publicadas de nuevo
|
||||
# Label for reset note body font size, Appearance settings section
|
||||
Reset_4e60 = Reset
|
||||
Reset_4e60 = Restablecer
|
||||
# Label for reset zoom level, Appearance settings section
|
||||
Reset_62d4 = Restablecer
|
||||
# Heading for support section
|
||||
@@ -309,10 +329,14 @@ Search_c573 = Búsqueda
|
||||
Search_notes_42a6 = Buscar notas...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Buscando '{ $query }'
|
||||
# label describing secret key
|
||||
SECRET_ACCOUNT_LOGIN_KEY_8440 = CLAVE DE INICIO DE SESIÓN DE CUENTA SECRETA
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
|
||||
# Description for universe column
|
||||
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||
# Button to select all profiles in follow pack
|
||||
Select_All_a319 = Seleccionar todo
|
||||
# Button label to send a zap
|
||||
Send_1ea4 = Enviar
|
||||
# Column title for app settings
|
||||
@@ -326,7 +350,7 @@ Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||
# Title for someone else's notifications column
|
||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
||||
# Label for Sort replies newest first, others settings section
|
||||
Sort_replies_newest_first_b6c3 = Sort replies newest first:
|
||||
Sort_replies_newest_first_b6c3 = Ordenar las respuestas más recientes primero:
|
||||
# Description for contact list column
|
||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
||||
# Description for hashtags column
|
||||
@@ -352,7 +376,7 @@ Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||
# Column title for subscribing to individual user
|
||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
||||
# Support email address
|
||||
Support_email_44d9 = Support email:
|
||||
Support_email_44d9 = Correo electrónico de ayuda:
|
||||
# Hover text for dark mode toggle button
|
||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||
# Hover text for light mode toggle button
|
||||
@@ -405,6 +429,30 @@ Zoom_Level_29a8 = Nivel de zoom:
|
||||
# Search results count
|
||||
Got__count__results_for___query_85fb =
|
||||
{ $count ->
|
||||
[uno] Obtuvo { $count } resultado para '{ $query }'
|
||||
*[otro] Obtuvo { $count } resultados para '{ $query }'
|
||||
[uno] Se ha obtenido { $count } resultado para '{ $query }'
|
||||
*[otro] Se han obtenido { $count } resultados para '{ $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 } y { $count } persona más han reaccionado a una nota en la que te han etiquetado
|
||||
*[other] { $name } y { $count } personas más han reaccionado a una nota en la que te han etiquetado
|
||||
}
|
||||
# describing the amount of reactions your note received
|
||||
name__and__count__others_reacted_to_your_note_0f6a =
|
||||
{ $count ->
|
||||
[one] { $name } y { $count } persona más han reaccionado a tu nota
|
||||
*[other] { $name } y { $count } personas más han reaccionado a tu nota
|
||||
}
|
||||
# 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 } y { $count } persona más han vuelto a publicar una nota en la que te han etiquetado
|
||||
*[other] { $name } y { $count } personas más han vuelto a publicar una nota en la que te han etiquetado
|
||||
}
|
||||
# describing the amount of reposts your note received
|
||||
name__and__count__others_reposted_your_note_70a0 =
|
||||
{ $count ->
|
||||
[one] { $name } y { $count } persona han vuelto a publicar tu nota
|
||||
*[other] { $name } y { $count } personas más han vuelto a publicar tu nota
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Ajouter une colonne pour les notificati
|
||||
Add_Hashtag_Column_ebf4 = Ajouter une colonne Hashtag
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Ajouter une colonne pour les dernières notes
|
||||
# Tooltip text for adding a new deck button
|
||||
Add_new_deck_f2fc = Ajouter un nouveau deck
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Ajouter une colonne pour les notifications
|
||||
# Button label to add a relay
|
||||
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copier le lien
|
||||
Copy_Note_ID_6b45 = Copier l'ID de la note
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copier le JSON de la note
|
||||
# Tooltip text for copying npub to clipboard
|
||||
Copy_npub_to_clipboard_c105 = Copier la npub dans le presse-papiers
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copier la Pubkey
|
||||
# Copy the text content of the note to clipboard
|
||||
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Supprimer le portefeuille
|
||||
Display_name_f9d9 = Nom d'utilisateur
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" sera utilisé pour l'identification
|
||||
# Button to indicate that the user is done going through the onboarding process.
|
||||
Done_50dd = Fait
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Modifier le deck
|
||||
# Button label to edit a deck
|
||||
@@ -191,6 +197,8 @@ k_50K_c2dc = 50K
|
||||
k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Gardez une trace de vos notes & réponses
|
||||
# label for keys setting section
|
||||
Keys_435f = Clés
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Langue :
|
||||
# Title for last note per user column
|
||||
@@ -209,6 +217,14 @@ Media_from_someone_you_don_t_follow_5611 = Média d'une personne que vous ne sui
|
||||
Moves_this_column_to_another_position_0d4b = Déplace cette colonne vers une autre position
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = Mon deck
|
||||
# reaction from user to a note you were tagged in
|
||||
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } a réagi à une note dans laquelle vous avez été tagué
|
||||
# reaction from user to your note
|
||||
name__reacted_to_your_note_ead9 = { $name } a réagi à votre note
|
||||
# repost from user
|
||||
name__reposted_a_note_you_were_tagged_in_1379 = { $name } a reposté une note dans laquelle vous avez été tagué
|
||||
# repost from user
|
||||
name__reposted_your_note_1379 = { $name } a reposté votre note
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = Nouveau sur Nostr ?
|
||||
# NIP-05 identity field label
|
||||
@@ -259,6 +275,8 @@ Post_now_8a49 = Publier maintenant
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Cliquez sur le bouton ci-dessous pour copier vos données les plus récentes dans le presse-papiers de votre système. Collez-les ensuite dans votre courrier électronique.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Photo de profil
|
||||
# label describing public key
|
||||
PUBLIC_ACCOUNT_ID_4394 = IDENTITE PUBLIQUE DU COMPTE
|
||||
# Column title for quote composition
|
||||
Quote_475c = Citation
|
||||
# Error message when quote note cannot be found
|
||||
@@ -311,6 +329,8 @@ Search_c573 = Rechercher
|
||||
Search_notes_42a6 = Rechercher des notes...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Recherche par '{ $query }'
|
||||
# label describing secret key
|
||||
SECRET_ACCOUNT_LOGIN_KEY_8440 = CLÉ SECRETE DE CONNEXION DU COMPTE
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
|
||||
# Description for universe column
|
||||
@@ -412,3 +432,27 @@ Got__count__results_for___query_85fb =
|
||||
[one] A obtenu { $count } pour '{ $query }'
|
||||
*[other] A obtenu { $count } pour '{ $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 ->
|
||||
[un] { $name } et { $count } a réagi à une note où vous êtes tagué
|
||||
*[autre] { $name } et { $count } ont réagi à une note où vous êtes tagué
|
||||
}
|
||||
# describing the amount of reactions your note received
|
||||
name__and__count__others_reacted_to_your_note_0f6a =
|
||||
{ $count ->
|
||||
[un] { $name } et { $count } autres ont réagi à votre note
|
||||
*[autre] { $name } et { $count } autres ont réagi à votre 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 ->
|
||||
[un] { $name } et { $count } a reposté une note où vous êtes tagué
|
||||
*[autre] { $name } et { $count } ont reposté une note où vous êtes tagué
|
||||
}
|
||||
# describing the amount of reposts your note received
|
||||
name__and__count__others_reposted_your_note_70a0 =
|
||||
{ $count ->
|
||||
[un] { $name } et { $count } a reposté votre note
|
||||
*[autre] { $name } et { $count } ont reposté votre note
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Adicionar coluna de notificações exte
|
||||
Add_Hashtag_Column_ebf4 = Adicionar coluna de marcadores
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = Adicionar coluna de últimas notas
|
||||
# Tooltip text for adding a new deck button
|
||||
Add_new_deck_f2fc = Adicionar nova aba
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
|
||||
# Button label to add a relay
|
||||
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar link
|
||||
Copy_Note_ID_6b45 = Copiar ID da nota
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = Copiar JSON da nota
|
||||
# Tooltip text for copying npub to clipboard
|
||||
Copy_npub_to_clipboard_c105 = Copiar npub para área de transferência
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = Copiar chave pública
|
||||
# Copy the text content of the note to clipboard
|
||||
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar carteira
|
||||
Display_name_f9d9 = Nome a mostrar
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = "{ $domain }" será usado para identificação
|
||||
# Button to indicate that the user is done going through the onboarding process.
|
||||
Done_50dd = Concluído
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = Editar aba
|
||||
# Button label to edit a deck
|
||||
@@ -191,6 +197,8 @@ k_50K_c2dc = 50K
|
||||
k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = Acompanha as tuas notas e respostas
|
||||
# label for keys setting section
|
||||
Keys_435f = Chaves
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = Idioma:
|
||||
# Title for last note per user column
|
||||
@@ -209,6 +217,14 @@ Media_from_someone_you_don_t_follow_5611 = Conteúdo de alguém que não segues
|
||||
Moves_this_column_to_another_position_0d4b = Mover esta coluna para outra posição
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = Minha aba
|
||||
# reaction from user to a note you were tagged in
|
||||
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } reagiu a uma nota em que te marcaram
|
||||
# reaction from user to your note
|
||||
name__reacted_to_your_note_ead9 = { $name } reagiu à tua nota
|
||||
# repost from user
|
||||
name__reposted_a_note_you_were_tagged_in_1379 = { $name } republicou uma nota em que te marcaram
|
||||
# repost from user
|
||||
name__reposted_your_note_1379 = { $name } republicou a tua nota
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = Nov@ no Nostr?
|
||||
# NIP-05 identity field label
|
||||
@@ -259,6 +275,8 @@ Post_now_8a49 = Publicar agora
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Prime o botão abaixo para copiar os teus registos mais recentes para a área de transferência do teu sistema. Depois cola-os no teu e-mail.
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = Foto de perfil
|
||||
# label describing public key
|
||||
PUBLIC_ACCOUNT_ID_4394 = ID da CONTA PÚBLICA
|
||||
# Column title for quote composition
|
||||
Quote_475c = Citação
|
||||
# Error message when quote note cannot be found
|
||||
@@ -311,6 +329,8 @@ Search_c573 = Procurar
|
||||
Search_notes_42a6 = Procurar notas...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = Procurando por '{ $query }'
|
||||
# label describing secret key
|
||||
SECRET_ACCOUNT_LOGIN_KEY_8440 = CHAVE SECRETA DE LOGIN DA CONTA
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos
|
||||
# Description for universe column
|
||||
@@ -412,3 +432,27 @@ Got__count__results_for___query_85fb =
|
||||
[one] { $count } resultado obtido para '{ $query }'
|
||||
*[other] { $count } resultados obtidos para '{ $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 } e { $count } outro reagiram a uma nota em que te marcaram
|
||||
*[other] { $name } e { $count } outros reagiram a uma nota que te marcaram
|
||||
}
|
||||
# describing the amount of reactions your note received
|
||||
name__and__count__others_reacted_to_your_note_0f6a =
|
||||
{ $count ->
|
||||
[one] { $name } e { $count } outro reagiram à tua nota
|
||||
*[other] { $name } e { $count } outros reagiram à tua nota
|
||||
}
|
||||
# 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 } e { $count } outro republicaram uma nota em que te marcaram
|
||||
*[other] { $name } e { $count } outros republicaram uma nota que te marcaram
|
||||
}
|
||||
# describing the amount of reposts your note received
|
||||
name__and__count__others_reposted_your_note_70a0 =
|
||||
{ $count ->
|
||||
[one] { $name } e { $count } outro republicaram a tua nota
|
||||
*[other] { $name } e { $count } outros republicaram a tua nota
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = เพิ่มคอลัมน์ก
|
||||
Add_Hashtag_Column_ebf4 = เพิ่มคอลัมน์แฮชแท็ก
|
||||
# Column title for adding last notes column
|
||||
Add_Last_Notes_Column_bbad = เพิ่มคอลัมน์โน้ตล่าสุด
|
||||
# Tooltip text for adding a new deck button
|
||||
Add_new_deck_f2fc = เพิ่ม deck ใหม่
|
||||
# Column title for adding notifications column
|
||||
Add_Notifications_Column_79f8 = เพิ่มคอลัมน์การแจ้งเตือน
|
||||
# Button label to add a relay
|
||||
@@ -93,6 +95,8 @@ Copy_Link_dc7c = คัดลอกลิงก์
|
||||
Copy_Note_ID_6b45 = คัดลอก โน้ต ID
|
||||
# Copy the raw note data in JSON format to clipboard
|
||||
Copy_Note_JSON_9e4e = คัดลอก JSON ของโน้ต
|
||||
# Tooltip text for copying npub to clipboard
|
||||
Copy_npub_to_clipboard_c105 = คัดลอก npub ไปยังคลิปบอร์ด
|
||||
# Copy the author's public key to clipboard
|
||||
Copy_Pubkey_9cc4 = คัดลอก npub
|
||||
# Copy the text content of the note to clipboard
|
||||
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = ลบวอลเล็ต
|
||||
Display_name_f9d9 = ชื่อที่แสดง
|
||||
# Domain identification message
|
||||
domain___will_be_used_for_identification_b67e = { $domain } จะใช้สำหรับการระบุตัวตน
|
||||
# Button to indicate that the user is done going through the onboarding process.
|
||||
Done_50dd = เสร็จ
|
||||
# Column title for editing deck
|
||||
Edit_Deck_4018 = แก้ไข Deck
|
||||
# Button label to edit a deck
|
||||
@@ -193,6 +199,8 @@ k_50K_c2dc = 50K
|
||||
k_5K_f7e6 = 5K
|
||||
# Description for your notes column
|
||||
Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ
|
||||
# label for keys setting section
|
||||
Keys_435f = คีย์
|
||||
# Label for language, Appearance settings section
|
||||
Language_e264 = ภาษา:
|
||||
# Title for last note per user column
|
||||
@@ -211,6 +219,14 @@ Media_from_someone_you_don_t_follow_5611 = สื่อจากคนที่
|
||||
Moves_this_column_to_another_position_0d4b = ย้ายคอลัมน์นี้ไปยังตำแหน่งอื่น
|
||||
# Title for the user's deck
|
||||
My_Deck_4ac5 = Deck ของฉัน
|
||||
# reaction from user to a note you were tagged in
|
||||
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } react ต่อโน้ตที่คุณถูกแท็ก
|
||||
# reaction from user to your note
|
||||
name__reacted_to_your_note_ead9 = { $name } react ต่อโน้ตของคุณ
|
||||
# repost from user
|
||||
name__reposted_a_note_you_were_tagged_in_1379 = { $name } รีโพสต์โน้ตที่คุณถูกแท็ก
|
||||
# repost from user
|
||||
name__reposted_your_note_1379 = { $name } รีโพสต์โน้ตของคุณ
|
||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||
New_to_Nostr_a2fd = มือใหม่สำหรับ Nostr?
|
||||
# NIP-05 identity field label
|
||||
@@ -261,6 +277,8 @@ Post_now_8a49 = โพสต์
|
||||
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = กดปุ่มด้านล่างเพื่อคัดลอกข้อมูลบันทึกล่าสุด ไปยังคลิปบอร์ด จากนั้นนำไปวางในอีเมลของคุณ
|
||||
# Profile picture URL field label
|
||||
Profile_picture_81ff = รูปโปรไฟล์
|
||||
# label describing public key
|
||||
PUBLIC_ACCOUNT_ID_4394 = ไอดีบัญชีสาธารณะ
|
||||
# Column title for quote composition
|
||||
Quote_475c = อ้างอิง
|
||||
# Error message when quote note cannot be found
|
||||
@@ -313,6 +331,8 @@ Search_c573 = ค้นหา
|
||||
Search_notes_42a6 = ค้นหาโน้ต...
|
||||
# Search in progress message
|
||||
Searching_for___query_5d18 = กำลังค้นหา '{ $query }'
|
||||
# label describing secret key
|
||||
SECRET_ACCOUNT_LOGIN_KEY_8440 = คีย์ลับสำหรับล็อกอินบัญชี
|
||||
# Description for Home column
|
||||
See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ
|
||||
# Description for universe column
|
||||
@@ -414,3 +434,27 @@ Got__count__results_for___query_85fb =
|
||||
[one] ผลการค้นหา '{ $query }': พบ { $count } รายการ
|
||||
*[other] ผลการค้นหา '{ $query }': พบ { $count } รายการ
|
||||
}
|
||||
# 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 } และอีก { $count } คน reacted ต่อโน้ตที่คุณถูกแท็ก
|
||||
*[other] { $name } และอีก { $count } คน reacted ต่อโน้ตที่คุณถูกแท็ก
|
||||
}
|
||||
# describing the amount of reactions your note received
|
||||
name__and__count__others_reacted_to_your_note_0f6a =
|
||||
{ $count ->
|
||||
[one] { $name } และอีก { $count } คน reacted ต่อโน้ตที่ของคุณ
|
||||
*[other] { $name } และอีก { $count } คน reacted ต่อโน้ตของคุณ
|
||||
}
|
||||
# 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 } และอีก { $count } คน รีโพสต์โน้ตที่คุณถูกแท็ก
|
||||
*[other] { $name } และอีก { $count } คน รีโพสต์โน้ตที่คุณถูกแท็ก
|
||||
}
|
||||
# describing the amount of reposts your note received
|
||||
name__and__count__others_reposted_your_note_70a0 =
|
||||
{ $count ->
|
||||
[one] { $name } และอีก { $count } คน รีโพสต์โน้ตของคุณ
|
||||
*[other] { $name } และอีก { $count } คน รีโพสต์โน้ตของคุณ
|
||||
}
|
||||
|
||||
@@ -152,7 +152,9 @@ impl<'a> RelayMessage<'a> {
|
||||
return Ok(Self::ok(event_id, status, message));
|
||||
}
|
||||
|
||||
Err(Error::DecodeFailed("unrecognized message type".into()))
|
||||
Err(Error::DecodeFailed(format!(
|
||||
"unrecognized message type: '{msg}'"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,15 +222,15 @@ mod tests {
|
||||
),
|
||||
(
|
||||
r#"["NOTICE": 404]"#,
|
||||
Err(Error::DecodeFailed("unrecognized message type".into())),
|
||||
Err(Error::DecodeFailed("unrecognized message type: '[\"NOTICE\": 404]'".into())),
|
||||
),
|
||||
(
|
||||
r#"["OK","event_id"]"#,
|
||||
Err(Error::DecodeFailed("unrecognized message type".into())),
|
||||
Err(Error::DecodeFailed("unrecognized message type: '[\"OK\",\"event_id\"]'".into())),
|
||||
),
|
||||
(
|
||||
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"]"#,
|
||||
Err(Error::DecodeFailed("unrecognized message type".into())),
|
||||
Err(Error::DecodeFailed("unrecognized message type: '[\"OK\",\"b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30\"]'".into())),
|
||||
),
|
||||
(
|
||||
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,""]"#,
|
||||
|
||||
@@ -51,6 +51,7 @@ bitflags = { workspace = true }
|
||||
regex = "1"
|
||||
chrono = { workspace = true }
|
||||
indexmap = {workspace = true}
|
||||
rand = {workspace = true}
|
||||
crossbeam-channel = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
filter::{self, HybridFilter},
|
||||
filter::{self, HybridFilter, ValidKind},
|
||||
Error,
|
||||
};
|
||||
use nostrdb::{Filter, Note};
|
||||
@@ -15,10 +15,16 @@ pub fn hybrid_contacts_filter(
|
||||
add_pk: Option<&[u8; 32]>,
|
||||
with_hashtags: bool,
|
||||
) -> Result<HybridFilter, Error> {
|
||||
let local = filter::filter_from_tags(note, add_pk, with_hashtags)?
|
||||
.into_filter([1], filter::default_limit());
|
||||
let local = vec![
|
||||
filter::filter_from_tags(note, add_pk, with_hashtags)?
|
||||
.into_query_package(ValidKind::One, filter::default_limit()),
|
||||
filter::filter_from_tags(note, add_pk, with_hashtags)?
|
||||
.into_query_package(ValidKind::Six, filter::default_limit()),
|
||||
filter::filter_from_tags(note, add_pk, with_hashtags)?
|
||||
.into_query_package(ValidKind::Zero, filter::default_limit()),
|
||||
];
|
||||
let remote = filter::filter_from_tags(note, add_pk, with_hashtags)?
|
||||
.into_filter([1, 0], filter::default_remote_limit());
|
||||
.into_filter(vec![1, 0], filter::default_remote_limit());
|
||||
|
||||
Ok(HybridFilter::split(local, remote))
|
||||
}
|
||||
|
||||
@@ -142,12 +142,6 @@ impl FilterState {
|
||||
Self::Ready(HybridFilter::unsplit(filter))
|
||||
}
|
||||
|
||||
/// The filter is ready, but we have a different local filter from
|
||||
/// our remote one
|
||||
pub fn ready_split(local: Vec<Filter>, remote: Vec<Filter>) -> Self {
|
||||
Self::Ready(HybridFilter::split(local, remote))
|
||||
}
|
||||
|
||||
/// Our hybrid filter is ready (either split or unsplit)
|
||||
pub fn ready_hybrid(filter: HybridFilter) -> Self {
|
||||
Self::Ready(filter)
|
||||
@@ -219,7 +213,7 @@ pub struct FilteredTags {
|
||||
/// The local and remote filter are related but slightly different
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SplitFilter {
|
||||
pub local: Vec<Filter>,
|
||||
pub local: Vec<NdbQueryPackage>,
|
||||
pub remote: Vec<Filter>,
|
||||
}
|
||||
|
||||
@@ -236,16 +230,23 @@ impl HybridFilter {
|
||||
HybridFilter::Unsplit(filter)
|
||||
}
|
||||
|
||||
pub fn split(local: Vec<Filter>, remote: Vec<Filter>) -> Self {
|
||||
pub fn split(local: Vec<NdbQueryPackage>, remote: Vec<Filter>) -> Self {
|
||||
HybridFilter::Split(SplitFilter { local, remote })
|
||||
}
|
||||
|
||||
pub fn local(&self) -> &[Filter] {
|
||||
pub fn local(&self) -> NdbQueryPackages<'_> {
|
||||
match self {
|
||||
Self::Split(split) => &split.local,
|
||||
Self::Split(split) => NdbQueryPackages {
|
||||
packages: split.local.iter().map(NdbQueryPackage::borrow).collect(),
|
||||
},
|
||||
|
||||
// local as the same as remote in unsplit
|
||||
Self::Unsplit(local) => local,
|
||||
Self::Unsplit(local) => NdbQueryPackages {
|
||||
packages: vec![NdbQueryPackageUnowned {
|
||||
filters: local,
|
||||
kind: None,
|
||||
}],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,77 +261,139 @@ impl HybridFilter {
|
||||
}
|
||||
|
||||
impl FilteredTags {
|
||||
pub fn into_follow_filter(self) -> Vec<Filter> {
|
||||
self.into_filter([1], default_limit())
|
||||
}
|
||||
|
||||
// TODO: make this more general
|
||||
pub fn into_filter<I>(self, kinds: I, limit: u64) -> Vec<Filter>
|
||||
where
|
||||
I: IntoIterator<Item = u64> + Copy,
|
||||
{
|
||||
pub fn into_query_package(self, kind: ValidKind, limit: u64) -> NdbQueryPackage {
|
||||
let mut filters: Vec<Filter> = Vec::with_capacity(2);
|
||||
|
||||
if let Some(authors) = self.authors {
|
||||
filters.push(authors.kinds(kinds).limit(limit).build())
|
||||
filters.push(authors.kinds(vec![kind.kind()]).limit(limit).build())
|
||||
}
|
||||
|
||||
if let Some(hashtags) = self.hashtags {
|
||||
filters.push(hashtags.kinds(kinds).limit(limit).build())
|
||||
if matches!(&kind, ValidKind::One | ValidKind::Zero) {
|
||||
filters.push(hashtags.kinds(vec![kind.kind()]).limit(limit).build())
|
||||
}
|
||||
}
|
||||
|
||||
NdbQueryPackage { filters, kind }
|
||||
}
|
||||
|
||||
// TODO: make this more general
|
||||
pub fn into_filter(self, shared_kinds: Vec<u64>, limit: u64) -> Vec<Filter> {
|
||||
let mut filters: Vec<Filter> = Vec::with_capacity(2);
|
||||
|
||||
if let Some(authors) = self.authors {
|
||||
let mut author_kinds = shared_kinds.clone();
|
||||
author_kinds.insert(0, 6);
|
||||
|
||||
filters.push(authors.kinds(author_kinds).limit(limit).build())
|
||||
}
|
||||
|
||||
if let Some(hashtags) = self.hashtags {
|
||||
filters.push(hashtags.kinds(shared_kinds).limit(limit).build())
|
||||
}
|
||||
|
||||
filters
|
||||
}
|
||||
}
|
||||
|
||||
/// `Ndb::query` retrieves the most recent notes of one kind until it can't find anymore THEN proceeds to the next kind.
|
||||
/// This is not optimal for many scenarios, so this data structure represents data that is packaged optimally for one `Ndb::query`,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NdbQueryPackage {
|
||||
pub kind: ValidKind,
|
||||
pub filters: Vec<Filter>,
|
||||
}
|
||||
|
||||
impl NdbQueryPackage {
|
||||
pub fn borrow(&self) -> NdbQueryPackageUnowned<'_> {
|
||||
NdbQueryPackageUnowned {
|
||||
filters: &self.filters,
|
||||
kind: Some(self.kind.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NdbQueryPackageUnowned<'a> {
|
||||
pub kind: Option<ValidKind>,
|
||||
pub filters: &'a Vec<Filter>,
|
||||
}
|
||||
|
||||
pub struct NdbQueryPackages<'a> {
|
||||
pub packages: Vec<NdbQueryPackageUnowned<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> NdbQueryPackages<'a> {
|
||||
pub fn combined(&self) -> Vec<Filter> {
|
||||
let mut combined = Vec::new();
|
||||
for package in &self.packages {
|
||||
combined.extend_from_slice(package.filters);
|
||||
}
|
||||
|
||||
combined
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ValidKind {
|
||||
Zero,
|
||||
One,
|
||||
Six,
|
||||
}
|
||||
|
||||
impl ValidKind {
|
||||
fn kind(&self) -> u64 {
|
||||
match self {
|
||||
ValidKind::Zero => 0,
|
||||
ValidKind::One => 1,
|
||||
ValidKind::Six => 6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a "last N notes per pubkey" query.
|
||||
pub fn last_n_per_pubkey_from_tags(
|
||||
note: &Note,
|
||||
kind: u64,
|
||||
notes_per_pubkey: u64,
|
||||
) -> Result<Vec<Filter>, Error> {
|
||||
use rand::Rng;
|
||||
|
||||
let mut filters: Vec<Filter> = vec![];
|
||||
let mut rng = rand::rng();
|
||||
|
||||
for tag in note.tags() {
|
||||
// TODO: fix arbitrary MAX_FILTER limit in nostrdb
|
||||
if filters.len() == 15 {
|
||||
break;
|
||||
}
|
||||
const LIMIT: usize = 15;
|
||||
|
||||
for (i, tag) in note.tags().iter().enumerate() {
|
||||
if tag.count() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let t = if let Some(t) = tag.get_unchecked(0).variant().str() {
|
||||
t
|
||||
} else {
|
||||
let Some("p") = tag.get_str(0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if t == "p" {
|
||||
let author = if let Some(author) = tag.get_unchecked(1).variant().id() {
|
||||
author
|
||||
} else {
|
||||
let Some(author) = tag.get_id(1) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mk_filter = || {
|
||||
let mut filter = Filter::new();
|
||||
filter.start_authors_field()?;
|
||||
filter.add_id_element(author)?;
|
||||
let _ = filter.start_authors_field();
|
||||
let _ = filter.add_id_element(author);
|
||||
filter.end_field();
|
||||
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build());
|
||||
} else if t == "t" {
|
||||
let hashtag = if let Some(hashtag) = tag.get_unchecked(1).variant().str() {
|
||||
hashtag
|
||||
} else {
|
||||
continue;
|
||||
filter.kinds([kind]).limit(notes_per_pubkey).build()
|
||||
};
|
||||
|
||||
let mut filter = Filter::new();
|
||||
filter.start_tags_field('t')?;
|
||||
filter.add_str_element(hashtag)?;
|
||||
filter.end_field();
|
||||
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build());
|
||||
// since we're limited due to a nostrdb bug, we reservoir sample to keep things interesting
|
||||
if filters.len() < LIMIT {
|
||||
filters.push(mk_filter());
|
||||
} else {
|
||||
let j = rng.random_range(0..=i);
|
||||
if j < LIMIT {
|
||||
filters[j] = mk_filter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ impl TexturesCache {
|
||||
|
||||
entry.replace_entry_with(|_, v| {
|
||||
let TextureStateInternal::Loading(textured) = v else {
|
||||
return None;
|
||||
return Some(v);
|
||||
};
|
||||
|
||||
Some(TextureStateInternal::Loaded(textured))
|
||||
|
||||
@@ -305,7 +305,7 @@ fn generate_gif(
|
||||
);
|
||||
|
||||
if tex_input.send(texture_frame).is_err() {
|
||||
tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
|
||||
//tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,73 +10,75 @@ const ONE_WEEK_IN_SECONDS: u64 = 604_800;
|
||||
const ONE_MONTH_IN_SECONDS: u64 = 2_592_000; // 30 days
|
||||
const ONE_YEAR_IN_SECONDS: u64 = 31_536_000; // 365 days
|
||||
|
||||
// Range boundary constants for match patterns
|
||||
const MAX_SECONDS: u64 = ONE_MINUTE_IN_SECONDS - 1;
|
||||
const MAX_SECONDS_FOR_MINUTES: u64 = ONE_HOUR_IN_SECONDS - 1;
|
||||
const MAX_SECONDS_FOR_HOURS: u64 = ONE_DAY_IN_SECONDS - 1;
|
||||
const MAX_SECONDS_FOR_DAYS: u64 = ONE_WEEK_IN_SECONDS - 1;
|
||||
const MAX_SECONDS_FOR_WEEKS: u64 = ONE_MONTH_IN_SECONDS - 1;
|
||||
const MAX_SECONDS_FOR_MONTHS: u64 = ONE_YEAR_IN_SECONDS - 1;
|
||||
|
||||
/// Calculate relative time between two timestamps
|
||||
/// Calculate relative time between two timestamps, with two units only
|
||||
/// when the scale is large enough (e.g., "1y 6m", "5d 4h"),
|
||||
/// but not for hours/minutes/seconds.
|
||||
fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String {
|
||||
// Determine if the timestamp is in the future or the past
|
||||
let duration = if now >= timestamp {
|
||||
now.saturating_sub(timestamp)
|
||||
} else {
|
||||
timestamp.saturating_sub(now)
|
||||
};
|
||||
|
||||
let time_str = match duration {
|
||||
0..=2 => tr!(
|
||||
// Special-case: "now" for < 3 seconds
|
||||
if duration <= 2 {
|
||||
let s = tr!(
|
||||
i18n,
|
||||
"now",
|
||||
"Relative time for very recent events (less than 3 seconds)"
|
||||
),
|
||||
3..=MAX_SECONDS => tr!(
|
||||
i18n,
|
||||
"{count}s",
|
||||
"Relative time in seconds",
|
||||
count = duration
|
||||
),
|
||||
ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!(
|
||||
i18n,
|
||||
"{count}m",
|
||||
"Relative time in minutes",
|
||||
count = duration / ONE_MINUTE_IN_SECONDS
|
||||
),
|
||||
ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!(
|
||||
i18n,
|
||||
"{count}h",
|
||||
"Relative time in hours",
|
||||
count = duration / ONE_HOUR_IN_SECONDS
|
||||
),
|
||||
ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!(
|
||||
i18n,
|
||||
"{count}d",
|
||||
"Relative time in days",
|
||||
count = duration / ONE_DAY_IN_SECONDS
|
||||
),
|
||||
ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!(
|
||||
i18n,
|
||||
"{count}w",
|
||||
"Relative time in weeks",
|
||||
count = duration / ONE_WEEK_IN_SECONDS
|
||||
),
|
||||
ONE_MONTH_IN_SECONDS..=MAX_SECONDS_FOR_MONTHS => tr!(
|
||||
i18n,
|
||||
"{count}mo",
|
||||
"Relative time in months",
|
||||
count = duration / ONE_MONTH_IN_SECONDS
|
||||
),
|
||||
_ => tr!(
|
||||
i18n,
|
||||
"{count}y",
|
||||
"Relative time in years",
|
||||
count = duration / ONE_YEAR_IN_SECONDS
|
||||
),
|
||||
);
|
||||
return if timestamp > now { format!("+{s}") } else { s };
|
||||
}
|
||||
|
||||
// Break into buckets
|
||||
let years = duration / ONE_YEAR_IN_SECONDS;
|
||||
let rem_y = duration % ONE_YEAR_IN_SECONDS;
|
||||
|
||||
let months = rem_y / ONE_MONTH_IN_SECONDS;
|
||||
let rem_m = rem_y % ONE_MONTH_IN_SECONDS;
|
||||
|
||||
let weeks = rem_m / ONE_WEEK_IN_SECONDS;
|
||||
let rem_w = rem_m % ONE_WEEK_IN_SECONDS;
|
||||
|
||||
let days = rem_w / ONE_DAY_IN_SECONDS;
|
||||
let rem_d = rem_w % ONE_DAY_IN_SECONDS;
|
||||
|
||||
let hours = rem_d / ONE_HOUR_IN_SECONDS;
|
||||
let rem_h = rem_d % ONE_HOUR_IN_SECONDS;
|
||||
|
||||
let mins = rem_h / ONE_MINUTE_IN_SECONDS;
|
||||
let secs = rem_h % ONE_MINUTE_IN_SECONDS;
|
||||
|
||||
let mut parts: Vec<String> = Vec::with_capacity(2);
|
||||
|
||||
let mut push_part = |count: u64, key: &str, desc: &str| {
|
||||
if count > 0 && parts.len() < 2 {
|
||||
parts.push(tr!(i18n, key, desc, count = count));
|
||||
}
|
||||
};
|
||||
|
||||
if years > 0 {
|
||||
push_part(years, "{count}y", "Relative time in years");
|
||||
push_part(months, "{count}mo", "Relative time in months");
|
||||
} else if months > 0 {
|
||||
push_part(months, "{count}mo", "Relative time in months");
|
||||
push_part(weeks, "{count}w", "Relative time in weeks");
|
||||
} else if weeks > 0 {
|
||||
push_part(weeks, "{count}w", "Relative time in weeks");
|
||||
push_part(days, "{count}d", "Relative time in days");
|
||||
} else if days > 0 {
|
||||
push_part(days, "{count}d", "Relative time in days");
|
||||
push_part(hours, "{count}h", "Relative time in hours");
|
||||
} else if hours > 0 {
|
||||
push_part(hours, "{count}h", "Relative time in hours");
|
||||
} else if mins > 0 {
|
||||
push_part(mins, "{count}m", "Relative time in minutes");
|
||||
} else {
|
||||
push_part(secs.max(1), "{count}s", "Relative time in seconds");
|
||||
}
|
||||
|
||||
let time_str = parts.join(" ");
|
||||
|
||||
if timestamp > now {
|
||||
format!("+{time_str}")
|
||||
} else {
|
||||
|
||||
@@ -311,7 +311,7 @@ impl TimelineSub {
|
||||
let before = self.state.clone();
|
||||
match &mut self.state {
|
||||
SubState::NoSub { dependers } => {
|
||||
let Some(sub) = ndb_sub(ndb, filter.local(), "") else {
|
||||
let Some(sub) = ndb_sub(ndb, &filter.local().combined(), "") else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -326,7 +326,7 @@ impl TimelineSub {
|
||||
dependers: _,
|
||||
} => {}
|
||||
SubState::RemoteOnly { remote, dependers } => {
|
||||
let Some(local) = ndb_sub(ndb, filter.local(), "") else {
|
||||
let Some(local) = ndb_sub(ndb, &filter.local().combined(), "") else {
|
||||
return;
|
||||
};
|
||||
self.state = SubState::Unified {
|
||||
|
||||
@@ -56,11 +56,12 @@ impl NewPost {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_note(&self, seckey: &[u8; 32]) -> Note<'_> {
|
||||
let mut content = self.content.clone();
|
||||
/// creates a NoteBuilder with all the shared data between note, reply & quote reply
|
||||
fn builder_with_shared_tags<'a>(&self, mut content: String) -> NoteBuilder<'a> {
|
||||
append_urls(&mut content, &self.media);
|
||||
|
||||
let mut builder = add_client_tag(NoteBuilder::new()).kind(1).content(&content);
|
||||
let mut builder = NoteBuilder::new().kind(1).content(&content);
|
||||
builder = add_client_tag(builder);
|
||||
|
||||
for hashtag in Self::extract_hashtags(&self.content) {
|
||||
builder = builder.start_tag().tag_str("t").tag_str(&hashtag);
|
||||
@@ -74,18 +75,21 @@ impl NewPost {
|
||||
builder = add_mention_tags(builder, &self.mentions);
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
pub fn to_note(&self, seckey: &[u8; 32]) -> Note<'_> {
|
||||
let builder = self.builder_with_shared_tags(self.content.clone());
|
||||
|
||||
builder.sign(seckey).build().expect("note should be ok")
|
||||
}
|
||||
|
||||
pub fn to_reply(&self, seckey: &[u8; 32], replying_to: &Note) -> Note<'_> {
|
||||
let mut content = self.content.clone();
|
||||
append_urls(&mut content, &self.media);
|
||||
|
||||
let builder = add_client_tag(NoteBuilder::new()).kind(1).content(&content);
|
||||
let mut builder = self.builder_with_shared_tags(self.content.clone());
|
||||
|
||||
let nip10 = NoteReply::new(replying_to.tags());
|
||||
|
||||
let mut builder = if let Some(root) = nip10.root() {
|
||||
builder = if let Some(root) = nip10.root() {
|
||||
builder
|
||||
.start_tag()
|
||||
.tag_str("e")
|
||||
@@ -143,14 +147,6 @@ impl NewPost {
|
||||
builder = builder.start_tag().tag_str("p").tag_str(&hex::encode(id));
|
||||
}
|
||||
|
||||
if !self.media.is_empty() {
|
||||
builder = add_imeta_tags(builder, &self.media);
|
||||
}
|
||||
|
||||
if !self.mentions.is_empty() {
|
||||
builder = add_mention_tags(builder, &self.mentions);
|
||||
}
|
||||
|
||||
builder
|
||||
.sign(seckey)
|
||||
.build()
|
||||
@@ -158,27 +154,13 @@ impl NewPost {
|
||||
}
|
||||
|
||||
pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note<'_> {
|
||||
let mut new_content = format!(
|
||||
let new_content = format!(
|
||||
"{}\nnostr:{}",
|
||||
self.content,
|
||||
enostr::NoteId::new(*quoting.id()).to_bech().unwrap()
|
||||
);
|
||||
|
||||
append_urls(&mut new_content, &self.media);
|
||||
|
||||
let mut builder = NoteBuilder::new().kind(1).content(&new_content);
|
||||
|
||||
for hashtag in Self::extract_hashtags(&self.content) {
|
||||
builder = builder.start_tag().tag_str("t").tag_str(&hashtag);
|
||||
}
|
||||
|
||||
if !self.media.is_empty() {
|
||||
builder = add_imeta_tags(builder, &self.media);
|
||||
}
|
||||
|
||||
if !self.mentions.is_empty() {
|
||||
builder = add_mention_tags(builder, &self.mentions);
|
||||
}
|
||||
let builder = self.builder_with_shared_tags(new_content);
|
||||
|
||||
builder
|
||||
.start_tag()
|
||||
|
||||
@@ -134,15 +134,22 @@ impl TimelineCache {
|
||||
}
|
||||
|
||||
let notes = if let FilterState::Ready(filters) = id.filters(txn, ndb) {
|
||||
if let Ok(results) = ndb.query(txn, filters.local(), 1000) {
|
||||
results
|
||||
let mut notes = Vec::new();
|
||||
|
||||
for package in filters.local().packages {
|
||||
if let Ok(results) = ndb.query(txn, package.filters, 1000) {
|
||||
let cur_notes: Vec<NoteRef> = results
|
||||
.into_iter()
|
||||
.map(NoteRef::from_query_result)
|
||||
.collect()
|
||||
.collect();
|
||||
|
||||
notes.extend(cur_notes);
|
||||
} else {
|
||||
debug!("got no results from TimelineCache lookup for {:?}", id);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
notes
|
||||
} else {
|
||||
// filter is not ready yet
|
||||
vec![]
|
||||
@@ -178,12 +185,20 @@ impl TimelineCache {
|
||||
let (mut open_result, timeline) = match notes_resp.vitality {
|
||||
Vitality::Stale(timeline) => {
|
||||
// The timeline cache is stale, let's update it
|
||||
let notes = find_new_notes(
|
||||
let notes = {
|
||||
let mut notes = Vec::new();
|
||||
for package in timeline.subscription.get_filter()?.local().packages {
|
||||
let cur_notes = find_new_notes(
|
||||
timeline.all_or_any_entries().latest(),
|
||||
timeline.subscription.get_filter()?.local(),
|
||||
package.filters,
|
||||
txn,
|
||||
ndb,
|
||||
);
|
||||
notes.extend(cur_notes);
|
||||
}
|
||||
notes
|
||||
};
|
||||
|
||||
let open_result = if notes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::search::SearchQuery;
|
||||
use crate::timeline::{Timeline, TimelineTab};
|
||||
use enostr::{Filter, NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::filter::{NdbQueryPackage, ValidKind};
|
||||
use notedeck::{
|
||||
contacts::{contacts_filter, hybrid_contacts_filter},
|
||||
filter::{self, default_limit, default_remote_limit, HybridFilter},
|
||||
@@ -728,15 +729,29 @@ fn last_per_pubkey_filter_state(ndb: &Ndb, pk: &Pubkey) -> FilterState {
|
||||
}
|
||||
|
||||
fn profile_filter(pk: &[u8; 32]) -> HybridFilter {
|
||||
HybridFilter::split(
|
||||
vec![Filter::new()
|
||||
let local = vec![
|
||||
NdbQueryPackage {
|
||||
filters: vec![Filter::new()
|
||||
.authors([pk])
|
||||
.kinds([1])
|
||||
.limit(default_limit())
|
||||
.build()],
|
||||
kind: ValidKind::One,
|
||||
},
|
||||
NdbQueryPackage {
|
||||
filters: vec![Filter::new()
|
||||
.authors([pk])
|
||||
.kinds([6])
|
||||
.limit(default_limit())
|
||||
.build()],
|
||||
kind: ValidKind::Six,
|
||||
},
|
||||
];
|
||||
HybridFilter::split(
|
||||
local,
|
||||
vec![Filter::new()
|
||||
.authors([pk])
|
||||
.kinds([1, 0])
|
||||
.kinds([1, 6, 0])
|
||||
.limit(default_remote_limit())
|
||||
.build()],
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ impl ViewFilter {
|
||||
}
|
||||
|
||||
pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool {
|
||||
!cache.reply.borrow(note.tags()).is_reply()
|
||||
note.kind() == 6 || !cache.reply.borrow(note.tags()).is_reply()
|
||||
}
|
||||
|
||||
fn identity(_cache: &CachedNote, _note: &Note) -> bool {
|
||||
@@ -133,6 +133,7 @@ impl TimelineTab {
|
||||
ndb: &Ndb,
|
||||
txn: &Transaction,
|
||||
reversed: bool,
|
||||
use_front_insert: bool,
|
||||
) -> Option<UnknownPks<'a>> {
|
||||
if payloads.is_empty() {
|
||||
return None;
|
||||
@@ -158,7 +159,11 @@ impl TimelineTab {
|
||||
debug!("spliced when inserting {num_refs} new notes, resetting virtual list",);
|
||||
list.reset();
|
||||
}
|
||||
MergeKind::FrontInsert => {
|
||||
MergeKind::FrontInsert => 's: {
|
||||
if !use_front_insert {
|
||||
break 's;
|
||||
}
|
||||
|
||||
// only run this logic if we're reverse-chronological
|
||||
// reversed in this case means chronological, since the
|
||||
// default is reverse-chronological. yeah it's confusing.
|
||||
@@ -210,6 +215,7 @@ pub struct Timeline {
|
||||
pub selected_view: usize,
|
||||
|
||||
pub subscription: TimelineSub,
|
||||
pub enable_front_insert: bool,
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
@@ -271,12 +277,16 @@ impl Timeline {
|
||||
let subscription = TimelineSub::default();
|
||||
let selected_view = 0;
|
||||
|
||||
// by default, disabled for profiles since they contain widgets above the list items
|
||||
let enable_front_insert = !matches!(kind, TimelineKind::Profile(_));
|
||||
|
||||
Timeline {
|
||||
kind,
|
||||
filter,
|
||||
views,
|
||||
subscription,
|
||||
selected_view,
|
||||
enable_front_insert,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +412,9 @@ impl Timeline {
|
||||
match view.filter {
|
||||
ViewFilter::NotesAndReplies => {
|
||||
let res: Vec<&NotePayload<'_>> = payloads.iter().collect();
|
||||
if let Some(res) = view.insert(res, ndb, txn, reversed) {
|
||||
if let Some(res) =
|
||||
view.insert(res, ndb, txn, reversed, self.enable_front_insert)
|
||||
{
|
||||
res.process(unknown_ids, ndb, txn);
|
||||
}
|
||||
}
|
||||
@@ -418,7 +430,13 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(res) = view.insert(filtered_payloads, ndb, txn, reversed) {
|
||||
if let Some(res) = view.insert(
|
||||
filtered_payloads,
|
||||
ndb,
|
||||
txn,
|
||||
reversed,
|
||||
self.enable_front_insert,
|
||||
) {
|
||||
res.process(unknown_ids, ndb, txn);
|
||||
}
|
||||
}
|
||||
@@ -676,18 +694,32 @@ fn setup_initial_timeline(
|
||||
timeline.subscription, timeline.filter
|
||||
);
|
||||
|
||||
let notes = {
|
||||
let mut notes = Vec::new();
|
||||
|
||||
for package in filters.local().packages {
|
||||
let mut lim = 0i32;
|
||||
for filter in filters.local() {
|
||||
for filter in package.filters {
|
||||
lim += filter.limit().unwrap_or(1) as i32;
|
||||
}
|
||||
|
||||
debug!("setup_initial_timeline: limit for local filter is {}", lim);
|
||||
|
||||
let notes: Vec<NoteRef> = ndb
|
||||
.query(txn, filters.local(), lim)?
|
||||
let cur_notes: Vec<NoteRef> = ndb
|
||||
.query(txn, package.filters, lim)?
|
||||
.into_iter()
|
||||
.map(NoteRef::from_query_result)
|
||||
.collect();
|
||||
tracing::debug!(
|
||||
"Found {} notes for kind: {:?}",
|
||||
cur_notes.len(),
|
||||
package.kind
|
||||
);
|
||||
notes.extend(&cur_notes);
|
||||
}
|
||||
|
||||
notes
|
||||
};
|
||||
|
||||
if let Some(pks) = timeline.insert_new(txn, ndb, note_cache, ¬es) {
|
||||
pks.process(ndb, txn, unknown_ids);
|
||||
|
||||
@@ -101,16 +101,25 @@ impl NoteUnits {
|
||||
|
||||
let inserted_new = new_order.len();
|
||||
|
||||
let front_insertion = inserted_new > 0
|
||||
&& if self.order.is_empty() || new_order.is_empty() {
|
||||
true
|
||||
} else if !self.reversed {
|
||||
let first_new = *new_order.first().unwrap();
|
||||
let last_old = *self.order.last().unwrap();
|
||||
let front_insertion = if self.order.is_empty() || new_order.is_empty() {
|
||||
!new_order.is_empty()
|
||||
} else if self.reversed {
|
||||
// reversed is true, sorting should occur less recent to most recent (oldest to newest, opposite of `self.order`)
|
||||
let first_new = *new_order.first().unwrap(); // most recent unit of the new order
|
||||
let last_old = *self.order.last().unwrap(); // least recent unit of the current order
|
||||
|
||||
// if the most recent unit of the new order is less recent than the least recent unit of the current order,
|
||||
// all current order units are less recent than the new order units.
|
||||
// In other words, they are all being inserted in the front
|
||||
self.storage[first_new] >= self.storage[last_old]
|
||||
} else {
|
||||
let last_new = *new_order.last().unwrap();
|
||||
let first_old = *self.order.first().unwrap();
|
||||
// reversed is false, sorting should occur most recent to least recent (newest to oldest, as it is in `self.order`)
|
||||
let last_new = *new_order.last().unwrap(); // least recent unit of the new order
|
||||
let first_old = *self.order.first().unwrap(); // most recent unit of the current order
|
||||
|
||||
// if the least recent unit of the new order is more recent than the most recent unit of the current order,
|
||||
// all new units are more recent than the current units.
|
||||
// In other words, they are all being inserted in the front
|
||||
self.storage[last_new] <= self.storage[first_old]
|
||||
};
|
||||
|
||||
@@ -119,10 +128,10 @@ impl NoteUnits {
|
||||
while i < self.order.len() && j < new_order.len() {
|
||||
let index_left = self.order[i];
|
||||
let index_right = new_order[j];
|
||||
let left_item = &self.storage[index_left];
|
||||
let right_item = &self.storage[index_right];
|
||||
if left_item <= right_item {
|
||||
// left_item is newer than right_item
|
||||
let left_unit = &self.storage[index_left];
|
||||
let right_unit = &self.storage[index_right];
|
||||
if left_unit <= right_unit {
|
||||
// the left unit is more recent than (or the same recency as) the right unit
|
||||
merged.push(index_left);
|
||||
i += 1;
|
||||
} else {
|
||||
|
||||
@@ -31,7 +31,6 @@ pub fn render_timeline_route(
|
||||
| TimelineKind::Generic(_) => {
|
||||
let note_action =
|
||||
ui::TimelineView::new(kind, timeline_cache, note_context, note_options, jobs, col)
|
||||
.scroll_to_top(scroll_to_top)
|
||||
.ui(ui);
|
||||
|
||||
note_action.map(RenderNavAction::NoteAction)
|
||||
|
||||
@@ -209,7 +209,7 @@ fn to_repost<'a>(
|
||||
let reposted_note = match get_reposted_note(ndb, txn, &payload.note) {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
tracing::error!(
|
||||
tracing::debug!(
|
||||
"Could not get reposted note for note id {}",
|
||||
enostr::NoteId::new(*payload.note.id()).hex()
|
||||
);
|
||||
|
||||
@@ -342,6 +342,13 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
|
||||
ScrollArea::vertical()
|
||||
.id_salt(PostView::scroll_id())
|
||||
.show(ui, |ui| self.ui_no_scroll(txn, ui))
|
||||
.inner
|
||||
}
|
||||
|
||||
pub fn ui_no_scroll(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
|
||||
while let Some(selected_file) = get_next_selected_file() {
|
||||
match selected_file {
|
||||
Ok(selected_media) => {
|
||||
@@ -358,13 +365,6 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
}
|
||||
}
|
||||
|
||||
ScrollArea::vertical()
|
||||
.id_salt(PostView::scroll_id())
|
||||
.show(ui, |ui| self.ui_no_scroll(txn, ui))
|
||||
.inner
|
||||
}
|
||||
|
||||
pub fn ui_no_scroll(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
|
||||
let focused = self.focused(ui);
|
||||
let stroke = if focused {
|
||||
ui.visuals().selection.stroke
|
||||
|
||||
@@ -39,6 +39,11 @@ pub enum ProfileViewAction {
|
||||
Follow(Pubkey),
|
||||
}
|
||||
|
||||
struct ProfileScrollResponse {
|
||||
body_end_pos: f32,
|
||||
action: Option<ProfileViewAction>,
|
||||
}
|
||||
|
||||
impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
@@ -65,15 +70,13 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<ProfileViewAction> {
|
||||
let scroll_id = ProfileView::scroll_id(self.col_id, self.pubkey);
|
||||
let offset_id = scroll_id.with("scroll_offset");
|
||||
let scroll_area = ScrollArea::vertical().id_salt(scroll_id).animated(false);
|
||||
|
||||
let mut scroll_area = ScrollArea::vertical().id_salt(scroll_id);
|
||||
let profile_timeline = self
|
||||
.timeline_cache
|
||||
.get_mut(&TimelineKind::Profile(*self.pubkey))?;
|
||||
|
||||
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
||||
}
|
||||
|
||||
let output = scroll_area.show(ui, |ui| 's: {
|
||||
let output = scroll_area.show(ui, |ui| {
|
||||
let mut action = None;
|
||||
let txn = Transaction::new(self.note_context.ndb).expect("txn");
|
||||
let profile = self
|
||||
@@ -82,23 +85,19 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
.get_profile_by_pubkey(&txn, self.pubkey.bytes())
|
||||
.ok();
|
||||
|
||||
if let Some(profile_view_action) = self.profile_body(ui, profile.as_ref()) {
|
||||
if let Some(profile_view_action) =
|
||||
profile_body(ui, self.pubkey, self.note_context, profile.as_ref())
|
||||
{
|
||||
action = Some(profile_view_action);
|
||||
}
|
||||
|
||||
let Some(profile_timeline) = self
|
||||
.timeline_cache
|
||||
.get_mut(&TimelineKind::Profile(*self.pubkey))
|
||||
else {
|
||||
break 's action;
|
||||
};
|
||||
|
||||
profile_timeline.selected_view = tabs_ui(
|
||||
let tabs_resp = tabs_ui(
|
||||
ui,
|
||||
self.note_context.i18n,
|
||||
profile_timeline.selected_view,
|
||||
&profile_timeline.views,
|
||||
);
|
||||
profile_timeline.selected_view = tabs_resp.inner;
|
||||
|
||||
let reversed = false;
|
||||
// poll for new notes and insert them into our existing notes
|
||||
@@ -124,17 +123,23 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
action = Some(ProfileViewAction::Note(note_action));
|
||||
}
|
||||
|
||||
action
|
||||
ProfileScrollResponse {
|
||||
body_end_pos: tabs_resp.response.rect.bottom(),
|
||||
action,
|
||||
}
|
||||
});
|
||||
|
||||
ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y));
|
||||
// only allow front insert when the profile body is fully obstructed
|
||||
profile_timeline.enable_front_insert = output.inner.body_end_pos < ui.clip_rect().top();
|
||||
|
||||
output.inner
|
||||
output.inner.action
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_body(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
pubkey: &Pubkey,
|
||||
note_context: &mut NoteContext,
|
||||
profile: Option<&ProfileRecord<'_>>,
|
||||
) -> Option<ProfileViewAction> {
|
||||
let mut action = None;
|
||||
@@ -158,16 +163,16 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
ui.horizontal(|ui| {
|
||||
ui.put(
|
||||
pfp_rect,
|
||||
&mut ProfilePic::new(self.note_context.img_cache, get_profile_url(profile))
|
||||
&mut ProfilePic::new(note_context.img_cache, get_profile_url(profile))
|
||||
.size(size)
|
||||
.border(ProfilePic::border_stroke(ui)),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(copy_key_widget(&pfp_rect, self.note_context.i18n))
|
||||
.add(copy_key_widget(&pfp_rect, note_context.i18n))
|
||||
.clicked()
|
||||
{
|
||||
let to_copy = if let Some(bech) = self.pubkey.npub() {
|
||||
let to_copy = if let Some(bech) = pubkey.npub() {
|
||||
bech
|
||||
} else {
|
||||
error!("Could not convert Pubkey to bech");
|
||||
@@ -179,12 +184,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
ui.with_layout(Layout::right_to_left(egui::Align::RIGHT), |ui| {
|
||||
ui.add_space(24.0);
|
||||
|
||||
let target_key = self.pubkey;
|
||||
let selected = self.note_context.accounts.get_selected_account();
|
||||
let target_key = pubkey;
|
||||
let selected = note_context.accounts.get_selected_account();
|
||||
|
||||
let profile_type = if selected.key.secret_key.is_none() {
|
||||
ProfileType::ReadOnly
|
||||
} else if &selected.key.pubkey == self.pubkey {
|
||||
} else if &selected.key.pubkey == pubkey {
|
||||
ProfileType::MyProfile
|
||||
} else {
|
||||
ProfileType::Followable(selected.is_following(target_key.bytes()))
|
||||
@@ -192,10 +197,7 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
|
||||
match profile_type {
|
||||
ProfileType::MyProfile => {
|
||||
if ui
|
||||
.add(edit_profile_button(self.note_context.i18n))
|
||||
.clicked()
|
||||
{
|
||||
if ui.add(edit_profile_button(note_context.i18n)).clicked() {
|
||||
action = Some(ProfileViewAction::EditProfile);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +265,6 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
|
||||
action
|
||||
}
|
||||
}
|
||||
|
||||
enum ProfileType {
|
||||
MyProfile,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use egui::containers::scroll_area::ScrollBarVisibility;
|
||||
use egui::{vec2, Direction, Layout, Margin, Pos2, ScrollArea, Sense, Stroke};
|
||||
use egui::{vec2, Color32, Direction, Layout, Margin, Pos2, RichText, ScrollArea, Sense, Stroke};
|
||||
use egui_tabs::TabColor;
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{ProfileRecord, Transaction};
|
||||
use nostrdb::{Note, ProfileRecord, Transaction};
|
||||
use notedeck::fonts::get_font_size;
|
||||
use notedeck::name::get_display_name;
|
||||
use notedeck::ui::is_narrow;
|
||||
use notedeck::{tr_plural, JobsCache, Muted, NoteRef};
|
||||
use notedeck::{tr_plural, JobsCache, Muted, NotedeckTextStyle};
|
||||
use notedeck_ui::app_images::{like_image, repost_image};
|
||||
use notedeck_ui::ProfilePic;
|
||||
use notedeck_ui::{ProfilePic, ProfilePreview};
|
||||
use std::f32::consts::PI;
|
||||
use tracing::{error, warn};
|
||||
|
||||
@@ -88,7 +89,7 @@ fn timeline_ui(
|
||||
ui: &mut egui::Ui,
|
||||
timeline_id: &TimelineKind,
|
||||
timeline_cache: &mut TimelineCache,
|
||||
note_options: NoteOptions,
|
||||
mut note_options: NoteOptions,
|
||||
note_context: &mut NoteContext,
|
||||
jobs: &mut JobsCache,
|
||||
col: usize,
|
||||
@@ -118,7 +119,8 @@ fn timeline_ui(
|
||||
note_context.i18n,
|
||||
timeline.selected_view,
|
||||
&timeline.views,
|
||||
);
|
||||
)
|
||||
.inner;
|
||||
|
||||
// need this for some reason??
|
||||
ui.add_space(3.0);
|
||||
@@ -151,12 +153,6 @@ fn timeline_ui(
|
||||
.auto_shrink([false, false])
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible);
|
||||
|
||||
let offset_id = scroll_id.with("timeline_scroll_offset");
|
||||
|
||||
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
||||
}
|
||||
|
||||
if goto_top_resp.is_some_and(|r| r.clicked()) {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(0.0);
|
||||
}
|
||||
@@ -181,6 +177,10 @@ fn timeline_ui(
|
||||
|
||||
let txn = Transaction::new(note_context.ndb).expect("failed to create txn");
|
||||
|
||||
if matches!(timeline_id, TimelineKind::Notifications(_)) {
|
||||
note_options.set(NoteOptions::Notification, true)
|
||||
}
|
||||
|
||||
TimelineTabView::new(
|
||||
timeline.current_view(),
|
||||
note_options,
|
||||
@@ -191,8 +191,6 @@ fn timeline_ui(
|
||||
.show(ui)
|
||||
});
|
||||
|
||||
ui.data_mut(|d| d.insert_temp(offset_id, scroll_output.state.offset.y));
|
||||
|
||||
let at_top_after_scroll = scroll_output.state.offset.y == 0.0;
|
||||
let cur_show_top_button = ui.ctx().data(|d| d.get_temp::<bool>(show_top_button_id));
|
||||
|
||||
@@ -280,7 +278,7 @@ pub fn tabs_ui(
|
||||
i18n: &mut Localization,
|
||||
selected: usize,
|
||||
views: &[TimelineTab],
|
||||
) -> usize {
|
||||
) -> egui::InnerResponse<usize> {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
|
||||
let tab_res = egui_tabs::Tabs::new(views.len() as i32)
|
||||
@@ -328,7 +326,9 @@ pub fn tabs_ui(
|
||||
|
||||
let sel = tab_res.selected().unwrap_or_default();
|
||||
|
||||
let (underline, underline_y) = tab_res.inner()[sel as usize].inner;
|
||||
let res_inner = &tab_res.inner()[sel as usize];
|
||||
|
||||
let (underline, underline_y) = res_inner.inner;
|
||||
let underline_width = underline.span();
|
||||
|
||||
let tab_anim_id = ui.id().with("tab_anim");
|
||||
@@ -355,7 +355,7 @@ pub fn tabs_ui(
|
||||
|
||||
ui.painter().hline(underline, underline_y, stroke);
|
||||
|
||||
sel as usize
|
||||
egui::InnerResponse::new(sel as usize, res_inner.response.clone())
|
||||
}
|
||||
|
||||
fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 {
|
||||
@@ -440,15 +440,46 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
||||
entry: &NoteUnit,
|
||||
mute: &std::sync::Arc<Muted>,
|
||||
) -> RenderEntryResponse {
|
||||
let underlying_note = {
|
||||
let underlying_note_key = match entry {
|
||||
NoteUnit::Single(note_ref) => note_ref.key,
|
||||
NoteUnit::Composite(composite_unit) => match composite_unit {
|
||||
CompositeUnit::Reaction(reaction_unit) => reaction_unit.note_reacted_to.key,
|
||||
CompositeUnit::Repost(repost_unit) => repost_unit.note_reposted.key,
|
||||
},
|
||||
};
|
||||
|
||||
let Ok(note) = self
|
||||
.note_context
|
||||
.ndb
|
||||
.get_note_by_key(self.txn, underlying_note_key)
|
||||
else {
|
||||
warn!("failed to query note {:?}", underlying_note_key);
|
||||
return RenderEntryResponse::Unsuccessful;
|
||||
};
|
||||
|
||||
note
|
||||
};
|
||||
|
||||
let muted = root_note_id_from_selected_id(
|
||||
self.note_context.ndb,
|
||||
self.note_context.note_cache,
|
||||
self.txn,
|
||||
underlying_note.id(),
|
||||
)
|
||||
.is_ok_and(|root_id| mute.is_muted(&underlying_note, root_id.bytes()));
|
||||
|
||||
if muted {
|
||||
return RenderEntryResponse::Success(None);
|
||||
}
|
||||
|
||||
match entry {
|
||||
NoteUnit::Single(note_ref) => render_note(
|
||||
NoteUnit::Single(_) => render_note(
|
||||
ui,
|
||||
self.note_context,
|
||||
self.note_options,
|
||||
self.jobs,
|
||||
mute,
|
||||
self.txn,
|
||||
note_ref,
|
||||
&underlying_note,
|
||||
),
|
||||
NoteUnit::Composite(composite) => match composite {
|
||||
CompositeUnit::Reaction(reaction_unit) => render_reaction_cluster(
|
||||
@@ -458,6 +489,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
||||
self.jobs,
|
||||
mute,
|
||||
self.txn,
|
||||
&underlying_note,
|
||||
reaction_unit,
|
||||
),
|
||||
CompositeUnit::Repost(repost_unit) => render_repost_cluster(
|
||||
@@ -467,6 +499,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
||||
self.jobs,
|
||||
mute,
|
||||
self.txn,
|
||||
&underlying_note,
|
||||
repost_unit,
|
||||
),
|
||||
},
|
||||
@@ -483,7 +516,9 @@ impl CompositeType {
|
||||
fn image(&self, darkmode: bool) -> egui::Image<'static> {
|
||||
match self {
|
||||
CompositeType::Reaction => like_image(),
|
||||
CompositeType::Repost => repost_image(darkmode),
|
||||
CompositeType::Repost => {
|
||||
repost_image(darkmode).tint(Color32::from_rgb(0x68, 0xC3, 0x51))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,6 +528,7 @@ impl CompositeType {
|
||||
first_name: &str,
|
||||
total_count: usize,
|
||||
referenced_type: ReferencedNoteType,
|
||||
notification: bool,
|
||||
) -> String {
|
||||
let count = total_count - 1;
|
||||
|
||||
@@ -500,7 +536,16 @@ impl CompositeType {
|
||||
CompositeType::Reaction => {
|
||||
reaction_description(loc, first_name, count, referenced_type)
|
||||
}
|
||||
CompositeType::Repost => repost_description(loc, first_name, count, referenced_type),
|
||||
CompositeType::Repost => repost_description(
|
||||
loc,
|
||||
first_name,
|
||||
count,
|
||||
if notification {
|
||||
DescriptionType::Notification(referenced_type)
|
||||
} else {
|
||||
DescriptionType::Other
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,13 +598,19 @@ fn reaction_description(
|
||||
}
|
||||
}
|
||||
|
||||
enum DescriptionType {
|
||||
Notification(ReferencedNoteType),
|
||||
Other,
|
||||
}
|
||||
|
||||
fn repost_description(
|
||||
loc: &mut Localization,
|
||||
first_name: &str,
|
||||
count: usize,
|
||||
referenced_type: ReferencedNoteType,
|
||||
description_type: DescriptionType,
|
||||
) -> String {
|
||||
match referenced_type {
|
||||
match description_type {
|
||||
DescriptionType::Notification(referenced_type) => match referenced_type {
|
||||
ReferencedNoteType::Tagged => {
|
||||
if count == 0 {
|
||||
tr!(
|
||||
@@ -598,6 +649,26 @@ fn repost_description(
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
DescriptionType::Other => {
|
||||
if count == 0 {
|
||||
tr!(
|
||||
loc,
|
||||
"{name} reposted",
|
||||
"repost from user",
|
||||
name = first_name
|
||||
)
|
||||
} else {
|
||||
tr_plural!(
|
||||
loc,
|
||||
"{name} and {count} other reposted",
|
||||
"{name} and {count} others reposted",
|
||||
"describing the amount of reposts a note has",
|
||||
count,
|
||||
name = first_name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,32 +677,11 @@ fn render_note(
|
||||
note_context: &mut NoteContext,
|
||||
note_options: NoteOptions,
|
||||
jobs: &mut JobsCache,
|
||||
mute: &std::sync::Arc<Muted>,
|
||||
txn: &Transaction,
|
||||
note_ref: &NoteRef,
|
||||
note: &Note,
|
||||
) -> RenderEntryResponse {
|
||||
let note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, note_ref.key) {
|
||||
note
|
||||
} else {
|
||||
warn!("failed to query note {:?}", note_ref.key);
|
||||
return RenderEntryResponse::Unsuccessful;
|
||||
};
|
||||
|
||||
let muted = if let Ok(root_id) =
|
||||
root_note_id_from_selected_id(note_context.ndb, note_context.note_cache, txn, note.id())
|
||||
{
|
||||
mute.is_muted(¬e, root_id.bytes())
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if muted {
|
||||
return RenderEntryResponse::Success(None);
|
||||
}
|
||||
|
||||
let mut action = None;
|
||||
notedeck_ui::padding(8.0, ui, |ui| {
|
||||
let resp = NoteView::new(note_context, ¬e, note_options, jobs).show(ui);
|
||||
let resp = NoteView::new(note_context, note, note_options, jobs).show(ui);
|
||||
|
||||
if let Some(note_action) = resp.action {
|
||||
action = Some(note_action);
|
||||
@@ -643,6 +693,7 @@ fn render_note(
|
||||
RenderEntryResponse::Success(action)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_reaction_cluster(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
@@ -650,16 +701,9 @@ fn render_reaction_cluster(
|
||||
jobs: &mut JobsCache,
|
||||
mute: &std::sync::Arc<Muted>,
|
||||
txn: &Transaction,
|
||||
underlying_note: &Note,
|
||||
reaction: &ReactionUnit,
|
||||
) -> RenderEntryResponse {
|
||||
let reacted_to_key = reaction.note_reacted_to.key;
|
||||
let reacted_to_note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, reacted_to_key) {
|
||||
note
|
||||
} else {
|
||||
warn!("failed to query note {:?}", reacted_to_key);
|
||||
return RenderEntryResponse::Unsuccessful;
|
||||
};
|
||||
|
||||
let profiles_to_show: Vec<ProfileEntry> = reaction
|
||||
.reactions
|
||||
.values()
|
||||
@@ -674,20 +718,21 @@ fn render_reaction_cluster(
|
||||
render_composite_entry(
|
||||
ui,
|
||||
note_context,
|
||||
note_options,
|
||||
note_options | NoteOptions::Notification,
|
||||
jobs,
|
||||
reacted_to_note,
|
||||
underlying_note,
|
||||
profiles_to_show,
|
||||
CompositeType::Reaction,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_composite_entry(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
note_options: NoteOptions,
|
||||
mut note_options: NoteOptions,
|
||||
jobs: &mut JobsCache,
|
||||
underlying_note: nostrdb::Note<'_>,
|
||||
underlying_note: &nostrdb::Note<'_>,
|
||||
profiles_to_show: Vec<ProfileEntry>,
|
||||
composite_type: CompositeType,
|
||||
) -> RenderEntryResponse {
|
||||
@@ -697,45 +742,6 @@ fn render_composite_entry(
|
||||
let num_profiles = profiles_to_show.len();
|
||||
|
||||
let mut action = None;
|
||||
egui::Frame::new()
|
||||
.inner_margin(Margin::symmetric(8, 4))
|
||||
.show(ui, |ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
vec2(ui.available_width(), 32.0),
|
||||
Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.add_sized(
|
||||
vec2(28.0, 28.0),
|
||||
composite_type.image(ui.visuals().dark_mode),
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ScrollArea::horizontal()
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.show(ui, |ui| {
|
||||
for entry in profiles_to_show {
|
||||
let resp = ui.add(
|
||||
&mut ProfilePic::from_profile_or_default(
|
||||
note_context.img_cache,
|
||||
entry.record.as_ref(),
|
||||
)
|
||||
.size(24.0)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
|
||||
if resp.clicked() {
|
||||
action = Some(NoteAction::Profile(*entry.pk))
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let referenced_type = if note_context
|
||||
.accounts
|
||||
@@ -750,39 +756,189 @@ fn render_composite_entry(
|
||||
ReferencedNoteType::Yours
|
||||
};
|
||||
|
||||
ui.add_space(2.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(52.0);
|
||||
if !note_options.contains(NoteOptions::TrustMedia) {
|
||||
let acc = note_context.accounts.get_selected_account();
|
||||
for entry in &profiles_to_show {
|
||||
if matches!(acc.is_following(entry.pk), notedeck::IsFollowing::Yes) {
|
||||
note_options = note_options.union(NoteOptions::TrustMedia);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(composite_type.description(
|
||||
egui::Frame::new()
|
||||
.inner_margin(Margin::symmetric(8, 4))
|
||||
.show(ui, |ui| {
|
||||
let show_label_newline = ui
|
||||
.horizontal_wrapped(|ui| {
|
||||
let pfps_resp = ui
|
||||
.allocate_ui_with_layout(
|
||||
vec2(ui.available_width(), 32.0),
|
||||
Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
render_profiles(
|
||||
ui,
|
||||
profiles_to_show,
|
||||
&composite_type,
|
||||
note_context.img_cache,
|
||||
note_options.contains(NoteOptions::Notification),
|
||||
)
|
||||
},
|
||||
)
|
||||
.inner;
|
||||
|
||||
if let Some(cur_action) = pfps_resp.action {
|
||||
action = Some(cur_action);
|
||||
}
|
||||
|
||||
let description = composite_type.description(
|
||||
note_context.i18n,
|
||||
&first_name,
|
||||
num_profiles,
|
||||
referenced_type,
|
||||
))
|
||||
note_options.contains(NoteOptions::Notification),
|
||||
);
|
||||
let galley = ui.painter().layout_no_wrap(
|
||||
description.clone(),
|
||||
NotedeckTextStyle::Small.get_font_id(ui.ctx()),
|
||||
ui.visuals().text_color(),
|
||||
);
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
let galley_pos = {
|
||||
let mut galley_pos = ui.next_widget_position();
|
||||
galley_pos.y = pfps_resp.resp.rect.right_center().y;
|
||||
galley_pos.y -= galley.rect.height() / 2.0;
|
||||
galley_pos
|
||||
};
|
||||
|
||||
let fits_no_wrap = {
|
||||
let mut rightmost_pos = galley_pos;
|
||||
rightmost_pos.x += galley.rect.width();
|
||||
|
||||
ui.available_rect_before_wrap().contains(rightmost_pos)
|
||||
};
|
||||
|
||||
if fits_no_wrap {
|
||||
ui.painter()
|
||||
.galley(galley_pos, galley, ui.visuals().text_color());
|
||||
None
|
||||
} else {
|
||||
Some(description)
|
||||
}
|
||||
})
|
||||
.inner;
|
||||
|
||||
if let Some(desc) = show_label_newline {
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(48.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.add(egui::Label::new(
|
||||
RichText::new(desc)
|
||||
.size(get_font_size(ui.ctx(), &NotedeckTextStyle::Small)),
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(48.0);
|
||||
let options = note_options
|
||||
let resp = ui
|
||||
.horizontal(|ui| {
|
||||
if note_options.contains(NoteOptions::Notification) {
|
||||
note_options = note_options
|
||||
.difference(NoteOptions::ActionBar | NoteOptions::OptionsButton)
|
||||
.union(NoteOptions::NotificationPreview);
|
||||
let resp = NoteView::new(note_context, &underlying_note, options, jobs).show(ui);
|
||||
|
||||
ui.add_space(48.0);
|
||||
};
|
||||
NoteView::new(note_context, underlying_note, note_options, jobs).show(ui)
|
||||
})
|
||||
.inner;
|
||||
|
||||
if let Some(note_action) = resp.action {
|
||||
action = Some(note_action);
|
||||
action.get_or_insert(note_action);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
notedeck_ui::hline(ui);
|
||||
RenderEntryResponse::Success(action)
|
||||
}
|
||||
|
||||
fn render_profiles(
|
||||
ui: &mut egui::Ui,
|
||||
profiles_to_show: Vec<ProfileEntry>,
|
||||
composite_type: &CompositeType,
|
||||
img_cache: &mut notedeck::Images,
|
||||
notification: bool,
|
||||
) -> PfpsResponse {
|
||||
let mut action = None;
|
||||
if notification {
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(9.0);
|
||||
ui.add_sized(
|
||||
vec2(20.0, 20.0),
|
||||
composite_type.image(ui.visuals().dark_mode),
|
||||
);
|
||||
});
|
||||
|
||||
if notification {
|
||||
ui.add_space(16.0);
|
||||
} else {
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
|
||||
let resp = ui.horizontal(|ui| {
|
||||
ScrollArea::horizontal()
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.show(ui, |ui| {
|
||||
let mut last_pfp_resp = None;
|
||||
for entry in profiles_to_show {
|
||||
let mut resp = ui.add(
|
||||
&mut ProfilePic::from_profile_or_default(img_cache, entry.record.as_ref())
|
||||
.size(24.0)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
|
||||
if let Some(record) = entry.record.as_ref() {
|
||||
resp = resp.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(300.0);
|
||||
ui.add(ProfilePreview::new(record, img_cache));
|
||||
});
|
||||
}
|
||||
|
||||
last_pfp_resp = Some(resp.clone());
|
||||
|
||||
if resp.clicked() {
|
||||
action = Some(NoteAction::Profile(*entry.pk))
|
||||
}
|
||||
}
|
||||
|
||||
last_pfp_resp
|
||||
})
|
||||
.inner
|
||||
});
|
||||
|
||||
let resp = if let Some(r) = resp.inner {
|
||||
r
|
||||
} else {
|
||||
resp.response
|
||||
};
|
||||
|
||||
PfpsResponse { action, resp }
|
||||
}
|
||||
|
||||
struct PfpsResponse {
|
||||
action: Option<NoteAction>,
|
||||
resp: egui::Response,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_repost_cluster(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
@@ -790,16 +946,9 @@ fn render_repost_cluster(
|
||||
jobs: &mut JobsCache,
|
||||
mute: &std::sync::Arc<Muted>,
|
||||
txn: &Transaction,
|
||||
underlying_note: &Note,
|
||||
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()
|
||||
@@ -815,7 +964,7 @@ fn render_repost_cluster(
|
||||
note_context,
|
||||
note_options,
|
||||
jobs,
|
||||
reposted_note,
|
||||
underlying_note,
|
||||
profiles_to_show,
|
||||
CompositeType::Repost,
|
||||
)
|
||||
|
||||
@@ -6,9 +6,7 @@ use crate::{
|
||||
use egui::{Color32, Hyperlink, Label, RichText};
|
||||
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
|
||||
use notedeck::Localization;
|
||||
use notedeck::{
|
||||
time_format, update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle,
|
||||
};
|
||||
use notedeck::{time_format, update_imeta_blurhashes, NoteCache, NoteContext, NotedeckTextStyle};
|
||||
use notedeck::{JobsCache, RenderableMedia};
|
||||
use tracing::warn;
|
||||
|
||||
@@ -374,21 +372,6 @@ fn render_undecorated_note_contents<'a>(
|
||||
ui.add_space(2.0);
|
||||
let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note")));
|
||||
|
||||
let is_self = note.pubkey()
|
||||
== note_context
|
||||
.accounts
|
||||
.get_selected_account()
|
||||
.key
|
||||
.pubkey
|
||||
.bytes();
|
||||
|
||||
let trusted_media = is_self
|
||||
|| note_context
|
||||
.accounts
|
||||
.get_selected_account()
|
||||
.is_following(note.pubkey())
|
||||
== IsFollowing::Yes;
|
||||
|
||||
media_action = image_carousel(
|
||||
ui,
|
||||
note_context.img_cache,
|
||||
@@ -396,7 +379,6 @@ fn render_undecorated_note_contents<'a>(
|
||||
jobs,
|
||||
&supported_medias,
|
||||
carousel_id,
|
||||
trusted_media,
|
||||
note_context.i18n,
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -33,7 +33,6 @@ pub fn image_carousel(
|
||||
jobs: &mut JobsCache,
|
||||
medias: &[RenderableMedia],
|
||||
carousel_id: egui::Id,
|
||||
trusted_media: bool,
|
||||
i18n: &mut Localization,
|
||||
note_options: NoteOptions,
|
||||
) -> Option<MediaAction> {
|
||||
@@ -68,7 +67,7 @@ pub fn image_carousel(
|
||||
job_pool,
|
||||
jobs,
|
||||
media,
|
||||
trusted_media,
|
||||
note_options.contains(NoteOptions::TrustMedia),
|
||||
i18n,
|
||||
size,
|
||||
if note_options.contains(NoteOptions::NoAnimations) {
|
||||
|
||||
@@ -5,10 +5,7 @@ pub mod options;
|
||||
pub mod reply_description;
|
||||
|
||||
use crate::{app_images, secondary_label};
|
||||
use crate::{
|
||||
profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview,
|
||||
PulseAlpha, Username,
|
||||
};
|
||||
use crate::{widgets::x_button, ProfilePic, ProfilePreview, PulseAlpha, Username};
|
||||
|
||||
pub use contents::{render_note_preview, NoteContents};
|
||||
pub use context::NoteContextButton;
|
||||
@@ -25,14 +22,12 @@ pub use options::NoteOptions;
|
||||
pub use reply_description::reply_desc;
|
||||
|
||||
use egui::emath::{pos2, Vec2};
|
||||
use egui::{Id, Pos2, Rect, Response, RichText, Sense};
|
||||
use egui::{Id, Pos2, Rect, Response, Sense};
|
||||
use enostr::{KeypairUnowned, NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
|
||||
use notedeck::{
|
||||
name::get_display_name,
|
||||
note::{NoteAction, NoteContext, ZapAction},
|
||||
tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, NotedeckTextStyle,
|
||||
ZapTarget, Zaps,
|
||||
tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, ZapTarget, Zaps,
|
||||
};
|
||||
|
||||
pub struct NoteView<'a, 'd> {
|
||||
@@ -306,60 +301,19 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_repost(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
txn: &Transaction,
|
||||
note_to_repost: Note<'_>,
|
||||
) -> NoteResponse {
|
||||
let profile = self
|
||||
.note_context
|
||||
.ndb
|
||||
.get_profile_by_pubkey(txn, self.note.pubkey());
|
||||
|
||||
let style = NotedeckTextStyle::Small;
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.add_sized([20.0, 20.0], repost_icon(ui.visuals().dark_mode));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
let resp = ui.add(one_line_display_name_widget(
|
||||
ui.visuals(),
|
||||
get_display_name(profile.as_ref().ok()),
|
||||
style,
|
||||
));
|
||||
if let Ok(rec) = &profile {
|
||||
resp.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(300.0);
|
||||
ui.add(ProfilePreview::new(rec, self.note_context.img_cache));
|
||||
});
|
||||
}
|
||||
let color = ui.style().visuals.noninteractive().fg_stroke.color;
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(tr!(
|
||||
self.note_context.i18n,
|
||||
"Reposted",
|
||||
"Label for reposted notes"
|
||||
))
|
||||
.color(color)
|
||||
.text_style(style.text_style()),
|
||||
);
|
||||
});
|
||||
NoteView::new(self.note_context, ¬e_to_repost, self.flags, self.jobs).show(ui)
|
||||
}
|
||||
|
||||
pub fn show_impl(&mut self, ui: &mut egui::Ui) -> NoteResponse {
|
||||
let txn = self.note.txn().expect("txn");
|
||||
if let Some(note_to_repost) = get_reposted_note(self.note_context.ndb, txn, self.note) {
|
||||
self.show_repost(ui, txn, note_to_repost)
|
||||
} else {
|
||||
self.show_standard(ui)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse {
|
||||
if !self.flags.contains(NoteOptions::TrustMedia) {
|
||||
let acc = self.note_context.accounts.get_selected_account();
|
||||
if self.note.pubkey() == acc.key.pubkey.bytes()
|
||||
|| matches!(
|
||||
acc.is_following(self.note.pubkey()),
|
||||
notedeck::IsFollowing::Yes
|
||||
)
|
||||
{
|
||||
self.flags = self.flags.union(NoteOptions::TrustMedia);
|
||||
}
|
||||
}
|
||||
|
||||
if self.options().contains(NoteOptions::Textmode) {
|
||||
NoteResponse::new(self.textmode_ui(ui))
|
||||
} else if self.options().contains(NoteOptions::Framed) {
|
||||
@@ -376,11 +330,11 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
||||
if is_narrow(ui.ctx()) {
|
||||
ui.set_width(ui.available_width());
|
||||
}
|
||||
self.show_impl(ui)
|
||||
self.show_standard(ui)
|
||||
})
|
||||
.inner
|
||||
} else {
|
||||
self.show_impl(ui)
|
||||
self.show_standard(ui)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,7 +743,7 @@ fn note_hitbox_id(
|
||||
|
||||
fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option<Response> {
|
||||
ui.ctx()
|
||||
.data_mut(|d| d.get_persisted(hitbox_id))
|
||||
.data_mut(|d| d.get_temp(hitbox_id))
|
||||
.map(|note_size: Vec2| {
|
||||
// The hitbox should extend the entire width of the
|
||||
// container. The hitbox height was cached last layout.
|
||||
|
||||
@@ -39,8 +39,14 @@ bitflags! {
|
||||
/// no animation override (accessibility)
|
||||
const NoAnimations = 1 << 17;
|
||||
|
||||
/// Styled for a notification preview
|
||||
/// The note should be displayed as a preview of the underlying note of a composite unit
|
||||
const NotificationPreview = 1 << 18;
|
||||
|
||||
/// The note is a notification
|
||||
const Notification = 1 << 19;
|
||||
|
||||
/// There is enough trust to show media in this note
|
||||
const TrustMedia = 1 << 20;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user