36 Commits

Author SHA1 Message Date
e3e7d54142 Import translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-09-22 00:46:40 -04:00
William Casarin
a6d91c43e4 Merge a bunch of fixes from kernel
PRs

* 1141
* 1137
* 1136

kernelkind (10):
      Revert "feat: transitively trust images from parent note"
      feat: enable transitive trust for repost
      fix `NoteUnits` front insertion logic
      fix: don't reset scroll position when switching toolbar
      fix: no longer make the scroll position jump oddly
      fix: repost desc text size on newline
      make `tabs_ui` return `InnerResponse`
      refactor: impl transitive trust via `NoteOptions::TrustMedia`
      refactor: move `profile_body` to fn
      refactor: remove unnecessary code
2025-09-16 11:28:48 -07:00
kernelkind
19fe3703d9 fix: add tag for hashtag in reply
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-13 17:15:58 -04:00
kernelkind
ca67977b82 fix: don't reset scroll position when switching toolbar
Closes: https://github.com/damus-io/notedeck/issues/1140

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-13 15:37:05 -04:00
kernelkind
559e9577fc fix: no longer make the scroll position jump oddly
only allow front insert in profile when body is fully obstructed

Closes: https://github.com/damus-io/notedeck/issues/1072

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-13 15:36:18 -04:00
kernelkind
563fbb9c4b fix NoteUnits front insertion logic
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-13 15:18:29 -04:00
kernelkind
391900d393 make tabs_ui return InnerResponse
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-13 14:51:09 -04:00
kernelkind
11700d6217 refactor: move profile_body to fn
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-13 14:51:03 -04:00
kernelkind
50293a6f34 refactor: remove unnecessary code
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-13 14:49:46 -04:00
kernelkind
d6182ed7c3 Revert "feat: transitively trust images from parent note"
This reverts commit ea14713b58.
2025-09-11 19:39:12 -04:00
kernelkind
a0e9c8b434 feat: enable transitive trust for repost
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-11 19:38:32 -04:00
kernelkind
4ac2e59983 refactor: impl transitive trust via NoteOptions::TrustMedia
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-11 19:37:56 -04:00
kernelkind
3f1a194983 fix: repost desc text size on newline
make the repost desc size small when it is on a newline instead of
inline with the pfps, which was introduced here: eb446376

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-11 17:14:25 -04:00
William Casarin
a8eaea6509 test: fix relay message tests
Signed-off-by: William Casarin <jb55@jb55.com>
2025-09-10 16:56:33 -07:00
William Casarin
9278c90802 time: more time-ago granularity in months/years
before: 1y
after:  1y 8mo

etc

Signed-off-by: William Casarin <jb55@jb55.com>
2025-09-10 16:40:13 -07:00
William Casarin
02a90eccd1 enostr: show unrecognized message in log
Signed-off-by: William Casarin <jb55@jb55.com>
2025-09-10 15:33:28 -07:00
William Casarin
c0fcf53ff6 Merge a bunch of fixes by kernel
kernelkind (3):
      fix: can upload photo from reply or quote
      fix: image shimmer bug
      feat: transitively trust images from parent note
2025-09-10 12:06:31 -07:00
William Casarin
f889b54ed9 refactor: replace notification bool prop drill with note option
Signed-off-by: William Casarin <jb55@jb55.com>
2025-09-10 12:04:59 -07:00
William Casarin
7b4c96df91 images: disable useless animation frame log
Signed-off-by: William Casarin <jb55@jb55.com>
2025-09-10 12:03:41 -07:00
William Casarin
eb44637601 ui/timeline: make notification text smaller
Signed-off-by: William Casarin <jb55@jb55.com>
2025-09-10 12:03:23 -07:00
kernelkind
ea14713b58 feat: transitively trust images from parent note
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 19:56:12 -04:00
kernelkind
a5e7880e25 fix: image shimmer bug
if the same image on two seperate columns unblur at the same time,
it caused them both to continually cycle between blurred and
unblurred

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 19:20:42 -04:00
kernelkind
409ca68567 fix: can upload photo from reply or quote
moved the file retrieval check from `PostView::ui` ->
`PostView::ui_no_scroll`, which is used by the `PostReplyView` &
`QuoteRepostView`

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 18:39:02 -04:00
kernelkind
6cf193b7e3 ui: minor tweaks
closes: https://github.com/damus-io/notedeck/issues/1120

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 18:13:52 -04:00
kernelkind
5bb17cd810 log: info -> debug for ndb can't find repost
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 17:56:26 -04:00
kernelkind
ba359c95c2 allow reposts in "Notes" timeline tab
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 17:56:23 -04:00
kernelkind
e0ed122951 use NdbQueryPackage to call ndb::query multiple times
necessary to ensure we can retrieve reposts from ndb

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 17:56:19 -04:00
kernelkind
e1ad2e231f filter: add repost kind to FilteredTags::into_filter
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 17:56:14 -04:00
kernelkind
91028929b2 ui: add support for non-notification composite rendering
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 17:56:09 -04:00
kernelkind
2eef34fa1c note: remove repost from note ui
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 17:53:28 -04:00
kernelkind
b8eecf0c9a introduce NdbQueryPackages
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 17:53:22 -04:00
kernelkind
1b9e77a1ff filter: remove unused code
Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-09 17:53:12 -04:00
William Casarin
28634301b8 Merge localization fixes by Terry
Terry Yiu (1):
      Add missing localized strings and export strings for translation
2025-09-08 15:04:26 -07:00
William Casarin
ce0d3e8e88 Merge fix blank thread from notifications by kernel
kernelkind (1):
      fix blank thread from notifications
2025-09-08 15:03:50 -07:00
William Casarin
0b4545d598 filter: reservoir sample the algo feed
so its not the same static 15 pubkeys

Signed-off-by: William Casarin <jb55@jb55.com>
2025-09-08 15:03:02 -07:00
kernelkind
6db03364fd fix blank thread from notifications
forgot to check whether underlying note is muted in reaction & reposts

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-09-05 16:21:33 -04:00
29 changed files with 1053 additions and 606 deletions

15
Cargo.lock generated
View File

@@ -193,7 +193,7 @@ dependencies = [
"objc2-foundation 0.3.1", "objc2-foundation 0.3.1",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"windows-sys 0.52.0", "windows-sys 0.59.0",
"x11rb", "x11rb",
] ]
@@ -246,7 +246,7 @@ dependencies = [
"enumflags2", "enumflags2",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"rand 0.9.1", "rand 0.9.2",
"raw-window-handle", "raw-window-handle",
"serde", "serde",
"serde_repr", "serde_repr",
@@ -3542,6 +3542,7 @@ dependencies = [
"profiling", "profiling",
"puffin", "puffin",
"puffin_egui", "puffin_egui",
"rand 0.9.2",
"regex", "regex",
"secp256k1 0.30.0", "secp256k1 0.30.0",
"serde", "serde",
@@ -3683,7 +3684,7 @@ dependencies = [
"nostrdb", "nostrdb",
"notedeck", "notedeck",
"notedeck_ui", "notedeck_ui",
"rand 0.9.1", "rand 0.9.2",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -4603,7 +4604,7 @@ dependencies = [
"bytes", "bytes",
"getrandom 0.3.3", "getrandom 0.3.3",
"lru-slab", "lru-slab",
"rand 0.9.1", "rand 0.9.2",
"ring", "ring",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"rustls", "rustls",
@@ -4657,9 +4658,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.1" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rand_core 0.9.3", "rand_core 0.9.3",
@@ -6278,7 +6279,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"log", "log",
"rand 0.9.1", "rand 0.9.2",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"sha1", "sha1",

View File

@@ -19,6 +19,7 @@ chrono = "0.4.40"
base32 = "0.4.0" base32 = "0.4.0"
base64 = "0.22.1" base64 = "0.22.1"
rmpv = "1.3.0" rmpv = "1.3.0"
rand = "0.9.2"
bech32 = { version = "0.11", default-features = false } bech32 = { version = "0.11", default-features = false }
bitflags = "2.5.0" bitflags = "2.5.0"
dirs = "5.0.1" dirs = "5.0.1"

View File

@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Agregar columna de notificaciones exter
Add_Hashtag_Column_ebf4 = Agregar columna de hashtags Add_Hashtag_Column_ebf4 = Agregar columna de hashtags
# Column title for adding last notes column # Column title for adding last notes column
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas 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 # Column title for adding notifications column
Add_Notifications_Column_79f8 = Agregar columna de notificaciones Add_Notifications_Column_79f8 = Agregar columna de notificaciones
# Button label to add a relay # Button label to add a relay
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar enlace
Copy_Note_ID_6b45 = Copiar ID de nota Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard # Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota 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 the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar pubkey Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard # Copy the text content of the note to clipboard
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar billetera
Display_name_f9d9 = Nombre para mostrar Display_name_f9d9 = Nombre para mostrar
# Domain identification message # Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación 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 # Column title for editing deck
Edit_Deck_4018 = Editar deck Edit_Deck_4018 = Editar deck
# Button label to edit a 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 # Label for find user button
Find_User_bd12 = Buscar usuario Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section # Label for font size, Appearance settings section
Font_size_dd73 = Font size: Font_size_dd73 = Tamaño de la fuente:
# Title for hashtags column # Title for hashtags column
Hashtags_f8e0 = Hashtags Hashtags_f8e0 = Hashtags
# Title for Home column # Title for Home column
@@ -191,6 +197,8 @@ k_50K_c2dc = 50.000
k_5K_f7e6 = 5.000 k_5K_f7e6 = 5.000
# Description for your notes column # Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas 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 # Label for language, Appearance settings section
Language_e264 = Idioma: Language_e264 = Idioma:
# Title for last note per user column # 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 Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck # Title for the user's deck
My_Deck_4ac5 = Mi 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. # 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? New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label # NIP-05 identity field label
@@ -236,7 +252,9 @@ Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds) # Relative time for very recent events (less than 3 seconds)
now_2181 = ahora now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first # 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 # Button label to open email client
Open_Email_25e9 = Abrir correo electrónico Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client # 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. 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 URL field label
Profile_picture_81ff = Imagen de perfil Profile_picture_81ff = Imagen de perfil
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = ID DE CUENTA PÚBLICA
# Column title for quote composition # Column title for quote composition
Quote_475c = Citar Quote_475c = Citar
# Error message when quote note cannot be found # 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 # Label for reposted notes
Reposted_61c8 = Publicadas de nuevo Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section # Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset Reset_4e60 = Restablecer
# Label for reset zoom level, Appearance settings section # Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer Reset_62d4 = Restablecer
# Heading for support section # Heading for support section
@@ -309,10 +329,14 @@ Search_c573 = Búsqueda
Search_notes_42a6 = Buscar notas... Search_notes_42a6 = Buscar notas...
# Search in progress message # Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }' 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 # Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column # Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr 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 # Button label to send a zap
Send_1ea4 = Enviar Send_1ea4 = Enviar
# Column title for app settings # 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 # Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section # 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 # 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 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 # 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 # Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address # Support email address
Support_email_44d9 = Support email: Support_email_44d9 = Correo electrónico de ayuda:
# Hover text for dark mode toggle button # Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button # Hover text for light mode toggle button
@@ -405,6 +429,30 @@ Zoom_Level_29a8 = Nivel de zoom:
# Search results count # Search results count
Got__count__results_for___query_85fb = Got__count__results_for___query_85fb =
{ $count -> { $count ->
[uno] Obtuvo { $count } resultado para '{ $query }' [uno] Se obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados 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
} }

View File

@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Añadir columna de notificaciones exter
Add_Hashtag_Column_ebf4 = Añadir columna de hashtags Add_Hashtag_Column_ebf4 = Añadir columna de hashtags
# Column title for adding last notes column # Column title for adding last notes column
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas 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 # Column title for adding notifications column
Add_Notifications_Column_79f8 = Añadir columna de notificaciones Add_Notifications_Column_79f8 = Añadir columna de notificaciones
# Button label to add a relay # Button label to add a relay
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar enlace
Copy_Note_ID_6b45 = Copiar ID de nota Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard # Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota 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 the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar pubkey Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard # Copy the text content of the note to clipboard
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar monedero
Display_name_f9d9 = Nombre para mostrar Display_name_f9d9 = Nombre para mostrar
# Domain identification message # Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación 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 # Column title for editing deck
Edit_Deck_4018 = Editar deck Edit_Deck_4018 = Editar deck
# Button label to edit a 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 # Label for find user button
Find_User_bd12 = Buscar usuario Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section # Label for font size, Appearance settings section
Font_size_dd73 = Font size: Font_size_dd73 = Tamaño de la fuente:
# Title for hashtags column # Title for hashtags column
Hashtags_f8e0 = Hashtags Hashtags_f8e0 = Hashtags
# Title for Home column # Title for Home column
@@ -191,6 +197,8 @@ k_50K_c2dc = 50.000
k_5K_f7e6 = 5.000 k_5K_f7e6 = 5.000
# Description for your notes column # Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas 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 # Label for language, Appearance settings section
Language_e264 = Idioma: Language_e264 = Idioma:
# Title for last note per user column # 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 Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck # Title for the user's deck
My_Deck_4ac5 = Mi 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. # 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? New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label # NIP-05 identity field label
@@ -236,7 +252,9 @@ Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds) # Relative time for very recent events (less than 3 seconds)
now_2181 = ahora now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first # 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 # Button label to open email client
Open_Email_25e9 = Abrir correo electrónico Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client # 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. 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 URL field label
Profile_picture_81ff = Imagen de perfil Profile_picture_81ff = Imagen de perfil
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = ID DE CUENTA PÚBLICA
# Column title for quote composition # Column title for quote composition
Quote_475c = Citar Quote_475c = Citar
# Error message when quote note cannot be found # 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 # Label for reposted notes
Reposted_61c8 = Publicadas de nuevo Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section # Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset Reset_4e60 = Restablecer
# Label for reset zoom level, Appearance settings section # Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer Reset_62d4 = Restablecer
# Heading for support section # Heading for support section
@@ -309,10 +329,14 @@ Search_c573 = Búsqueda
Search_notes_42a6 = Buscar notas... Search_notes_42a6 = Buscar notas...
# Search in progress message # Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }' 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 # Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column # Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr 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 # Button label to send a zap
Send_1ea4 = Enviar Send_1ea4 = Enviar
# Column title for app settings # 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 # Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section # 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 # 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 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 # 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 # Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address # Support email address
Support_email_44d9 = Support email: Support_email_44d9 = Correo electrónico de ayuda:
# Hover text for dark mode toggle button # Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button # Hover text for light mode toggle button
@@ -405,6 +429,30 @@ Zoom_Level_29a8 = Nivel de zoom:
# Search results count # Search results count
Got__count__results_for___query_85fb = Got__count__results_for___query_85fb =
{ $count -> { $count ->
[uno] Obtuvo { $count } resultado para '{ $query }' [uno] Se ha obtenido { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados 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
} }

View File

@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Ajouter une colonne pour les notificati
Add_Hashtag_Column_ebf4 = Ajouter une colonne Hashtag Add_Hashtag_Column_ebf4 = Ajouter une colonne Hashtag
# Column title for adding last notes column # Column title for adding last notes column
Add_Last_Notes_Column_bbad = Ajouter une colonne pour les dernières notes 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 # Column title for adding notifications column
Add_Notifications_Column_79f8 = Ajouter une colonne pour les notifications Add_Notifications_Column_79f8 = Ajouter une colonne pour les notifications
# Button label to add a relay # 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_Note_ID_6b45 = Copier l'ID de la note
# Copy the raw note data in JSON format to clipboard # Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copier le JSON de la note 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 the author's public key to clipboard
Copy_Pubkey_9cc4 = Copier la Pubkey Copy_Pubkey_9cc4 = Copier la Pubkey
# Copy the text content of the note to clipboard # 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 Display_name_f9d9 = Nom d'utilisateur
# Domain identification message # Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" sera utilisé pour l'identification 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 # Column title for editing deck
Edit_Deck_4018 = Modifier le deck Edit_Deck_4018 = Modifier le deck
# Button label to edit a deck # Button label to edit a deck
@@ -191,6 +197,8 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K k_5K_f7e6 = 5K
# Description for your notes column # Description for your notes column
Keep_track_of_your_notes___replies_a334 = Gardez une trace de vos notes & réponses 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 # Label for language, Appearance settings section
Language_e264 = Langue : Language_e264 = Langue :
# Title for last note per user column # 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 Moves_this_column_to_another_position_0d4b = Déplace cette colonne vers une autre position
# Title for the user's deck # Title for the user's deck
My_Deck_4ac5 = Mon 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. # 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 ? New_to_Nostr_a2fd = Nouveau sur Nostr ?
# NIP-05 identity field label # 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. 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 URL field label
Profile_picture_81ff = Photo de profil Profile_picture_81ff = Photo de profil
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = IDENTITE PUBLIQUE DU COMPTE
# Column title for quote composition # Column title for quote composition
Quote_475c = Citation Quote_475c = Citation
# Error message when quote note cannot be found # Error message when quote note cannot be found
@@ -311,6 +329,8 @@ Search_c573 = Rechercher
Search_notes_42a6 = Rechercher des notes... Search_notes_42a6 = Rechercher des notes...
# Search in progress message # Search in progress message
Searching_for___query_5d18 = Recherche par '{ $query }' 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 # Description for Home column
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
# Description for universe column # Description for universe column
@@ -412,3 +432,27 @@ Got__count__results_for___query_85fb =
[one] A obtenu { $count } pour '{ $query }' [one] A obtenu { $count } pour '{ $query }'
*[other] 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
}

View File

@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Adicionar coluna de notificações exte
Add_Hashtag_Column_ebf4 = Adicionar coluna de marcadores Add_Hashtag_Column_ebf4 = Adicionar coluna de marcadores
# Column title for adding last notes column # Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar coluna de últimas notas 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 # Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay # Button label to add a relay
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar link
Copy_Note_ID_6b45 = Copiar ID da nota Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard # Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON da nota 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 the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard # Copy the text content of the note to clipboard
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar carteira
Display_name_f9d9 = Nome a mostrar Display_name_f9d9 = Nome a mostrar
# Domain identification message # Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será usado para identificação 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 # Column title for editing deck
Edit_Deck_4018 = Editar aba Edit_Deck_4018 = Editar aba
# Button label to edit a deck # Button label to edit a deck
@@ -191,6 +197,8 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K k_5K_f7e6 = 5K
# Description for your notes column # Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanha as tuas notas e respostas 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 # Label for language, Appearance settings section
Language_e264 = Idioma: Language_e264 = Idioma:
# Title for last note per user column # 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 Moves_this_column_to_another_position_0d4b = Mover esta coluna para outra posição
# Title for the user's deck # Title for the user's deck
My_Deck_4ac5 = Minha aba 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. # 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? New_to_Nostr_a2fd = Nov@ no Nostr?
# NIP-05 identity field label # 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. 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 URL field label
Profile_picture_81ff = Foto de perfil Profile_picture_81ff = Foto de perfil
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = ID da CONTA PÚBLICA
# Column title for quote composition # Column title for quote composition
Quote_475c = Citação Quote_475c = Citação
# Error message when quote note cannot be found # Error message when quote note cannot be found
@@ -311,6 +329,8 @@ Search_c573 = Procurar
Search_notes_42a6 = Procurar notas... Search_notes_42a6 = Procurar notas...
# Search in progress message # Search in progress message
Searching_for___query_5d18 = Procurando por '{ $query }' 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 # Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos
# Description for universe column # Description for universe column
@@ -412,3 +432,27 @@ Got__count__results_for___query_85fb =
[one] { $count } resultado obtido para '{ $query }' [one] { $count } resultado obtido para '{ $query }'
*[other] { $count } resultados obtidos 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
}

View File

@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = เพิ่มคอลัมน์ก
Add_Hashtag_Column_ebf4 = เพิ่มคอลัมน์แฮชแท็ก Add_Hashtag_Column_ebf4 = เพิ่มคอลัมน์แฮชแท็ก
# Column title for adding last notes column # Column title for adding last notes column
Add_Last_Notes_Column_bbad = เพิ่มคอลัมน์โน้ตล่าสุด Add_Last_Notes_Column_bbad = เพิ่มคอลัมน์โน้ตล่าสุด
# Tooltip text for adding a new deck button
Add_new_deck_f2fc = เพิ่ม deck ใหม่
# Column title for adding notifications column # Column title for adding notifications column
Add_Notifications_Column_79f8 = เพิ่มคอลัมน์การแจ้งเตือน Add_Notifications_Column_79f8 = เพิ่มคอลัมน์การแจ้งเตือน
# Button label to add a relay # Button label to add a relay
@@ -93,6 +95,8 @@ Copy_Link_dc7c = คัดลอกลิงก์
Copy_Note_ID_6b45 = คัดลอก โน้ต ID Copy_Note_ID_6b45 = คัดลอก โน้ต ID
# Copy the raw note data in JSON format to clipboard # Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = คัดลอก JSON ของโน้ต 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 the author's public key to clipboard
Copy_Pubkey_9cc4 = คัดลอก npub Copy_Pubkey_9cc4 = คัดลอก npub
# Copy the text content of the note to clipboard # Copy the text content of the note to clipboard
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = ลบวอลเล็ต
Display_name_f9d9 = ชื่อที่แสดง Display_name_f9d9 = ชื่อที่แสดง
# Domain identification message # Domain identification message
domain___will_be_used_for_identification_b67e = { $domain } จะใช้สำหรับการระบุตัวตน 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 # Column title for editing deck
Edit_Deck_4018 = แก้ไข Deck Edit_Deck_4018 = แก้ไข Deck
# Button label to edit a deck # Button label to edit a deck
@@ -193,6 +199,8 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K k_5K_f7e6 = 5K
# Description for your notes column # Description for your notes column
Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ Keep_track_of_your_notes___replies_a334 = ติดตามโน้ตและการตอบกลับของคุณ
# label for keys setting section
Keys_435f = คีย์
# Label for language, Appearance settings section # Label for language, Appearance settings section
Language_e264 = ภาษา: Language_e264 = ภาษา:
# Title for last note per user column # 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 = ย้ายคอลัมน์นี้ไปยังตำแหน่งอื่น Moves_this_column_to_another_position_0d4b = ย้ายคอลัมน์นี้ไปยังตำแหน่งอื่น
# Title for the user's deck # Title for the user's deck
My_Deck_4ac5 = 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. # Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = มือใหม่สำหรับ Nostr? New_to_Nostr_a2fd = มือใหม่สำหรับ Nostr?
# NIP-05 identity field label # 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 = กดปุ่มด้านล่างเพื่อคัดลอกข้อมูลบันทึกล่าสุด ไปยังคลิปบอร์ด จากนั้นนำไปวางในอีเมลของคุณ 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 URL field label
Profile_picture_81ff = รูปโปรไฟล์ Profile_picture_81ff = รูปโปรไฟล์
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = ไอดีบัญชีสาธารณะ
# Column title for quote composition # Column title for quote composition
Quote_475c = อ้างอิง Quote_475c = อ้างอิง
# Error message when quote note cannot be found # Error message when quote note cannot be found
@@ -313,6 +331,8 @@ Search_c573 = ค้นหา
Search_notes_42a6 = ค้นหาโน้ต... Search_notes_42a6 = ค้นหาโน้ต...
# Search in progress message # Search in progress message
Searching_for___query_5d18 = กำลังค้นหา '{ $query }' Searching_for___query_5d18 = กำลังค้นหา '{ $query }'
# label describing secret key
SECRET_ACCOUNT_LOGIN_KEY_8440 = คีย์ลับสำหรับล็อกอินบัญชี
# Description for Home column # Description for Home column
See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ See_notes_from_your_contacts_ac16 = ดูโน้ตจากผู้ติดต่อของคุณ
# Description for universe column # Description for universe column
@@ -414,3 +434,27 @@ Got__count__results_for___query_85fb =
[one] ผลการค้นหา '{ $query }': พบ { $count } รายการ [one] ผลการค้นหา '{ $query }': พบ { $count } รายการ
*[other] ผลการค้นหา '{ $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 } คน รีโพสต์โน้ตของคุณ
}

View File

@@ -152,7 +152,9 @@ impl<'a> RelayMessage<'a> {
return Ok(Self::ok(event_id, status, message)); 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]"#, r#"["NOTICE": 404]"#,
Err(Error::DecodeFailed("unrecognized message type".into())), Err(Error::DecodeFailed("unrecognized message type: '[\"NOTICE\": 404]'".into())),
), ),
( (
r#"["OK","event_id"]"#, r#"["OK","event_id"]"#,
Err(Error::DecodeFailed("unrecognized message type".into())), Err(Error::DecodeFailed("unrecognized message type: '[\"OK\",\"event_id\"]'".into())),
), ),
( (
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"]"#, r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"]"#,
Err(Error::DecodeFailed("unrecognized message type".into())), Err(Error::DecodeFailed("unrecognized message type: '[\"OK\",\"b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30\"]'".into())),
), ),
( (
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,""]"#, r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,""]"#,

View File

@@ -51,6 +51,7 @@ bitflags = { workspace = true }
regex = "1" regex = "1"
chrono = { workspace = true } chrono = { workspace = true }
indexmap = {workspace = true} indexmap = {workspace = true}
rand = {workspace = true}
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
[dev-dependencies] [dev-dependencies]

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
filter::{self, HybridFilter}, filter::{self, HybridFilter, ValidKind},
Error, Error,
}; };
use nostrdb::{Filter, Note}; use nostrdb::{Filter, Note};
@@ -15,10 +15,16 @@ pub fn hybrid_contacts_filter(
add_pk: Option<&[u8; 32]>, add_pk: Option<&[u8; 32]>,
with_hashtags: bool, with_hashtags: bool,
) -> Result<HybridFilter, Error> { ) -> Result<HybridFilter, Error> {
let local = filter::filter_from_tags(note, add_pk, with_hashtags)? let local = vec![
.into_filter([1], filter::default_limit()); 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)? 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)) Ok(HybridFilter::split(local, remote))
} }

View File

@@ -142,12 +142,6 @@ impl FilterState {
Self::Ready(HybridFilter::unsplit(filter)) 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) /// Our hybrid filter is ready (either split or unsplit)
pub fn ready_hybrid(filter: HybridFilter) -> Self { pub fn ready_hybrid(filter: HybridFilter) -> Self {
Self::Ready(filter) Self::Ready(filter)
@@ -219,7 +213,7 @@ pub struct FilteredTags {
/// The local and remote filter are related but slightly different /// The local and remote filter are related but slightly different
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SplitFilter { pub struct SplitFilter {
pub local: Vec<Filter>, pub local: Vec<NdbQueryPackage>,
pub remote: Vec<Filter>, pub remote: Vec<Filter>,
} }
@@ -236,16 +230,23 @@ impl HybridFilter {
HybridFilter::Unsplit(filter) 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 }) HybridFilter::Split(SplitFilter { local, remote })
} }
pub fn local(&self) -> &[Filter] { pub fn local(&self) -> NdbQueryPackages<'_> {
match self { 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 // 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 { impl FilteredTags {
pub fn into_follow_filter(self) -> Vec<Filter> { pub fn into_query_package(self, kind: ValidKind, limit: u64) -> NdbQueryPackage {
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,
{
let mut filters: Vec<Filter> = Vec::with_capacity(2); let mut filters: Vec<Filter> = Vec::with_capacity(2);
if let Some(authors) = self.authors { 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 { 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 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. /// Create a "last N notes per pubkey" query.
pub fn last_n_per_pubkey_from_tags( pub fn last_n_per_pubkey_from_tags(
note: &Note, note: &Note,
kind: u64, kind: u64,
notes_per_pubkey: u64, notes_per_pubkey: u64,
) -> Result<Vec<Filter>, Error> { ) -> Result<Vec<Filter>, Error> {
use rand::Rng;
let mut filters: Vec<Filter> = vec![]; let mut filters: Vec<Filter> = vec![];
let mut rng = rand::rng();
for tag in note.tags() { // TODO: fix arbitrary MAX_FILTER limit in nostrdb
// TODO: fix arbitrary MAX_FILTER limit in nostrdb const LIMIT: usize = 15;
if filters.len() == 15 {
break;
}
for (i, tag) in note.tags().iter().enumerate() {
if tag.count() < 2 { if tag.count() < 2 {
continue; continue;
} }
let t = if let Some(t) = tag.get_unchecked(0).variant().str() { let Some("p") = tag.get_str(0) else {
t
} else {
continue; continue;
}; };
if t == "p" { let Some(author) = tag.get_id(1) else {
let author = if let Some(author) = tag.get_unchecked(1).variant().id() { continue;
author };
} else {
continue;
};
let mk_filter = || {
let mut filter = Filter::new(); let mut filter = Filter::new();
filter.start_authors_field()?; let _ = filter.start_authors_field();
filter.add_id_element(author)?; let _ = filter.add_id_element(author);
filter.end_field(); filter.end_field();
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build()); 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;
};
let mut filter = Filter::new(); // since we're limited due to a nostrdb bug, we reservoir sample to keep things interesting
filter.start_tags_field('t')?; if filters.len() < LIMIT {
filter.add_str_element(hashtag)?; filters.push(mk_filter());
filter.end_field(); } else {
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build()); let j = rng.random_range(0..=i);
if j < LIMIT {
filters[j] = mk_filter();
}
} }
} }

View File

@@ -89,7 +89,7 @@ impl TexturesCache {
entry.replace_entry_with(|_, v| { entry.replace_entry_with(|_, v| {
let TextureStateInternal::Loading(textured) = v else { let TextureStateInternal::Loading(textured) = v else {
return None; return Some(v);
}; };
Some(TextureStateInternal::Loaded(textured)) Some(TextureStateInternal::Loaded(textured))

View File

@@ -305,7 +305,7 @@ fn generate_gif(
); );
if tex_input.send(texture_frame).is_err() { if tex_input.send(texture_frame).is_err() {
tracing::debug!("AnimationTextureFrame mpsc stopped abruptly"); //tracing::debug!("AnimationTextureFrame mpsc stopped abruptly");
break; break;
} }
} }

View File

@@ -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_MONTH_IN_SECONDS: u64 = 2_592_000; // 30 days
const ONE_YEAR_IN_SECONDS: u64 = 31_536_000; // 365 days const ONE_YEAR_IN_SECONDS: u64 = 31_536_000; // 365 days
// Range boundary constants for match patterns /// Calculate relative time between two timestamps, with two units only
const MAX_SECONDS: u64 = ONE_MINUTE_IN_SECONDS - 1; /// when the scale is large enough (e.g., "1y 6m", "5d 4h"),
const MAX_SECONDS_FOR_MINUTES: u64 = ONE_HOUR_IN_SECONDS - 1; /// but not for hours/minutes/seconds.
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
fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String { 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 { let duration = if now >= timestamp {
now.saturating_sub(timestamp) now.saturating_sub(timestamp)
} else { } else {
timestamp.saturating_sub(now) timestamp.saturating_sub(now)
}; };
let time_str = match duration { // Special-case: "now" for < 3 seconds
0..=2 => tr!( if duration <= 2 {
let s = tr!(
i18n, i18n,
"now", "now",
"Relative time for very recent events (less than 3 seconds)" "Relative time for very recent events (less than 3 seconds)"
), );
3..=MAX_SECONDS => tr!( return if timestamp > now { format!("+{s}") } else { s };
i18n, }
"{count}s",
"Relative time in seconds", // Break into buckets
count = duration let years = duration / ONE_YEAR_IN_SECONDS;
), let rem_y = duration % ONE_YEAR_IN_SECONDS;
ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!(
i18n, let months = rem_y / ONE_MONTH_IN_SECONDS;
"{count}m", let rem_m = rem_y % ONE_MONTH_IN_SECONDS;
"Relative time in minutes",
count = duration / ONE_MINUTE_IN_SECONDS let weeks = rem_m / ONE_WEEK_IN_SECONDS;
), let rem_w = rem_m % ONE_WEEK_IN_SECONDS;
ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!(
i18n, let days = rem_w / ONE_DAY_IN_SECONDS;
"{count}h", let rem_d = rem_w % ONE_DAY_IN_SECONDS;
"Relative time in hours",
count = duration / ONE_HOUR_IN_SECONDS let hours = rem_d / ONE_HOUR_IN_SECONDS;
), let rem_h = rem_d % ONE_HOUR_IN_SECONDS;
ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!(
i18n, let mins = rem_h / ONE_MINUTE_IN_SECONDS;
"{count}d", let secs = rem_h % ONE_MINUTE_IN_SECONDS;
"Relative time in days",
count = duration / ONE_DAY_IN_SECONDS let mut parts: Vec<String> = Vec::with_capacity(2);
),
ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!( let mut push_part = |count: u64, key: &str, desc: &str| {
i18n, if count > 0 && parts.len() < 2 {
"{count}w", parts.push(tr!(i18n, key, desc, count = count));
"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
),
}; };
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 { if timestamp > now {
format!("+{time_str}") format!("+{time_str}")
} else { } else {

View File

@@ -311,7 +311,7 @@ impl TimelineSub {
let before = self.state.clone(); let before = self.state.clone();
match &mut self.state { match &mut self.state {
SubState::NoSub { dependers } => { SubState::NoSub { dependers } => {
let Some(sub) = ndb_sub(ndb, filter.local(), "") else { let Some(sub) = ndb_sub(ndb, &filter.local().combined(), "") else {
return; return;
}; };
@@ -326,7 +326,7 @@ impl TimelineSub {
dependers: _, dependers: _,
} => {} } => {}
SubState::RemoteOnly { remote, 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; return;
}; };
self.state = SubState::Unified { self.state = SubState::Unified {

View File

@@ -56,11 +56,12 @@ impl NewPost {
} }
} }
pub fn to_note(&self, seckey: &[u8; 32]) -> Note<'_> { /// creates a NoteBuilder with all the shared data between note, reply & quote reply
let mut content = self.content.clone(); fn builder_with_shared_tags<'a>(&self, mut content: String) -> NoteBuilder<'a> {
append_urls(&mut content, &self.media); 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) { for hashtag in Self::extract_hashtags(&self.content) {
builder = builder.start_tag().tag_str("t").tag_str(&hashtag); builder = builder.start_tag().tag_str("t").tag_str(&hashtag);
@@ -74,18 +75,21 @@ impl NewPost {
builder = add_mention_tags(builder, &self.mentions); 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") builder.sign(seckey).build().expect("note should be ok")
} }
pub fn to_reply(&self, seckey: &[u8; 32], replying_to: &Note) -> Note<'_> { pub fn to_reply(&self, seckey: &[u8; 32], replying_to: &Note) -> Note<'_> {
let mut content = self.content.clone(); let mut builder = self.builder_with_shared_tags(self.content.clone());
append_urls(&mut content, &self.media);
let builder = add_client_tag(NoteBuilder::new()).kind(1).content(&content);
let nip10 = NoteReply::new(replying_to.tags()); let nip10 = NoteReply::new(replying_to.tags());
let mut builder = if let Some(root) = nip10.root() { builder = if let Some(root) = nip10.root() {
builder builder
.start_tag() .start_tag()
.tag_str("e") .tag_str("e")
@@ -143,14 +147,6 @@ impl NewPost {
builder = builder.start_tag().tag_str("p").tag_str(&hex::encode(id)); 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 builder
.sign(seckey) .sign(seckey)
.build() .build()
@@ -158,27 +154,13 @@ impl NewPost {
} }
pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note<'_> { pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note<'_> {
let mut new_content = format!( let new_content = format!(
"{}\nnostr:{}", "{}\nnostr:{}",
self.content, self.content,
enostr::NoteId::new(*quoting.id()).to_bech().unwrap() enostr::NoteId::new(*quoting.id()).to_bech().unwrap()
); );
append_urls(&mut new_content, &self.media); let builder = self.builder_with_shared_tags(new_content);
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);
}
builder builder
.start_tag() .start_tag()

View File

@@ -134,15 +134,22 @@ impl TimelineCache {
} }
let notes = if let FilterState::Ready(filters) = id.filters(txn, ndb) { let notes = if let FilterState::Ready(filters) = id.filters(txn, ndb) {
if let Ok(results) = ndb.query(txn, filters.local(), 1000) { let mut notes = Vec::new();
results
.into_iter() for package in filters.local().packages {
.map(NoteRef::from_query_result) if let Ok(results) = ndb.query(txn, package.filters, 1000) {
.collect() let cur_notes: Vec<NoteRef> = results
} else { .into_iter()
debug!("got no results from TimelineCache lookup for {:?}", id); .map(NoteRef::from_query_result)
vec![] .collect();
notes.extend(cur_notes);
} else {
debug!("got no results from TimelineCache lookup for {:?}", id);
}
} }
notes
} else { } else {
// filter is not ready yet // filter is not ready yet
vec![] vec![]
@@ -178,12 +185,20 @@ impl TimelineCache {
let (mut open_result, timeline) = match notes_resp.vitality { let (mut open_result, timeline) = match notes_resp.vitality {
Vitality::Stale(timeline) => { Vitality::Stale(timeline) => {
// The timeline cache is stale, let's update it // The timeline cache is stale, let's update it
let notes = find_new_notes( let notes = {
timeline.all_or_any_entries().latest(), let mut notes = Vec::new();
timeline.subscription.get_filter()?.local(), for package in timeline.subscription.get_filter()?.local().packages {
txn, let cur_notes = find_new_notes(
ndb, timeline.all_or_any_entries().latest(),
); package.filters,
txn,
ndb,
);
notes.extend(cur_notes);
}
notes
};
let open_result = if notes.is_empty() { let open_result = if notes.is_empty() {
None None
} else { } else {

View File

@@ -3,6 +3,7 @@ use crate::search::SearchQuery;
use crate::timeline::{Timeline, TimelineTab}; use crate::timeline::{Timeline, TimelineTab};
use enostr::{Filter, NoteId, Pubkey}; use enostr::{Filter, NoteId, Pubkey};
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::filter::{NdbQueryPackage, ValidKind};
use notedeck::{ use notedeck::{
contacts::{contacts_filter, hybrid_contacts_filter}, contacts::{contacts_filter, hybrid_contacts_filter},
filter::{self, default_limit, default_remote_limit, HybridFilter}, 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 { fn profile_filter(pk: &[u8; 32]) -> HybridFilter {
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( HybridFilter::split(
local,
vec![Filter::new() vec![Filter::new()
.authors([pk]) .authors([pk])
.kinds([1]) .kinds([1, 6, 0])
.limit(default_limit())
.build()],
vec![Filter::new()
.authors([pk])
.kinds([1, 0])
.limit(default_remote_limit()) .limit(default_remote_limit())
.build()], .build()],
) )

View File

@@ -63,7 +63,7 @@ impl ViewFilter {
} }
pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool { 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 { fn identity(_cache: &CachedNote, _note: &Note) -> bool {
@@ -133,6 +133,7 @@ impl TimelineTab {
ndb: &Ndb, ndb: &Ndb,
txn: &Transaction, txn: &Transaction,
reversed: bool, reversed: bool,
use_front_insert: bool,
) -> Option<UnknownPks<'a>> { ) -> Option<UnknownPks<'a>> {
if payloads.is_empty() { if payloads.is_empty() {
return None; return None;
@@ -158,7 +159,11 @@ impl TimelineTab {
debug!("spliced when inserting {num_refs} new notes, resetting virtual list",); debug!("spliced when inserting {num_refs} new notes, resetting virtual list",);
list.reset(); list.reset();
} }
MergeKind::FrontInsert => { MergeKind::FrontInsert => 's: {
if !use_front_insert {
break 's;
}
// only run this logic if we're reverse-chronological // only run this logic if we're reverse-chronological
// reversed in this case means chronological, since the // reversed in this case means chronological, since the
// default is reverse-chronological. yeah it's confusing. // default is reverse-chronological. yeah it's confusing.
@@ -210,6 +215,7 @@ pub struct Timeline {
pub selected_view: usize, pub selected_view: usize,
pub subscription: TimelineSub, pub subscription: TimelineSub,
pub enable_front_insert: bool,
} }
impl Timeline { impl Timeline {
@@ -271,12 +277,16 @@ impl Timeline {
let subscription = TimelineSub::default(); let subscription = TimelineSub::default();
let selected_view = 0; 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 { Timeline {
kind, kind,
filter, filter,
views, views,
subscription, subscription,
selected_view, selected_view,
enable_front_insert,
} }
} }
@@ -402,7 +412,9 @@ impl Timeline {
match view.filter { match view.filter {
ViewFilter::NotesAndReplies => { ViewFilter::NotesAndReplies => {
let res: Vec<&NotePayload<'_>> = payloads.iter().collect(); 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); 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); res.process(unknown_ids, ndb, txn);
} }
} }
@@ -676,18 +694,32 @@ fn setup_initial_timeline(
timeline.subscription, timeline.filter timeline.subscription, timeline.filter
); );
let mut lim = 0i32; let notes = {
for filter in filters.local() { let mut notes = Vec::new();
lim += filter.limit().unwrap_or(1) as i32;
}
debug!("setup_initial_timeline: limit for local filter is {}", lim); for package in filters.local().packages {
let mut lim = 0i32;
for filter in package.filters {
lim += filter.limit().unwrap_or(1) as i32;
}
let notes: Vec<NoteRef> = ndb debug!("setup_initial_timeline: limit for local filter is {}", lim);
.query(txn, filters.local(), lim)?
.into_iter() let cur_notes: Vec<NoteRef> = ndb
.map(NoteRef::from_query_result) .query(txn, package.filters, lim)?
.collect(); .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, &notes) { if let Some(pks) = timeline.insert_new(txn, ndb, note_cache, &notes) {
pks.process(ndb, txn, unknown_ids); pks.process(ndb, txn, unknown_ids);

View File

@@ -101,28 +101,37 @@ impl NoteUnits {
let inserted_new = new_order.len(); let inserted_new = new_order.len();
let front_insertion = inserted_new > 0 let front_insertion = if self.order.is_empty() || new_order.is_empty() {
&& if self.order.is_empty() || new_order.is_empty() { !new_order.is_empty()
true } else if self.reversed {
} 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(); let first_new = *new_order.first().unwrap(); // most recent unit of the new order
let last_old = *self.order.last().unwrap(); let last_old = *self.order.last().unwrap(); // least recent unit of the current order
self.storage[first_new] >= self.storage[last_old]
} else { // if the most recent unit of the new order is less recent than the least recent unit of the current order,
let last_new = *new_order.last().unwrap(); // all current order units are less recent than the new order units.
let first_old = *self.order.first().unwrap(); // In other words, they are all being inserted in the front
self.storage[last_new] <= self.storage[first_old] self.storage[first_new] >= self.storage[last_old]
}; } else {
// 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]
};
let mut merged = Vec::with_capacity(self.order.len() + new_order.len()); let mut merged = Vec::with_capacity(self.order.len() + new_order.len());
let (mut i, mut j) = (0, 0); let (mut i, mut j) = (0, 0);
while i < self.order.len() && j < new_order.len() { while i < self.order.len() && j < new_order.len() {
let index_left = self.order[i]; let index_left = self.order[i];
let index_right = new_order[j]; let index_right = new_order[j];
let left_item = &self.storage[index_left]; let left_unit = &self.storage[index_left];
let right_item = &self.storage[index_right]; let right_unit = &self.storage[index_right];
if left_item <= right_item { if left_unit <= right_unit {
// left_item is newer than right_item // the left unit is more recent than (or the same recency as) the right unit
merged.push(index_left); merged.push(index_left);
i += 1; i += 1;
} else { } else {

View File

@@ -31,7 +31,6 @@ pub fn render_timeline_route(
| TimelineKind::Generic(_) => { | TimelineKind::Generic(_) => {
let note_action = let note_action =
ui::TimelineView::new(kind, timeline_cache, note_context, note_options, jobs, col) ui::TimelineView::new(kind, timeline_cache, note_context, note_options, jobs, col)
.scroll_to_top(scroll_to_top)
.ui(ui); .ui(ui);
note_action.map(RenderNavAction::NoteAction) note_action.map(RenderNavAction::NoteAction)

View File

@@ -209,7 +209,7 @@ fn to_repost<'a>(
let reposted_note = match get_reposted_note(ndb, txn, &payload.note) { let reposted_note = match get_reposted_note(ndb, txn, &payload.note) {
Some(r) => r, Some(r) => r,
None => { None => {
tracing::error!( tracing::debug!(
"Could not get reposted note for note id {}", "Could not get reposted note for note id {}",
enostr::NoteId::new(*payload.note.id()).hex() enostr::NoteId::new(*payload.note.id()).hex()
); );

View File

@@ -342,6 +342,13 @@ impl<'a, 'd> PostView<'a, 'd> {
} }
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse { 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() { while let Some(selected_file) = get_next_selected_file() {
match selected_file { match selected_file {
Ok(selected_media) => { 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 focused = self.focused(ui);
let stroke = if focused { let stroke = if focused {
ui.visuals().selection.stroke ui.visuals().selection.stroke

View File

@@ -39,6 +39,11 @@ pub enum ProfileViewAction {
Follow(Pubkey), Follow(Pubkey),
} }
struct ProfileScrollResponse {
body_end_pos: f32,
action: Option<ProfileViewAction>,
}
impl<'a, 'd> ProfileView<'a, 'd> { impl<'a, 'd> ProfileView<'a, 'd> {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
@@ -65,15 +70,13 @@ impl<'a, 'd> ProfileView<'a, 'd> {
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<ProfileViewAction> { pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<ProfileViewAction> {
let scroll_id = ProfileView::scroll_id(self.col_id, self.pubkey); 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)) { let output = scroll_area.show(ui, |ui| {
scroll_area = scroll_area.vertical_scroll_offset(offset);
}
let output = scroll_area.show(ui, |ui| 's: {
let mut action = None; let mut action = None;
let txn = Transaction::new(self.note_context.ndb).expect("txn"); let txn = Transaction::new(self.note_context.ndb).expect("txn");
let profile = self let profile = self
@@ -82,23 +85,19 @@ impl<'a, 'd> ProfileView<'a, 'd> {
.get_profile_by_pubkey(&txn, self.pubkey.bytes()) .get_profile_by_pubkey(&txn, self.pubkey.bytes())
.ok(); .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); action = Some(profile_view_action);
} }
let Some(profile_timeline) = self let tabs_resp = tabs_ui(
.timeline_cache
.get_mut(&TimelineKind::Profile(*self.pubkey))
else {
break 's action;
};
profile_timeline.selected_view = tabs_ui(
ui, ui,
self.note_context.i18n, self.note_context.i18n,
profile_timeline.selected_view, profile_timeline.selected_view,
&profile_timeline.views, &profile_timeline.views,
); );
profile_timeline.selected_view = tabs_resp.inner;
let reversed = false; let reversed = false;
// poll for new notes and insert them into our existing notes // poll for new notes and insert them into our existing notes
@@ -124,145 +123,147 @@ impl<'a, 'd> ProfileView<'a, 'd> {
action = Some(ProfileViewAction::Note(note_action)); 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( fn profile_body(
&mut self, ui: &mut egui::Ui,
ui: &mut egui::Ui, pubkey: &Pubkey,
profile: Option<&ProfileRecord<'_>>, note_context: &mut NoteContext,
) -> Option<ProfileViewAction> { profile: Option<&ProfileRecord<'_>>,
let mut action = None; ) -> Option<ProfileViewAction> {
ui.vertical(|ui| { let mut action = None;
banner( ui.vertical(|ui| {
ui, banner(
profile ui,
.map(|p| p.record().profile()) profile
.and_then(|p| p.and_then(|p| p.banner())), .map(|p| p.record().profile())
120.0, .and_then(|p| p.and_then(|p| p.banner())),
); 120.0,
);
let padding = 12.0; let padding = 12.0;
notedeck_ui::padding(padding, ui, |ui| { notedeck_ui::padding(padding, ui, |ui| {
let mut pfp_rect = ui.available_rect_before_wrap(); let mut pfp_rect = ui.available_rect_before_wrap();
let size = 80.0; let size = 80.0;
pfp_rect.set_width(size); pfp_rect.set_width(size);
pfp_rect.set_height(size); pfp_rect.set_height(size);
let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0)))); let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0))));
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.put( ui.put(
pfp_rect, 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) .size(size)
.border(ProfilePic::border_stroke(ui)), .border(ProfilePic::border_stroke(ui)),
); );
if ui if ui
.add(copy_key_widget(&pfp_rect, self.note_context.i18n)) .add(copy_key_widget(&pfp_rect, note_context.i18n))
.clicked() .clicked()
{ {
let to_copy = if let Some(bech) = self.pubkey.npub() { let to_copy = if let Some(bech) = pubkey.npub() {
bech bech
} else { } else {
error!("Could not convert Pubkey to bech"); error!("Could not convert Pubkey to bech");
String::new() String::new()
}; };
ui.ctx().copy_text(to_copy) ui.ctx().copy_text(to_copy)
} }
ui.with_layout(Layout::right_to_left(egui::Align::RIGHT), |ui| { ui.with_layout(Layout::right_to_left(egui::Align::RIGHT), |ui| {
ui.add_space(24.0); ui.add_space(24.0);
let target_key = self.pubkey; let target_key = pubkey;
let selected = self.note_context.accounts.get_selected_account(); let selected = note_context.accounts.get_selected_account();
let profile_type = if selected.key.secret_key.is_none() { let profile_type = if selected.key.secret_key.is_none() {
ProfileType::ReadOnly ProfileType::ReadOnly
} else if &selected.key.pubkey == self.pubkey { } else if &selected.key.pubkey == pubkey {
ProfileType::MyProfile ProfileType::MyProfile
} else { } else {
ProfileType::Followable(selected.is_following(target_key.bytes())) ProfileType::Followable(selected.is_following(target_key.bytes()))
}; };
match profile_type { match profile_type {
ProfileType::MyProfile => { ProfileType::MyProfile => {
if ui if ui.add(edit_profile_button(note_context.i18n)).clicked() {
.add(edit_profile_button(self.note_context.i18n)) action = Some(ProfileViewAction::EditProfile);
.clicked()
{
action = Some(ProfileViewAction::EditProfile);
}
} }
ProfileType::Followable(is_following) => { }
let follow_button = ui.add(follow_button(is_following)); ProfileType::Followable(is_following) => {
let follow_button = ui.add(follow_button(is_following));
if follow_button.clicked() { if follow_button.clicked() {
action = match is_following { action = match is_following {
IsFollowing::Unknown => { IsFollowing::Unknown => {
// don't do anything, we don't have contact list // don't do anything, we don't have contact list
None None
} }
IsFollowing::Yes => { IsFollowing::Yes => {
Some(ProfileViewAction::Unfollow(target_key.to_owned())) Some(ProfileViewAction::Unfollow(target_key.to_owned()))
} }
IsFollowing::No => { IsFollowing::No => {
Some(ProfileViewAction::Follow(target_key.to_owned())) Some(ProfileViewAction::Follow(target_key.to_owned()))
} }
}; };
}
} }
ProfileType::ReadOnly => {}
} }
}); ProfileType::ReadOnly => {}
});
ui.add_space(18.0);
ui.add(display_name_widget(&get_display_name(profile), false));
ui.add_space(8.0);
ui.add(about_section_widget(profile));
ui.horizontal_wrapped(|ui| {
let website_url = profile
.as_ref()
.map(|p| p.record().profile())
.and_then(|p| p.and_then(|p| p.website()).filter(|s| !s.is_empty()));
let lud16 = profile
.as_ref()
.map(|p| p.record().profile())
.and_then(|p| p.and_then(|p| p.lud16()).filter(|s| !s.is_empty()));
if let Some(website_url) = website_url {
ui.horizontal(|ui| {
handle_link(ui, website_url);
});
}
if let Some(lud16) = lud16 {
if website_url.is_some() {
ui.end_row();
}
ui.horizontal(|ui| {
handle_lud16(ui, lud16);
});
} }
}); });
}); });
});
action ui.add_space(18.0);
}
ui.add(display_name_widget(&get_display_name(profile), false));
ui.add_space(8.0);
ui.add(about_section_widget(profile));
ui.horizontal_wrapped(|ui| {
let website_url = profile
.as_ref()
.map(|p| p.record().profile())
.and_then(|p| p.and_then(|p| p.website()).filter(|s| !s.is_empty()));
let lud16 = profile
.as_ref()
.map(|p| p.record().profile())
.and_then(|p| p.and_then(|p| p.lud16()).filter(|s| !s.is_empty()));
if let Some(website_url) = website_url {
ui.horizontal(|ui| {
handle_link(ui, website_url);
});
}
if let Some(lud16) = lud16 {
if website_url.is_some() {
ui.end_row();
}
ui.horizontal(|ui| {
handle_lud16(ui, lud16);
});
}
});
});
});
action
} }
enum ProfileType { enum ProfileType {

View File

@@ -1,13 +1,14 @@
use egui::containers::scroll_area::ScrollBarVisibility; 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 egui_tabs::TabColor;
use enostr::Pubkey; 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::name::get_display_name;
use notedeck::ui::is_narrow; 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::app_images::{like_image, repost_image};
use notedeck_ui::ProfilePic; use notedeck_ui::{ProfilePic, ProfilePreview};
use std::f32::consts::PI; use std::f32::consts::PI;
use tracing::{error, warn}; use tracing::{error, warn};
@@ -88,7 +89,7 @@ fn timeline_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
timeline_id: &TimelineKind, timeline_id: &TimelineKind,
timeline_cache: &mut TimelineCache, timeline_cache: &mut TimelineCache,
note_options: NoteOptions, mut note_options: NoteOptions,
note_context: &mut NoteContext, note_context: &mut NoteContext,
jobs: &mut JobsCache, jobs: &mut JobsCache,
col: usize, col: usize,
@@ -118,7 +119,8 @@ fn timeline_ui(
note_context.i18n, note_context.i18n,
timeline.selected_view, timeline.selected_view,
&timeline.views, &timeline.views,
); )
.inner;
// need this for some reason?? // need this for some reason??
ui.add_space(3.0); ui.add_space(3.0);
@@ -151,12 +153,6 @@ fn timeline_ui(
.auto_shrink([false, false]) .auto_shrink([false, false])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible); .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()) { if goto_top_resp.is_some_and(|r| r.clicked()) {
scroll_area = scroll_area.vertical_scroll_offset(0.0); 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"); 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( TimelineTabView::new(
timeline.current_view(), timeline.current_view(),
note_options, note_options,
@@ -191,8 +191,6 @@ fn timeline_ui(
.show(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 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)); 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, i18n: &mut Localization,
selected: usize, selected: usize,
views: &[TimelineTab], views: &[TimelineTab],
) -> usize { ) -> egui::InnerResponse<usize> {
ui.spacing_mut().item_spacing.y = 0.0; ui.spacing_mut().item_spacing.y = 0.0;
let tab_res = egui_tabs::Tabs::new(views.len() as i32) 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 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 underline_width = underline.span();
let tab_anim_id = ui.id().with("tab_anim"); let tab_anim_id = ui.id().with("tab_anim");
@@ -355,7 +355,7 @@ pub fn tabs_ui(
ui.painter().hline(underline, underline_y, stroke); 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 { fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 {
@@ -440,15 +440,46 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
entry: &NoteUnit, entry: &NoteUnit,
mute: &std::sync::Arc<Muted>, mute: &std::sync::Arc<Muted>,
) -> RenderEntryResponse { ) -> 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 { match entry {
NoteUnit::Single(note_ref) => render_note( NoteUnit::Single(_) => render_note(
ui, ui,
self.note_context, self.note_context,
self.note_options, self.note_options,
self.jobs, self.jobs,
mute, &underlying_note,
self.txn,
note_ref,
), ),
NoteUnit::Composite(composite) => match composite { NoteUnit::Composite(composite) => match composite {
CompositeUnit::Reaction(reaction_unit) => render_reaction_cluster( CompositeUnit::Reaction(reaction_unit) => render_reaction_cluster(
@@ -458,6 +489,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
self.jobs, self.jobs,
mute, mute,
self.txn, self.txn,
&underlying_note,
reaction_unit, reaction_unit,
), ),
CompositeUnit::Repost(repost_unit) => render_repost_cluster( CompositeUnit::Repost(repost_unit) => render_repost_cluster(
@@ -467,6 +499,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
self.jobs, self.jobs,
mute, mute,
self.txn, self.txn,
&underlying_note,
repost_unit, repost_unit,
), ),
}, },
@@ -483,7 +516,9 @@ impl CompositeType {
fn image(&self, darkmode: bool) -> egui::Image<'static> { fn image(&self, darkmode: bool) -> egui::Image<'static> {
match self { match self {
CompositeType::Reaction => like_image(), 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, first_name: &str,
total_count: usize, total_count: usize,
referenced_type: ReferencedNoteType, referenced_type: ReferencedNoteType,
notification: bool,
) -> String { ) -> String {
let count = total_count - 1; let count = total_count - 1;
@@ -500,7 +536,16 @@ impl CompositeType {
CompositeType::Reaction => { CompositeType::Reaction => {
reaction_description(loc, first_name, count, referenced_type) 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,46 +598,72 @@ fn reaction_description(
} }
} }
enum DescriptionType {
Notification(ReferencedNoteType),
Other,
}
fn repost_description( fn repost_description(
loc: &mut Localization, loc: &mut Localization,
first_name: &str, first_name: &str,
count: usize, count: usize,
referenced_type: ReferencedNoteType, description_type: DescriptionType,
) -> String { ) -> String {
match referenced_type { match description_type {
ReferencedNoteType::Tagged => { DescriptionType::Notification(referenced_type) => match referenced_type {
if count == 0 { ReferencedNoteType::Tagged => {
tr!( if count == 0 {
loc, tr!(
"{name} reposted a note you were tagged in", loc,
"repost from user", "{name} reposted a note you were tagged in",
name = first_name "repost from user",
) name = first_name
} else { )
tr_plural!( } else {
loc, tr_plural!(
"{name} and {count} other reposted a note you were tagged in", loc,
"{name} and {count} others reposted a note you were tagged in", "{name} and {count} other reposted a note you were tagged in",
"describing the amount of reposts a note you were tagged in received", "{name} and {count} others reposted a note you were tagged in",
count, "describing the amount of reposts a note you were tagged in received",
name = first_name count,
) name = first_name
)
}
} }
} ReferencedNoteType::Yours => {
ReferencedNoteType::Yours => { if count == 0 {
tr!(
loc,
"{name} reposted your note",
"repost from user",
name = first_name
)
} else {
tr_plural!(
loc,
"{name} and {count} other reposted your note",
"{name} and {count} others reposted your note",
"describing the amount of reposts your note received",
count,
name = first_name
)
}
}
},
DescriptionType::Other => {
if count == 0 { if count == 0 {
tr!( tr!(
loc, loc,
"{name} reposted your note", "{name} reposted",
"repost from user", "repost from user",
name = first_name name = first_name
) )
} else { } else {
tr_plural!( tr_plural!(
loc, loc,
"{name} and {count} other reposted your note", "{name} and {count} other reposted",
"{name} and {count} others reposted your note", "{name} and {count} others reposted",
"describing the amount of reposts your note received", "describing the amount of reposts a note has",
count, count,
name = first_name name = first_name
) )
@@ -606,32 +677,11 @@ fn render_note(
note_context: &mut NoteContext, note_context: &mut NoteContext,
note_options: NoteOptions, note_options: NoteOptions,
jobs: &mut JobsCache, jobs: &mut JobsCache,
mute: &std::sync::Arc<Muted>, note: &Note,
txn: &Transaction,
note_ref: &NoteRef,
) -> RenderEntryResponse { ) -> 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(&note, root_id.bytes())
} else {
false
};
if muted {
return RenderEntryResponse::Success(None);
}
let mut action = None; let mut action = None;
notedeck_ui::padding(8.0, ui, |ui| { notedeck_ui::padding(8.0, ui, |ui| {
let resp = NoteView::new(note_context, &note, note_options, jobs).show(ui); let resp = NoteView::new(note_context, note, note_options, jobs).show(ui);
if let Some(note_action) = resp.action { if let Some(note_action) = resp.action {
action = Some(note_action); action = Some(note_action);
@@ -643,6 +693,7 @@ fn render_note(
RenderEntryResponse::Success(action) RenderEntryResponse::Success(action)
} }
#[allow(clippy::too_many_arguments)]
fn render_reaction_cluster( fn render_reaction_cluster(
ui: &mut egui::Ui, ui: &mut egui::Ui,
note_context: &mut NoteContext, note_context: &mut NoteContext,
@@ -650,16 +701,9 @@ fn render_reaction_cluster(
jobs: &mut JobsCache, jobs: &mut JobsCache,
mute: &std::sync::Arc<Muted>, mute: &std::sync::Arc<Muted>,
txn: &Transaction, txn: &Transaction,
underlying_note: &Note,
reaction: &ReactionUnit, reaction: &ReactionUnit,
) -> RenderEntryResponse { ) -> 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 let profiles_to_show: Vec<ProfileEntry> = reaction
.reactions .reactions
.values() .values()
@@ -674,20 +718,21 @@ fn render_reaction_cluster(
render_composite_entry( render_composite_entry(
ui, ui,
note_context, note_context,
note_options, note_options | NoteOptions::Notification,
jobs, jobs,
reacted_to_note, underlying_note,
profiles_to_show, profiles_to_show,
CompositeType::Reaction, CompositeType::Reaction,
) )
} }
#[allow(clippy::too_many_arguments)]
fn render_composite_entry( fn render_composite_entry(
ui: &mut egui::Ui, ui: &mut egui::Ui,
note_context: &mut NoteContext, note_context: &mut NoteContext,
note_options: NoteOptions, mut note_options: NoteOptions,
jobs: &mut JobsCache, jobs: &mut JobsCache,
underlying_note: nostrdb::Note<'_>, underlying_note: &nostrdb::Note<'_>,
profiles_to_show: Vec<ProfileEntry>, profiles_to_show: Vec<ProfileEntry>,
composite_type: CompositeType, composite_type: CompositeType,
) -> RenderEntryResponse { ) -> RenderEntryResponse {
@@ -697,92 +742,203 @@ fn render_composite_entry(
let num_profiles = profiles_to_show.len(); let num_profiles = profiles_to_show.len();
let mut action = None; let mut action = None;
let referenced_type = if note_context
.accounts
.get_selected_account()
.key
.pubkey
.bytes()
!= underlying_note.pubkey()
{
ReferencedNoteType::Tagged
} else {
ReferencedNoteType::Yours
};
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;
}
}
}
egui::Frame::new() egui::Frame::new()
.inner_margin(Margin::symmetric(8, 4)) .inner_margin(Margin::symmetric(8, 4))
.show(ui, |ui| { .show(ui, |ui| {
ui.allocate_ui_with_layout( let show_label_newline = ui
vec2(ui.available_width(), 32.0), .horizontal_wrapped(|ui| {
Layout::left_to_right(egui::Align::Center), let pfps_resp = ui
|ui| { .allocate_ui_with_layout(
ui.vertical(|ui| { vec2(ui.available_width(), 32.0),
ui.add_space(4.0); Layout::left_to_right(egui::Align::Center),
ui.add_sized( |ui| {
vec2(28.0, 28.0), render_profiles(
composite_type.image(ui.visuals().dark_mode), ui,
); profiles_to_show,
}); &composite_type,
note_context.img_cache,
note_options.contains(NoteOptions::Notification),
)
},
)
.inner;
ui.add_space(16.0); if let Some(cur_action) = pfps_resp.action {
action = Some(cur_action);
}
ui.horizontal(|ui| { let description = composite_type.description(
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
.get_selected_account()
.key
.pubkey
.bytes()
!= underlying_note.pubkey()
{
ReferencedNoteType::Tagged
} else {
ReferencedNoteType::Yours
};
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.add_space(52.0);
ui.horizontal_wrapped(|ui| {
ui.label(composite_type.description(
note_context.i18n, note_context.i18n,
&first_name, &first_name,
num_profiles, num_profiles,
referenced_type, 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.add_space(16.0);
ui.horizontal(|ui| { let resp = ui
ui.add_space(48.0); .horizontal(|ui| {
let options = note_options if note_options.contains(NoteOptions::Notification) {
.difference(NoteOptions::ActionBar | NoteOptions::OptionsButton) note_options = note_options
.union(NoteOptions::NotificationPreview); .difference(NoteOptions::ActionBar | NoteOptions::OptionsButton)
let resp = NoteView::new(note_context, &underlying_note, options, jobs).show(ui); .union(NoteOptions::NotificationPreview);
if let Some(note_action) = resp.action { ui.add_space(48.0);
action = Some(note_action); };
} NoteView::new(note_context, underlying_note, note_options, jobs).show(ui)
}); })
.inner;
if let Some(note_action) = resp.action {
action.get_or_insert(note_action);
}
}); });
notedeck_ui::hline(ui); notedeck_ui::hline(ui);
RenderEntryResponse::Success(action) 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( fn render_repost_cluster(
ui: &mut egui::Ui, ui: &mut egui::Ui,
note_context: &mut NoteContext, note_context: &mut NoteContext,
@@ -790,16 +946,9 @@ fn render_repost_cluster(
jobs: &mut JobsCache, jobs: &mut JobsCache,
mute: &std::sync::Arc<Muted>, mute: &std::sync::Arc<Muted>,
txn: &Transaction, txn: &Transaction,
underlying_note: &Note,
repost: &RepostUnit, repost: &RepostUnit,
) -> RenderEntryResponse { ) -> 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 let profiles_to_show: Vec<ProfileEntry> = repost
.reposts .reposts
.values() .values()
@@ -815,7 +964,7 @@ fn render_repost_cluster(
note_context, note_context,
note_options, note_options,
jobs, jobs,
reposted_note, underlying_note,
profiles_to_show, profiles_to_show,
CompositeType::Repost, CompositeType::Repost,
) )

View File

@@ -6,9 +6,7 @@ use crate::{
use egui::{Color32, Hyperlink, Label, RichText}; use egui::{Color32, Hyperlink, Label, RichText};
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
use notedeck::Localization; use notedeck::Localization;
use notedeck::{ use notedeck::{time_format, update_imeta_blurhashes, NoteCache, NoteContext, NotedeckTextStyle};
time_format, update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext, NotedeckTextStyle,
};
use notedeck::{JobsCache, RenderableMedia}; use notedeck::{JobsCache, RenderableMedia};
use tracing::warn; use tracing::warn;
@@ -374,21 +372,6 @@ fn render_undecorated_note_contents<'a>(
ui.add_space(2.0); ui.add_space(2.0);
let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note"))); 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( media_action = image_carousel(
ui, ui,
note_context.img_cache, note_context.img_cache,
@@ -396,7 +379,6 @@ fn render_undecorated_note_contents<'a>(
jobs, jobs,
&supported_medias, &supported_medias,
carousel_id, carousel_id,
trusted_media,
note_context.i18n, note_context.i18n,
options, options,
); );

View File

@@ -33,7 +33,6 @@ pub fn image_carousel(
jobs: &mut JobsCache, jobs: &mut JobsCache,
medias: &[RenderableMedia], medias: &[RenderableMedia],
carousel_id: egui::Id, carousel_id: egui::Id,
trusted_media: bool,
i18n: &mut Localization, i18n: &mut Localization,
note_options: NoteOptions, note_options: NoteOptions,
) -> Option<MediaAction> { ) -> Option<MediaAction> {
@@ -68,7 +67,7 @@ pub fn image_carousel(
job_pool, job_pool,
jobs, jobs,
media, media,
trusted_media, note_options.contains(NoteOptions::TrustMedia),
i18n, i18n,
size, size,
if note_options.contains(NoteOptions::NoAnimations) { if note_options.contains(NoteOptions::NoAnimations) {

View File

@@ -5,10 +5,7 @@ pub mod options;
pub mod reply_description; pub mod reply_description;
use crate::{app_images, secondary_label}; use crate::{app_images, secondary_label};
use crate::{ use crate::{widgets::x_button, ProfilePic, ProfilePreview, PulseAlpha, Username};
profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview,
PulseAlpha, Username,
};
pub use contents::{render_note_preview, NoteContents}; pub use contents::{render_note_preview, NoteContents};
pub use context::NoteContextButton; pub use context::NoteContextButton;
@@ -25,14 +22,12 @@ pub use options::NoteOptions;
pub use reply_description::reply_desc; pub use reply_description::reply_desc;
use egui::emath::{pos2, Vec2}; 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 enostr::{KeypairUnowned, NoteId, Pubkey};
use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction}; use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
use notedeck::{ use notedeck::{
name::get_display_name,
note::{NoteAction, NoteContext, ZapAction}, note::{NoteAction, NoteContext, ZapAction},
tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, NotedeckTextStyle, tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, ZapTarget, Zaps,
ZapTarget, Zaps,
}; };
pub struct NoteView<'a, 'd> { 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, &note_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 { 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) { if self.options().contains(NoteOptions::Textmode) {
NoteResponse::new(self.textmode_ui(ui)) NoteResponse::new(self.textmode_ui(ui))
} else if self.options().contains(NoteOptions::Framed) { } else if self.options().contains(NoteOptions::Framed) {
@@ -376,11 +330,11 @@ impl<'a, 'd> NoteView<'a, 'd> {
if is_narrow(ui.ctx()) { if is_narrow(ui.ctx()) {
ui.set_width(ui.available_width()); ui.set_width(ui.available_width());
} }
self.show_impl(ui) self.show_standard(ui)
}) })
.inner .inner
} else { } 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> { fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option<Response> {
ui.ctx() ui.ctx()
.data_mut(|d| d.get_persisted(hitbox_id)) .data_mut(|d| d.get_temp(hitbox_id))
.map(|note_size: Vec2| { .map(|note_size: Vec2| {
// The hitbox should extend the entire width of the // The hitbox should extend the entire width of the
// container. The hitbox height was cached last layout. // container. The hitbox height was cached last layout.

View File

@@ -39,8 +39,14 @@ bitflags! {
/// no animation override (accessibility) /// no animation override (accessibility)
const NoAnimations = 1 << 17; 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; 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;
} }
} }