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

View File

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

View File

@@ -33,6 +33,8 @@ Add_External_Notifications_Column_41ae = Agregar columna de notificaciones exter
Add_Hashtag_Column_ebf4 = Agregar columna de hashtags
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Agregar columna de últimas notas
# Tooltip text for adding a new deck button
Add_new_deck_f2fc = Agregar nuevo deck
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Agregar columna de notificaciones
# Button label to add a relay
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar enlace
Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota
# Tooltip text for copying npub to clipboard
Copy_npub_to_clipboard_c105 = Copiar npub al portapapeles
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar billetera
Display_name_f9d9 = Nombre para mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
# Button to indicate that the user is done going through the onboarding process.
Done_50dd = Listo
# Column title for editing deck
Edit_Deck_4018 = Editar deck
# Button label to edit a deck
@@ -162,7 +168,7 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
# Label for find user button
Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
Font_size_dd73 = Tamaño de la fuente:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
@@ -191,6 +197,8 @@ k_50K_c2dc = 50.000
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# label for keys setting section
Keys_435f = Claves
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
@@ -209,6 +217,14 @@ Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que n
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck
My_Deck_4ac5 = Mi deck
# reaction from user to a note you were tagged in
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } reaccionó a una nota en la que te etiquetaron
# reaction from user to your note
name__reacted_to_your_note_ead9 = { $name } reaccionó a tu nota
# repost from user
name__reposted_a_note_you_were_tagged_in_1379 = { $name } volvió a publicar una nota en la que te etiquetaron
# repost from user
name__reposted_your_note_1379 = { $name } volvió a publicar tu nota
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label
@@ -236,7 +252,9 @@ Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
On_f412 = Activado
# Column title for finding users to follow
Onboarding_4a25 = Incorporación
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
@@ -257,6 +275,8 @@ Post_now_8a49 = Publicar ahora
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
# Profile picture URL field label
Profile_picture_81ff = Imagen de perfil
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = ID DE CUENTA PÚBLICA
# Column title for quote composition
Quote_475c = Citar
# Error message when quote note cannot be found
@@ -290,7 +310,7 @@ Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
Reset_4e60 = Restablecer
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
@@ -309,10 +329,14 @@ Search_c573 = Búsqueda
Search_notes_42a6 = Buscar notas...
# Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }'
# label describing secret key
SECRET_ACCOUNT_LOGIN_KEY_8440 = CLAVE DE INICIO DE SESIÓN DE CUENTA SECRETA
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button to select all profiles in follow pack
Select_All_a319 = Seleccionar todo
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
@@ -326,7 +350,7 @@ Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
Sort_replies_newest_first_b6c3 = Ordenar las respuestas más recientes primero:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
@@ -352,7 +376,7 @@ Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address
Support_email_44d9 = Support email:
Support_email_44d9 = Correo electrónico de ayuda:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
@@ -405,6 +429,30 @@ Zoom_Level_29a8 = Nivel de zoom:
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
[uno] Se obtuvo { $count } resultado para '{ $query }'
*[otro] Se obtuvieron { $count } resultados para '{ $query }'
}
# amount of reactions a note you were tagged in received
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
{ $count ->
[one] { $name } y { $count } persona más reaccionaron a una nota en la que te etiquetaron
*[other] { $name } y { $count } personas más reaccionaron a una nota en la que te etiquetaron
}
# describing the amount of reactions your note received
name__and__count__others_reacted_to_your_note_0f6a =
{ $count ->
[one] { $name } y { $count } persona más reaccionaron a tu nota
*[other] { $name } y { $count } personas más reaccionaron a tu nota
}
# describing the amount of reposts a note you were tagged in received
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
{ $count ->
[one] { $name } y { $count } persona más volvieron a publicar una nota en la que te etiquetaron
*[other] { $name } y { $count } personas más volvieron a publicar una nota en la que te etiquetaron
}
# describing the amount of reposts your note received
name__and__count__others_reposted_your_note_70a0 =
{ $count ->
[one] { $name } y { $count } persona más volvieron a publicar tu nota
*[other] { $name } y { $count } personas más volvieron a publicar tu nota
}

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
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Añadir columna de últimas notas
# Tooltip text for adding a new deck button
Add_new_deck_f2fc = Añadir nuevo deck
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Añadir columna de notificaciones
# Button label to add a relay
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar enlace
Copy_Note_ID_6b45 = Copiar ID de nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON de nota
# Tooltip text for copying npub to clipboard
Copy_npub_to_clipboard_c105 = Copiar npub al portapapeles
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar pubkey
# Copy the text content of the note to clipboard
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar monedero
Display_name_f9d9 = Nombre para mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" se utilizará para la identificación
# Button to indicate that the user is done going through the onboarding process.
Done_50dd = Listo
# Column title for editing deck
Edit_Deck_4018 = Editar deck
# Button label to edit a deck
@@ -162,7 +168,7 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
# Label for find user button
Find_User_bd12 = Buscar usuario
# Label for font size, Appearance settings section
Font_size_dd73 = Font size:
Font_size_dd73 = Tamaño de la fuente:
# Title for hashtags column
Hashtags_f8e0 = Hashtags
# Title for Home column
@@ -191,6 +197,8 @@ k_50K_c2dc = 50.000
k_5K_f7e6 = 5.000
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
# label for keys setting section
Keys_435f = Claves
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
@@ -209,6 +217,14 @@ Media_from_someone_you_don_t_follow_5611 = Contenido multimedia de alguien que n
Moves_this_column_to_another_position_0d4b = Mueve esta columna a otra posición
# Title for the user's deck
My_Deck_4ac5 = Mi deck
# reaction from user to a note you were tagged in
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } ha reaccionado a una nota en la que te han etiquetado
# reaction from user to your note
name__reacted_to_your_note_ead9 = { $name } ha reaccionado a tu nota
# repost from user
name__reposted_a_note_you_were_tagged_in_1379 = { $name } ha vuelto a publicar una nota en la que te han etiquetado
# repost from user
name__reposted_your_note_1379 = { $name } ha vuelto a publicar tu nota
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = ¿Primera vez en Nostr?
# NIP-05 identity field label
@@ -236,7 +252,9 @@ Notifications_ef56 = Notificaciones
# Relative time for very recent events (less than 3 seconds)
now_2181 = ahora
# Setting to turn on sorting replies so that the newest are shown first
On_f412 = On
On_f412 = Activado
# Column title for finding users to follow
Onboarding_4a25 = Incorporación
# Button label to open email client
Open_Email_25e9 = Abrir correo electrónico
# Instruction to open email client
@@ -257,6 +275,8 @@ Post_now_8a49 = Publicar ahora
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Presiona el siguiente botón para copiar los registros más recientes al portapapeles del sistema. A continuación, pégalos en tu correo electrónico.
# Profile picture URL field label
Profile_picture_81ff = Imagen de perfil
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = ID DE CUENTA PÚBLICA
# Column title for quote composition
Quote_475c = Citar
# Error message when quote note cannot be found
@@ -290,7 +310,7 @@ Repost_this_note_8e56 = Volver a publicar esta nota
# Label for reposted notes
Reposted_61c8 = Publicadas de nuevo
# Label for reset note body font size, Appearance settings section
Reset_4e60 = Reset
Reset_4e60 = Restablecer
# Label for reset zoom level, Appearance settings section
Reset_62d4 = Restablecer
# Heading for support section
@@ -309,10 +329,14 @@ Search_c573 = Búsqueda
Search_notes_42a6 = Buscar notas...
# Search in progress message
Searching_for___query_5d18 = Buscando '{ $query }'
# label describing secret key
SECRET_ACCOUNT_LOGIN_KEY_8440 = CLAVE DE INICIO DE SESIÓN DE CUENTA SECRETA
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
# Description for universe column
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
# Button to select all profiles in follow pack
Select_All_a319 = Seleccionar todo
# Button label to send a zap
Send_1ea4 = Enviar
# Column title for app settings
@@ -326,7 +350,7 @@ Someone_else_s_Notes_7e5f = Notas de otra persona
# Title for someone else's notifications column
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
# Label for Sort replies newest first, others settings section
Sort_replies_newest_first_b6c3 = Sort replies newest first:
Sort_replies_newest_first_b6c3 = Ordenar las respuestas más recientes primero:
# Description for contact list column
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
# Description for hashtags column
@@ -352,7 +376,7 @@ Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
# Column title for subscribing to individual user
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
# Support email address
Support_email_44d9 = Support email:
Support_email_44d9 = Correo electrónico de ayuda:
# Hover text for dark mode toggle button
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
# Hover text for light mode toggle button
@@ -405,6 +429,30 @@ Zoom_Level_29a8 = Nivel de zoom:
# Search results count
Got__count__results_for___query_85fb =
{ $count ->
[uno] Obtuvo { $count } resultado para '{ $query }'
*[otro] Obtuvo { $count } resultados para '{ $query }'
[uno] Se ha obtenido { $count } resultado para '{ $query }'
*[otro] Se han obtenido { $count } resultados para '{ $query }'
}
# amount of reactions a note you were tagged in received
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
{ $count ->
[one] { $name } y { $count } persona más han reaccionado a una nota en la que te han etiquetado
*[other] { $name } y { $count } personas más han reaccionado a una nota en la que te han etiquetado
}
# describing the amount of reactions your note received
name__and__count__others_reacted_to_your_note_0f6a =
{ $count ->
[one] { $name } y { $count } persona más han reaccionado a tu nota
*[other] { $name } y { $count } personas más han reaccionado a tu nota
}
# describing the amount of reposts a note you were tagged in received
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
{ $count ->
[one] { $name } y { $count } persona más han vuelto a publicar una nota en la que te han etiquetado
*[other] { $name } y { $count } personas más han vuelto a publicar una nota en la que te han etiquetado
}
# describing the amount of reposts your note received
name__and__count__others_reposted_your_note_70a0 =
{ $count ->
[one] { $name } y { $count } persona han vuelto a publicar tu nota
*[other] { $name } y { $count } personas más han vuelto a publicar tu nota
}

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
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Ajouter une colonne pour les dernières notes
# Tooltip text for adding a new deck button
Add_new_deck_f2fc = Ajouter un nouveau deck
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Ajouter une colonne pour les notifications
# Button label to add a relay
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copier le lien
Copy_Note_ID_6b45 = Copier l'ID de la note
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copier le JSON de la note
# Tooltip text for copying npub to clipboard
Copy_npub_to_clipboard_c105 = Copier la npub dans le presse-papiers
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copier la Pubkey
# Copy the text content of the note to clipboard
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Supprimer le portefeuille
Display_name_f9d9 = Nom d'utilisateur
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" sera utilisé pour l'identification
# Button to indicate that the user is done going through the onboarding process.
Done_50dd = Fait
# Column title for editing deck
Edit_Deck_4018 = Modifier le deck
# Button label to edit a deck
@@ -191,6 +197,8 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Gardez une trace de vos notes & réponses
# label for keys setting section
Keys_435f = Clés
# Label for language, Appearance settings section
Language_e264 = Langue :
# Title for last note per user column
@@ -209,6 +217,14 @@ Media_from_someone_you_don_t_follow_5611 = Média d'une personne que vous ne sui
Moves_this_column_to_another_position_0d4b = Déplace cette colonne vers une autre position
# Title for the user's deck
My_Deck_4ac5 = Mon deck
# reaction from user to a note you were tagged in
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } a réagi à une note dans laquelle vous avez été tagué
# reaction from user to your note
name__reacted_to_your_note_ead9 = { $name } a réagi à votre note
# repost from user
name__reposted_a_note_you_were_tagged_in_1379 = { $name } a reposté une note dans laquelle vous avez été tagué
# repost from user
name__reposted_your_note_1379 = { $name } a reposté votre note
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nouveau sur Nostr ?
# NIP-05 identity field label
@@ -259,6 +275,8 @@ Post_now_8a49 = Publier maintenant
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Cliquez sur le bouton ci-dessous pour copier vos données les plus récentes dans le presse-papiers de votre système. Collez-les ensuite dans votre courrier électronique.
# Profile picture URL field label
Profile_picture_81ff = Photo de profil
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = IDENTITE PUBLIQUE DU COMPTE
# Column title for quote composition
Quote_475c = Citation
# Error message when quote note cannot be found
@@ -311,6 +329,8 @@ Search_c573 = Rechercher
Search_notes_42a6 = Rechercher des notes...
# Search in progress message
Searching_for___query_5d18 = Recherche par '{ $query }'
# label describing secret key
SECRET_ACCOUNT_LOGIN_KEY_8440 = CLÉ SECRETE DE CONNEXION DU COMPTE
# Description for Home column
See_notes_from_your_contacts_ac16 = Afficher les notes de vos contacts
# Description for universe column
@@ -412,3 +432,27 @@ Got__count__results_for___query_85fb =
[one] A obtenu { $count } pour '{ $query }'
*[other] A obtenu { $count } pour '{ $query }'
}
# amount of reactions a note you were tagged in received
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
{ $count ->
[un] { $name } et { $count } a réagi à une note où vous êtes tagué
*[autre] { $name } et { $count } ont réagi à une note où vous êtes tagué
}
# describing the amount of reactions your note received
name__and__count__others_reacted_to_your_note_0f6a =
{ $count ->
[un] { $name } et { $count } autres ont réagi à votre note
*[autre] { $name } et { $count } autres ont réagi à votre note
}
# describing the amount of reposts a note you were tagged in received
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
{ $count ->
[un] { $name } et { $count } a reposté une note où vous êtes tagué
*[autre] { $name } et { $count } ont reposté une note où vous êtes tagué
}
# describing the amount of reposts your note received
name__and__count__others_reposted_your_note_70a0 =
{ $count ->
[un] { $name } et { $count } a reposté votre note
*[autre] { $name } et { $count } ont reposté votre note
}

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
# Column title for adding last notes column
Add_Last_Notes_Column_bbad = Adicionar coluna de últimas notas
# Tooltip text for adding a new deck button
Add_new_deck_f2fc = Adicionar nova aba
# Column title for adding notifications column
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
# Button label to add a relay
@@ -93,6 +95,8 @@ Copy_Link_dc7c = Copiar link
Copy_Note_ID_6b45 = Copiar ID da nota
# Copy the raw note data in JSON format to clipboard
Copy_Note_JSON_9e4e = Copiar JSON da nota
# Tooltip text for copying npub to clipboard
Copy_npub_to_clipboard_c105 = Copiar npub para área de transferência
# Copy the author's public key to clipboard
Copy_Pubkey_9cc4 = Copiar chave pública
# Copy the text content of the note to clipboard
@@ -141,6 +145,8 @@ Delete_Wallet_d1d4 = Eliminar carteira
Display_name_f9d9 = Nome a mostrar
# Domain identification message
domain___will_be_used_for_identification_b67e = "{ $domain }" será usado para identificação
# Button to indicate that the user is done going through the onboarding process.
Done_50dd = Concluído
# Column title for editing deck
Edit_Deck_4018 = Editar aba
# Button label to edit a deck
@@ -191,6 +197,8 @@ k_50K_c2dc = 50K
k_5K_f7e6 = 5K
# Description for your notes column
Keep_track_of_your_notes___replies_a334 = Acompanha as tuas notas e respostas
# label for keys setting section
Keys_435f = Chaves
# Label for language, Appearance settings section
Language_e264 = Idioma:
# Title for last note per user column
@@ -209,6 +217,14 @@ Media_from_someone_you_don_t_follow_5611 = Conteúdo de alguém que não segues
Moves_this_column_to_another_position_0d4b = Mover esta coluna para outra posição
# Title for the user's deck
My_Deck_4ac5 = Minha aba
# reaction from user to a note you were tagged in
name__reacted_to_a_note_you_were_tagged_in_4b62 = { $name } reagiu a uma nota em que te marcaram
# reaction from user to your note
name__reacted_to_your_note_ead9 = { $name } reagiu à tua nota
# repost from user
name__reposted_a_note_you_were_tagged_in_1379 = { $name } republicou uma nota em que te marcaram
# repost from user
name__reposted_your_note_1379 = { $name } republicou a tua nota
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
New_to_Nostr_a2fd = Nov@ no Nostr?
# NIP-05 identity field label
@@ -259,6 +275,8 @@ Post_now_8a49 = Publicar agora
Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Prime o botão abaixo para copiar os teus registos mais recentes para a área de transferência do teu sistema. Depois cola-os no teu e-mail.
# Profile picture URL field label
Profile_picture_81ff = Foto de perfil
# label describing public key
PUBLIC_ACCOUNT_ID_4394 = ID da CONTA PÚBLICA
# Column title for quote composition
Quote_475c = Citação
# Error message when quote note cannot be found
@@ -311,6 +329,8 @@ Search_c573 = Procurar
Search_notes_42a6 = Procurar notas...
# Search in progress message
Searching_for___query_5d18 = Procurando por '{ $query }'
# label describing secret key
SECRET_ACCOUNT_LOGIN_KEY_8440 = CHAVE SECRETA DE LOGIN DA CONTA
# Description for Home column
See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos
# Description for universe column
@@ -412,3 +432,27 @@ Got__count__results_for___query_85fb =
[one] { $count } resultado obtido para '{ $query }'
*[other] { $count } resultados obtidos para '{ $query }'
}
# amount of reactions a note you were tagged in received
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
{ $count ->
[one] { $name } e { $count } outro reagiram a uma nota em que te marcaram
*[other] { $name } e { $count } outros reagiram a uma nota que te marcaram
}
# describing the amount of reactions your note received
name__and__count__others_reacted_to_your_note_0f6a =
{ $count ->
[one] { $name } e { $count } outro reagiram à tua nota
*[other] { $name } e { $count } outros reagiram à tua nota
}
# describing the amount of reposts a note you were tagged in received
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
{ $count ->
[one] { $name } e { $count } outro republicaram uma nota em que te marcaram
*[other] { $name } e { $count } outros republicaram uma nota que te marcaram
}
# describing the amount of reposts your note received
name__and__count__others_reposted_your_note_70a0 =
{ $count ->
[one] { $name } e { $count } outro republicaram a tua nota
*[other] { $name } e { $count } outros republicaram a tua nota
}

View File

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

View File

@@ -152,7 +152,9 @@ impl<'a> RelayMessage<'a> {
return Ok(Self::ok(event_id, status, message));
}
Err(Error::DecodeFailed("unrecognized message type".into()))
Err(Error::DecodeFailed(format!(
"unrecognized message type: '{msg}'"
)))
}
}
@@ -220,15 +222,15 @@ mod tests {
),
(
r#"["NOTICE": 404]"#,
Err(Error::DecodeFailed("unrecognized message type".into())),
Err(Error::DecodeFailed("unrecognized message type: '[\"NOTICE\": 404]'".into())),
),
(
r#"["OK","event_id"]"#,
Err(Error::DecodeFailed("unrecognized message type".into())),
Err(Error::DecodeFailed("unrecognized message type: '[\"OK\",\"event_id\"]'".into())),
),
(
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"]"#,
Err(Error::DecodeFailed("unrecognized message type".into())),
Err(Error::DecodeFailed("unrecognized message type: '[\"OK\",\"b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30\"]'".into())),
),
(
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,""]"#,

View File

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

View File

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

View File

@@ -142,12 +142,6 @@ impl FilterState {
Self::Ready(HybridFilter::unsplit(filter))
}
/// The filter is ready, but we have a different local filter from
/// our remote one
pub fn ready_split(local: Vec<Filter>, remote: Vec<Filter>) -> Self {
Self::Ready(HybridFilter::split(local, remote))
}
/// Our hybrid filter is ready (either split or unsplit)
pub fn ready_hybrid(filter: HybridFilter) -> Self {
Self::Ready(filter)
@@ -219,7 +213,7 @@ pub struct FilteredTags {
/// The local and remote filter are related but slightly different
#[derive(Debug, Clone)]
pub struct SplitFilter {
pub local: Vec<Filter>,
pub local: Vec<NdbQueryPackage>,
pub remote: Vec<Filter>,
}
@@ -236,16 +230,23 @@ impl HybridFilter {
HybridFilter::Unsplit(filter)
}
pub fn split(local: Vec<Filter>, remote: Vec<Filter>) -> Self {
pub fn split(local: Vec<NdbQueryPackage>, remote: Vec<Filter>) -> Self {
HybridFilter::Split(SplitFilter { local, remote })
}
pub fn local(&self) -> &[Filter] {
pub fn local(&self) -> NdbQueryPackages<'_> {
match self {
Self::Split(split) => &split.local,
Self::Split(split) => NdbQueryPackages {
packages: split.local.iter().map(NdbQueryPackage::borrow).collect(),
},
// local as the same as remote in unsplit
Self::Unsplit(local) => local,
Self::Unsplit(local) => NdbQueryPackages {
packages: vec![NdbQueryPackageUnowned {
filters: local,
kind: None,
}],
},
}
}
@@ -260,77 +261,139 @@ impl HybridFilter {
}
impl FilteredTags {
pub fn into_follow_filter(self) -> Vec<Filter> {
self.into_filter([1], default_limit())
}
// TODO: make this more general
pub fn into_filter<I>(self, kinds: I, limit: u64) -> Vec<Filter>
where
I: IntoIterator<Item = u64> + Copy,
{
pub fn into_query_package(self, kind: ValidKind, limit: u64) -> NdbQueryPackage {
let mut filters: Vec<Filter> = Vec::with_capacity(2);
if let Some(authors) = self.authors {
filters.push(authors.kinds(kinds).limit(limit).build())
filters.push(authors.kinds(vec![kind.kind()]).limit(limit).build())
}
if let Some(hashtags) = self.hashtags {
filters.push(hashtags.kinds(kinds).limit(limit).build())
if matches!(&kind, ValidKind::One | ValidKind::Zero) {
filters.push(hashtags.kinds(vec![kind.kind()]).limit(limit).build())
}
}
NdbQueryPackage { filters, kind }
}
// TODO: make this more general
pub fn into_filter(self, shared_kinds: Vec<u64>, limit: u64) -> Vec<Filter> {
let mut filters: Vec<Filter> = Vec::with_capacity(2);
if let Some(authors) = self.authors {
let mut author_kinds = shared_kinds.clone();
author_kinds.insert(0, 6);
filters.push(authors.kinds(author_kinds).limit(limit).build())
}
if let Some(hashtags) = self.hashtags {
filters.push(hashtags.kinds(shared_kinds).limit(limit).build())
}
filters
}
}
/// `Ndb::query` retrieves the most recent notes of one kind until it can't find anymore THEN proceeds to the next kind.
/// This is not optimal for many scenarios, so this data structure represents data that is packaged optimally for one `Ndb::query`,
#[derive(Debug, Clone)]
pub struct NdbQueryPackage {
pub kind: ValidKind,
pub filters: Vec<Filter>,
}
impl NdbQueryPackage {
pub fn borrow(&self) -> NdbQueryPackageUnowned<'_> {
NdbQueryPackageUnowned {
filters: &self.filters,
kind: Some(self.kind.clone()),
}
}
}
#[derive(Debug, Clone)]
pub struct NdbQueryPackageUnowned<'a> {
pub kind: Option<ValidKind>,
pub filters: &'a Vec<Filter>,
}
pub struct NdbQueryPackages<'a> {
pub packages: Vec<NdbQueryPackageUnowned<'a>>,
}
impl<'a> NdbQueryPackages<'a> {
pub fn combined(&self) -> Vec<Filter> {
let mut combined = Vec::new();
for package in &self.packages {
combined.extend_from_slice(package.filters);
}
combined
}
}
#[derive(Debug, Clone)]
pub enum ValidKind {
Zero,
One,
Six,
}
impl ValidKind {
fn kind(&self) -> u64 {
match self {
ValidKind::Zero => 0,
ValidKind::One => 1,
ValidKind::Six => 6,
}
}
}
/// Create a "last N notes per pubkey" query.
pub fn last_n_per_pubkey_from_tags(
note: &Note,
kind: u64,
notes_per_pubkey: u64,
) -> Result<Vec<Filter>, Error> {
use rand::Rng;
let mut filters: Vec<Filter> = vec![];
let mut rng = rand::rng();
for tag in note.tags() {
// TODO: fix arbitrary MAX_FILTER limit in nostrdb
if filters.len() == 15 {
break;
}
// TODO: fix arbitrary MAX_FILTER limit in nostrdb
const LIMIT: usize = 15;
for (i, tag) in note.tags().iter().enumerate() {
if tag.count() < 2 {
continue;
}
let t = if let Some(t) = tag.get_unchecked(0).variant().str() {
t
} else {
let Some("p") = tag.get_str(0) else {
continue;
};
if t == "p" {
let author = if let Some(author) = tag.get_unchecked(1).variant().id() {
author
} else {
continue;
};
let Some(author) = tag.get_id(1) else {
continue;
};
let mk_filter = || {
let mut filter = Filter::new();
filter.start_authors_field()?;
filter.add_id_element(author)?;
let _ = filter.start_authors_field();
let _ = filter.add_id_element(author);
filter.end_field();
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build());
} else if t == "t" {
let hashtag = if let Some(hashtag) = tag.get_unchecked(1).variant().str() {
hashtag
} else {
continue;
};
filter.kinds([kind]).limit(notes_per_pubkey).build()
};
let mut filter = Filter::new();
filter.start_tags_field('t')?;
filter.add_str_element(hashtag)?;
filter.end_field();
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build());
// since we're limited due to a nostrdb bug, we reservoir sample to keep things interesting
if filters.len() < LIMIT {
filters.push(mk_filter());
} else {
let j = rng.random_range(0..=i);
if j < LIMIT {
filters[j] = mk_filter();
}
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ use crate::search::SearchQuery;
use crate::timeline::{Timeline, TimelineTab};
use enostr::{Filter, NoteId, Pubkey};
use nostrdb::{Ndb, Transaction};
use notedeck::filter::{NdbQueryPackage, ValidKind};
use notedeck::{
contacts::{contacts_filter, hybrid_contacts_filter},
filter::{self, default_limit, default_remote_limit, HybridFilter},
@@ -728,15 +729,29 @@ fn last_per_pubkey_filter_state(ndb: &Ndb, pk: &Pubkey) -> FilterState {
}
fn profile_filter(pk: &[u8; 32]) -> HybridFilter {
let local = vec![
NdbQueryPackage {
filters: vec![Filter::new()
.authors([pk])
.kinds([1])
.limit(default_limit())
.build()],
kind: ValidKind::One,
},
NdbQueryPackage {
filters: vec![Filter::new()
.authors([pk])
.kinds([6])
.limit(default_limit())
.build()],
kind: ValidKind::Six,
},
];
HybridFilter::split(
local,
vec![Filter::new()
.authors([pk])
.kinds([1])
.limit(default_limit())
.build()],
vec![Filter::new()
.authors([pk])
.kinds([1, 0])
.kinds([1, 6, 0])
.limit(default_remote_limit())
.build()],
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -342,6 +342,13 @@ impl<'a, 'd> PostView<'a, 'd> {
}
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
ScrollArea::vertical()
.id_salt(PostView::scroll_id())
.show(ui, |ui| self.ui_no_scroll(txn, ui))
.inner
}
pub fn ui_no_scroll(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
while let Some(selected_file) = get_next_selected_file() {
match selected_file {
Ok(selected_media) => {
@@ -358,13 +365,6 @@ impl<'a, 'd> PostView<'a, 'd> {
}
}
ScrollArea::vertical()
.id_salt(PostView::scroll_id())
.show(ui, |ui| self.ui_no_scroll(txn, ui))
.inner
}
pub fn ui_no_scroll(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
let focused = self.focused(ui);
let stroke = if focused {
ui.visuals().selection.stroke

View File

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

View File

@@ -1,13 +1,14 @@
use egui::containers::scroll_area::ScrollBarVisibility;
use egui::{vec2, Direction, Layout, Margin, Pos2, ScrollArea, Sense, Stroke};
use egui::{vec2, Color32, Direction, Layout, Margin, Pos2, RichText, ScrollArea, Sense, Stroke};
use egui_tabs::TabColor;
use enostr::Pubkey;
use nostrdb::{ProfileRecord, Transaction};
use nostrdb::{Note, ProfileRecord, Transaction};
use notedeck::fonts::get_font_size;
use notedeck::name::get_display_name;
use notedeck::ui::is_narrow;
use notedeck::{tr_plural, JobsCache, Muted, NoteRef};
use notedeck::{tr_plural, JobsCache, Muted, NotedeckTextStyle};
use notedeck_ui::app_images::{like_image, repost_image};
use notedeck_ui::ProfilePic;
use notedeck_ui::{ProfilePic, ProfilePreview};
use std::f32::consts::PI;
use tracing::{error, warn};
@@ -88,7 +89,7 @@ fn timeline_ui(
ui: &mut egui::Ui,
timeline_id: &TimelineKind,
timeline_cache: &mut TimelineCache,
note_options: NoteOptions,
mut note_options: NoteOptions,
note_context: &mut NoteContext,
jobs: &mut JobsCache,
col: usize,
@@ -118,7 +119,8 @@ fn timeline_ui(
note_context.i18n,
timeline.selected_view,
&timeline.views,
);
)
.inner;
// need this for some reason??
ui.add_space(3.0);
@@ -151,12 +153,6 @@ fn timeline_ui(
.auto_shrink([false, false])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible);
let offset_id = scroll_id.with("timeline_scroll_offset");
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
scroll_area = scroll_area.vertical_scroll_offset(offset);
}
if goto_top_resp.is_some_and(|r| r.clicked()) {
scroll_area = scroll_area.vertical_scroll_offset(0.0);
}
@@ -181,6 +177,10 @@ fn timeline_ui(
let txn = Transaction::new(note_context.ndb).expect("failed to create txn");
if matches!(timeline_id, TimelineKind::Notifications(_)) {
note_options.set(NoteOptions::Notification, true)
}
TimelineTabView::new(
timeline.current_view(),
note_options,
@@ -191,8 +191,6 @@ fn timeline_ui(
.show(ui)
});
ui.data_mut(|d| d.insert_temp(offset_id, scroll_output.state.offset.y));
let at_top_after_scroll = scroll_output.state.offset.y == 0.0;
let cur_show_top_button = ui.ctx().data(|d| d.get_temp::<bool>(show_top_button_id));
@@ -280,7 +278,7 @@ pub fn tabs_ui(
i18n: &mut Localization,
selected: usize,
views: &[TimelineTab],
) -> usize {
) -> egui::InnerResponse<usize> {
ui.spacing_mut().item_spacing.y = 0.0;
let tab_res = egui_tabs::Tabs::new(views.len() as i32)
@@ -328,7 +326,9 @@ pub fn tabs_ui(
let sel = tab_res.selected().unwrap_or_default();
let (underline, underline_y) = tab_res.inner()[sel as usize].inner;
let res_inner = &tab_res.inner()[sel as usize];
let (underline, underline_y) = res_inner.inner;
let underline_width = underline.span();
let tab_anim_id = ui.id().with("tab_anim");
@@ -355,7 +355,7 @@ pub fn tabs_ui(
ui.painter().hline(underline, underline_y, stroke);
sel as usize
egui::InnerResponse::new(sel as usize, res_inner.response.clone())
}
fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 {
@@ -440,15 +440,46 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
entry: &NoteUnit,
mute: &std::sync::Arc<Muted>,
) -> RenderEntryResponse {
let underlying_note = {
let underlying_note_key = match entry {
NoteUnit::Single(note_ref) => note_ref.key,
NoteUnit::Composite(composite_unit) => match composite_unit {
CompositeUnit::Reaction(reaction_unit) => reaction_unit.note_reacted_to.key,
CompositeUnit::Repost(repost_unit) => repost_unit.note_reposted.key,
},
};
let Ok(note) = self
.note_context
.ndb
.get_note_by_key(self.txn, underlying_note_key)
else {
warn!("failed to query note {:?}", underlying_note_key);
return RenderEntryResponse::Unsuccessful;
};
note
};
let muted = root_note_id_from_selected_id(
self.note_context.ndb,
self.note_context.note_cache,
self.txn,
underlying_note.id(),
)
.is_ok_and(|root_id| mute.is_muted(&underlying_note, root_id.bytes()));
if muted {
return RenderEntryResponse::Success(None);
}
match entry {
NoteUnit::Single(note_ref) => render_note(
NoteUnit::Single(_) => render_note(
ui,
self.note_context,
self.note_options,
self.jobs,
mute,
self.txn,
note_ref,
&underlying_note,
),
NoteUnit::Composite(composite) => match composite {
CompositeUnit::Reaction(reaction_unit) => render_reaction_cluster(
@@ -458,6 +489,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
self.jobs,
mute,
self.txn,
&underlying_note,
reaction_unit,
),
CompositeUnit::Repost(repost_unit) => render_repost_cluster(
@@ -467,6 +499,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
self.jobs,
mute,
self.txn,
&underlying_note,
repost_unit,
),
},
@@ -483,7 +516,9 @@ impl CompositeType {
fn image(&self, darkmode: bool) -> egui::Image<'static> {
match self {
CompositeType::Reaction => like_image(),
CompositeType::Repost => repost_image(darkmode),
CompositeType::Repost => {
repost_image(darkmode).tint(Color32::from_rgb(0x68, 0xC3, 0x51))
}
}
}
@@ -493,6 +528,7 @@ impl CompositeType {
first_name: &str,
total_count: usize,
referenced_type: ReferencedNoteType,
notification: bool,
) -> String {
let count = total_count - 1;
@@ -500,7 +536,16 @@ impl CompositeType {
CompositeType::Reaction => {
reaction_description(loc, first_name, count, referenced_type)
}
CompositeType::Repost => repost_description(loc, first_name, count, referenced_type),
CompositeType::Repost => repost_description(
loc,
first_name,
count,
if notification {
DescriptionType::Notification(referenced_type)
} else {
DescriptionType::Other
},
),
}
}
}
@@ -553,46 +598,72 @@ fn reaction_description(
}
}
enum DescriptionType {
Notification(ReferencedNoteType),
Other,
}
fn repost_description(
loc: &mut Localization,
first_name: &str,
count: usize,
referenced_type: ReferencedNoteType,
description_type: DescriptionType,
) -> String {
match referenced_type {
ReferencedNoteType::Tagged => {
if count == 0 {
tr!(
loc,
"{name} reposted a note you were tagged in",
"repost from user",
name = first_name
)
} else {
tr_plural!(
loc,
"{name} and {count} other reposted a note you were tagged in",
"{name} and {count} others reposted a note you were tagged in",
"describing the amount of reposts a note you were tagged in received",
count,
name = first_name
)
match description_type {
DescriptionType::Notification(referenced_type) => match referenced_type {
ReferencedNoteType::Tagged => {
if count == 0 {
tr!(
loc,
"{name} reposted a note you were tagged in",
"repost from user",
name = first_name
)
} else {
tr_plural!(
loc,
"{name} and {count} other reposted a note you were tagged in",
"{name} and {count} others reposted a note you were tagged in",
"describing the amount of reposts a note you were tagged in received",
count,
name = first_name
)
}
}
}
ReferencedNoteType::Yours => {
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 {
tr!(
loc,
"{name} reposted your note",
"{name} reposted",
"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",
"{name} and {count} other reposted",
"{name} and {count} others reposted",
"describing the amount of reposts a note has",
count,
name = first_name
)
@@ -606,32 +677,11 @@ fn render_note(
note_context: &mut NoteContext,
note_options: NoteOptions,
jobs: &mut JobsCache,
mute: &std::sync::Arc<Muted>,
txn: &Transaction,
note_ref: &NoteRef,
note: &Note,
) -> RenderEntryResponse {
let note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, note_ref.key) {
note
} else {
warn!("failed to query note {:?}", note_ref.key);
return RenderEntryResponse::Unsuccessful;
};
let muted = if let Ok(root_id) =
root_note_id_from_selected_id(note_context.ndb, note_context.note_cache, txn, note.id())
{
mute.is_muted(&note, root_id.bytes())
} else {
false
};
if muted {
return RenderEntryResponse::Success(None);
}
let mut action = None;
notedeck_ui::padding(8.0, ui, |ui| {
let resp = NoteView::new(note_context, &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 {
action = Some(note_action);
@@ -643,6 +693,7 @@ fn render_note(
RenderEntryResponse::Success(action)
}
#[allow(clippy::too_many_arguments)]
fn render_reaction_cluster(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
@@ -650,16 +701,9 @@ fn render_reaction_cluster(
jobs: &mut JobsCache,
mute: &std::sync::Arc<Muted>,
txn: &Transaction,
underlying_note: &Note,
reaction: &ReactionUnit,
) -> RenderEntryResponse {
let reacted_to_key = reaction.note_reacted_to.key;
let reacted_to_note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, reacted_to_key) {
note
} else {
warn!("failed to query note {:?}", reacted_to_key);
return RenderEntryResponse::Unsuccessful;
};
let profiles_to_show: Vec<ProfileEntry> = reaction
.reactions
.values()
@@ -674,20 +718,21 @@ fn render_reaction_cluster(
render_composite_entry(
ui,
note_context,
note_options,
note_options | NoteOptions::Notification,
jobs,
reacted_to_note,
underlying_note,
profiles_to_show,
CompositeType::Reaction,
)
}
#[allow(clippy::too_many_arguments)]
fn render_composite_entry(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
note_options: NoteOptions,
mut note_options: NoteOptions,
jobs: &mut JobsCache,
underlying_note: nostrdb::Note<'_>,
underlying_note: &nostrdb::Note<'_>,
profiles_to_show: Vec<ProfileEntry>,
composite_type: CompositeType,
) -> RenderEntryResponse {
@@ -697,92 +742,203 @@ fn render_composite_entry(
let num_profiles = profiles_to_show.len();
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()
.inner_margin(Margin::symmetric(8, 4))
.show(ui, |ui| {
ui.allocate_ui_with_layout(
vec2(ui.available_width(), 32.0),
Layout::left_to_right(egui::Align::Center),
|ui| {
ui.vertical(|ui| {
ui.add_space(4.0);
ui.add_sized(
vec2(28.0, 28.0),
composite_type.image(ui.visuals().dark_mode),
);
});
let show_label_newline = ui
.horizontal_wrapped(|ui| {
let pfps_resp = ui
.allocate_ui_with_layout(
vec2(ui.available_width(), 32.0),
Layout::left_to_right(egui::Align::Center),
|ui| {
render_profiles(
ui,
profiles_to_show,
&composite_type,
note_context.img_cache,
note_options.contains(NoteOptions::Notification),
)
},
)
.inner;
ui.add_space(16.0);
if let Some(cur_action) = pfps_resp.action {
action = Some(cur_action);
}
ui.horizontal(|ui| {
ScrollArea::horizontal()
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
for entry in profiles_to_show {
let resp = ui.add(
&mut ProfilePic::from_profile_or_default(
note_context.img_cache,
entry.record.as_ref(),
)
.size(24.0)
.sense(Sense::click()),
);
if resp.clicked() {
action = Some(NoteAction::Profile(*entry.pk))
}
}
});
});
},
);
let referenced_type = if note_context
.accounts
.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(
let description = composite_type.description(
note_context.i18n,
&first_name,
num_profiles,
referenced_type,
))
note_options.contains(NoteOptions::Notification),
);
let galley = ui.painter().layout_no_wrap(
description.clone(),
NotedeckTextStyle::Small.get_font_id(ui.ctx()),
ui.visuals().text_color(),
);
ui.add_space(4.0);
let galley_pos = {
let mut galley_pos = ui.next_widget_position();
galley_pos.y = pfps_resp.resp.rect.right_center().y;
galley_pos.y -= galley.rect.height() / 2.0;
galley_pos
};
let fits_no_wrap = {
let mut rightmost_pos = galley_pos;
rightmost_pos.x += galley.rect.width();
ui.available_rect_before_wrap().contains(rightmost_pos)
};
if fits_no_wrap {
ui.painter()
.galley(galley_pos, galley, ui.visuals().text_color());
None
} else {
Some(description)
}
})
.inner;
if let Some(desc) = show_label_newline {
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.add_space(48.0);
ui.horizontal_wrapped(|ui| {
ui.add(egui::Label::new(
RichText::new(desc)
.size(get_font_size(ui.ctx(), &NotedeckTextStyle::Small)),
));
});
});
});
}
ui.add_space(16.0);
ui.horizontal(|ui| {
ui.add_space(48.0);
let options = note_options
.difference(NoteOptions::ActionBar | NoteOptions::OptionsButton)
.union(NoteOptions::NotificationPreview);
let resp = NoteView::new(note_context, &underlying_note, options, jobs).show(ui);
let resp = ui
.horizontal(|ui| {
if note_options.contains(NoteOptions::Notification) {
note_options = note_options
.difference(NoteOptions::ActionBar | NoteOptions::OptionsButton)
.union(NoteOptions::NotificationPreview);
if let Some(note_action) = resp.action {
action = Some(note_action);
}
});
ui.add_space(48.0);
};
NoteView::new(note_context, underlying_note, note_options, jobs).show(ui)
})
.inner;
if let Some(note_action) = resp.action {
action.get_or_insert(note_action);
}
});
notedeck_ui::hline(ui);
RenderEntryResponse::Success(action)
}
fn render_profiles(
ui: &mut egui::Ui,
profiles_to_show: Vec<ProfileEntry>,
composite_type: &CompositeType,
img_cache: &mut notedeck::Images,
notification: bool,
) -> PfpsResponse {
let mut action = None;
if notification {
ui.add_space(8.0);
}
ui.vertical(|ui| {
ui.add_space(9.0);
ui.add_sized(
vec2(20.0, 20.0),
composite_type.image(ui.visuals().dark_mode),
);
});
if notification {
ui.add_space(16.0);
} else {
ui.add_space(2.0);
}
let resp = ui.horizontal(|ui| {
ScrollArea::horizontal()
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
let mut last_pfp_resp = None;
for entry in profiles_to_show {
let mut resp = ui.add(
&mut ProfilePic::from_profile_or_default(img_cache, entry.record.as_ref())
.size(24.0)
.sense(Sense::click()),
);
if let Some(record) = entry.record.as_ref() {
resp = resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
ui.add(ProfilePreview::new(record, img_cache));
});
}
last_pfp_resp = Some(resp.clone());
if resp.clicked() {
action = Some(NoteAction::Profile(*entry.pk))
}
}
last_pfp_resp
})
.inner
});
let resp = if let Some(r) = resp.inner {
r
} else {
resp.response
};
PfpsResponse { action, resp }
}
struct PfpsResponse {
action: Option<NoteAction>,
resp: egui::Response,
}
#[allow(clippy::too_many_arguments)]
fn render_repost_cluster(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
@@ -790,16 +946,9 @@ fn render_repost_cluster(
jobs: &mut JobsCache,
mute: &std::sync::Arc<Muted>,
txn: &Transaction,
underlying_note: &Note,
repost: &RepostUnit,
) -> RenderEntryResponse {
let reposted_key = repost.note_reposted.key;
let reposted_note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, reposted_key) {
note
} else {
warn!("failed to query note {:?}", reposted_key);
return RenderEntryResponse::Unsuccessful;
};
let profiles_to_show: Vec<ProfileEntry> = repost
.reposts
.values()
@@ -815,7 +964,7 @@ fn render_repost_cluster(
note_context,
note_options,
jobs,
reposted_note,
underlying_note,
profiles_to_show,
CompositeType::Repost,
)

View File

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

View File

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

View File

@@ -5,10 +5,7 @@ pub mod options;
pub mod reply_description;
use crate::{app_images, secondary_label};
use crate::{
profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview,
PulseAlpha, Username,
};
use crate::{widgets::x_button, ProfilePic, ProfilePreview, PulseAlpha, Username};
pub use contents::{render_note_preview, NoteContents};
pub use context::NoteContextButton;
@@ -25,14 +22,12 @@ pub use options::NoteOptions;
pub use reply_description::reply_desc;
use egui::emath::{pos2, Vec2};
use egui::{Id, Pos2, Rect, Response, RichText, Sense};
use egui::{Id, Pos2, Rect, Response, Sense};
use enostr::{KeypairUnowned, NoteId, Pubkey};
use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
use notedeck::{
name::get_display_name,
note::{NoteAction, NoteContext, ZapAction},
tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, NotedeckTextStyle,
ZapTarget, Zaps,
tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, ZapTarget, Zaps,
};
pub struct NoteView<'a, 'd> {
@@ -306,60 +301,19 @@ impl<'a, 'd> NoteView<'a, 'd> {
}
}
fn show_repost(
&mut self,
ui: &mut egui::Ui,
txn: &Transaction,
note_to_repost: Note<'_>,
) -> NoteResponse {
let profile = self
.note_context
.ndb
.get_profile_by_pubkey(txn, self.note.pubkey());
let style = NotedeckTextStyle::Small;
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.add_space(2.0);
ui.add_sized([20.0, 20.0], repost_icon(ui.visuals().dark_mode));
});
ui.add_space(6.0);
let resp = ui.add(one_line_display_name_widget(
ui.visuals(),
get_display_name(profile.as_ref().ok()),
style,
));
if let Ok(rec) = &profile {
resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
ui.add(ProfilePreview::new(rec, self.note_context.img_cache));
});
}
let color = ui.style().visuals.noninteractive().fg_stroke.color;
ui.add_space(4.0);
ui.label(
RichText::new(tr!(
self.note_context.i18n,
"Reposted",
"Label for reposted notes"
))
.color(color)
.text_style(style.text_style()),
);
});
NoteView::new(self.note_context, &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 {
if !self.flags.contains(NoteOptions::TrustMedia) {
let acc = self.note_context.accounts.get_selected_account();
if self.note.pubkey() == acc.key.pubkey.bytes()
|| matches!(
acc.is_following(self.note.pubkey()),
notedeck::IsFollowing::Yes
)
{
self.flags = self.flags.union(NoteOptions::TrustMedia);
}
}
if self.options().contains(NoteOptions::Textmode) {
NoteResponse::new(self.textmode_ui(ui))
} else if self.options().contains(NoteOptions::Framed) {
@@ -376,11 +330,11 @@ impl<'a, 'd> NoteView<'a, 'd> {
if is_narrow(ui.ctx()) {
ui.set_width(ui.available_width());
}
self.show_impl(ui)
self.show_standard(ui)
})
.inner
} else {
self.show_impl(ui)
self.show_standard(ui)
}
}
@@ -789,7 +743,7 @@ fn note_hitbox_id(
fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option<Response> {
ui.ctx()
.data_mut(|d| d.get_persisted(hitbox_id))
.data_mut(|d| d.get_temp(hitbox_id))
.map(|note_size: Vec2| {
// The hitbox should extend the entire width of the
// container. The hitbox height was cached last layout.

View File

@@ -39,8 +39,14 @@ bitflags! {
/// no animation override (accessibility)
const NoAnimations = 1 << 17;
/// Styled for a notification preview
/// The note should be displayed as a preview of the underlying note of a composite unit
const NotificationPreview = 1 << 18;
/// The note is a notification
const Notification = 1 << 19;
/// There is enough trust to show media in this note
const TrustMedia = 1 << 20;
}
}