Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e3e7d54142
|
|||
| a6d91c43e4 | |||
| 19fe3703d9 | |||
| ca67977b82 | |||
| 559e9577fc | |||
| 563fbb9c4b | |||
| 391900d393 | |||
| 11700d6217 | |||
| 50293a6f34 | |||
| d6182ed7c3 | |||
| a0e9c8b434 | |||
| 4ac2e59983 | |||
| 3f1a194983 | |||
| a8eaea6509 | |||
| 9278c90802 | |||
| 02a90eccd1 | |||
| c0fcf53ff6 | |||
| f889b54ed9 | |||
| 7b4c96df91 | |||
| eb44637601 | |||
| ea14713b58 | |||
| a5e7880e25 | |||
| 409ca68567 | |||
| 6cf193b7e3 | |||
| 5bb17cd810 | |||
| ba359c95c2 | |||
| e0ed122951 | |||
| e1ad2e231f | |||
| 91028929b2 | |||
| 2eef34fa1c | |||
| b8eecf0c9a | |||
| 1b9e77a1ff | |||
| 28634301b8 | |||
| ce0d3e8e88 | |||
| 0b4545d598 | |||
| 6db03364fd | |||
|
97b6755504
|
|||
| c765b031e9 | |||
| 024cf3ef91 | |||
| 3a0da9a3e0 | |||
| 10b62a073b | |||
| ac212b96a6 | |||
| 637b05c1e2 | |||
| f436b49fec | |||
| 04ce29d1dd | |||
| ae1d5ab1c5 | |||
| 7caf77aa1c | |||
| 80ae489967 | |||
| 259c0b677a | |||
| 3b7f1f1b39 | |||
| f2258ab16b | |||
| 571435cf85 | |||
| 8f8ff42156 | |||
| 3a95ba05a8 | |||
| 73e44d1497 | |||
| 43b98fc6ed | |||
| 95ee275153 | |||
| 8bc54cc519 | |||
| 5282373434 | |||
| 14c59a6c94 | |||
| 09238baee0 | |||
| 594072cfb8 | |||
| 2882b1c2d9 | |||
| f4b8d235eb | |||
| cf48b29fd8 | |||
| 2a7c5eb983 | |||
| 72d696beb2 | |||
| dea695fa8e | |||
| fc1caf5eb4 | |||
| 5539e4ef82 | |||
| 408afbda50 | |||
| af4b896739 | |||
| d448caa369 | |||
|
b84ad4f1cd
|
|||
|
736ce50f64
|
|||
| e9ca793509 | |||
| ea65af8d5b | |||
| 11aa2142cf | |||
| 6ee2b28e70 | |||
| 31ee64827a | |||
| f243adc855 | |||
| 5224a5d8ae | |||
| 2c96dd99a8 | |||
| e7843bad2f | |||
| c2f012ff75 | |||
| 76fd7a9753 | |||
| 8b5464641d | |||
| c06d18f76b | |||
| 84e60e0642 | |||
| 23f35c60bb | |||
| 30c2ebdcc2 | |||
| 1658600604 | |||
| 529377a706 | |||
| 30af03cfcc | |||
| bb878d3772 | |||
| 5c9eb492b6 | |||
| 0b584a773f | |||
| 78504a6673 | |||
| ae204cbd5c | |||
| 7d4e9799e5 | |||
| 55d7cd3379 | |||
| 697040d862 | |||
| 49866418a6 | |||
| 9b784dfdf7 | |||
| c1d6c0f535 | |||
| 1a93663b1a | |||
| 4992e25b3a | |||
| 7b1ace328f | |||
| 2973a0c6c5 | |||
| 4f63629715 | |||
| 686dea9831 | |||
| 01171ff9d7 | |||
| b421e7e45f | |||
| 86641c6121 |
Generated
+38
-32
@@ -126,7 +126,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "android-activity"
|
name = "android-activity"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea#092a83b747937a2890ac219617a4252c001842ea"
|
source = "git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805#4ee16f1585e4a75031dc10785163d4b920f95805"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-properties",
|
"android-properties",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
@@ -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",
|
||||||
@@ -1403,7 +1403,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "git+https://github.com/damus-io/winit?rev=9e4ea9de75222d2523a20f18d3a0a108c573737d#9e4ea9de75222d2523a20f18d3a0a108c573737d"
|
source = "git+https://github.com/damus-io/winit?rev=a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d#a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
@@ -1420,17 +1420,17 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ecolor"
|
name = "ecolor"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eframe"
|
name = "eframe"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1466,13 +1466,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui"
|
name = "egui"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"ahash",
|
"ahash",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||||
"epaint",
|
"epaint",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
@@ -1484,7 +1484,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-wgpu"
|
name = "egui-wgpu"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1503,7 +1503,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-winit"
|
name = "egui-winit"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"arboard",
|
"arboard",
|
||||||
@@ -1521,7 +1521,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_extras"
|
name = "egui_extras"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -1538,7 +1538,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_glow"
|
name = "egui_glow"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1555,7 +1555,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_nav"
|
name = "egui_nav"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/damus-io/egui-nav?rev=de6e2d51892478fdd516df166f866e64dedbae07#de6e2d51892478fdd516df166f866e64dedbae07"
|
source = "git+https://github.com/damus-io/egui-nav?rev=e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9#e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
@@ -1617,7 +1617,7 @@ checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "emath"
|
name = "emath"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1715,13 +1715,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "epaint"
|
name = "epaint"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"ecolor",
|
"ecolor",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
||||||
"epaint_default_fonts",
|
"epaint_default_fonts",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
@@ -1733,7 +1733,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "epaint_default_fonts"
|
name = "epaint_default_fonts"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
source = "git+https://github.com/damus-io/egui?rev=c1f9e565aa17a7a8b40736602b6ea8a52876f46f#c1f9e565aa17a7a8b40736602b6ea8a52876f46f"
|
source = "git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equator"
|
name = "equator"
|
||||||
@@ -3505,15 +3505,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck"
|
name = "notedeck"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea)",
|
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805)",
|
||||||
"base32",
|
"base32",
|
||||||
"bech32",
|
"bech32",
|
||||||
"bincode",
|
"bincode",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"blurhash",
|
"blurhash",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -3527,10 +3528,12 @@ dependencies = [
|
|||||||
"hashbrown 0.15.4",
|
"hashbrown 0.15.4",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
|
"indexmap 2.9.0",
|
||||||
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lightning-invoice",
|
"lightning-invoice",
|
||||||
"md5",
|
"md5",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"ndk-context",
|
||||||
"nostr 0.37.0",
|
"nostr 0.37.0",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"nwc",
|
"nwc",
|
||||||
@@ -3539,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",
|
||||||
@@ -3558,7 +3562,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_chrome"
|
name = "notedeck_chrome"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"eframe",
|
"eframe",
|
||||||
@@ -3590,7 +3594,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_clndash"
|
name = "notedeck_clndash"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -3609,7 +3613,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_columns"
|
name = "notedeck_columns"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bech32",
|
"bech32",
|
||||||
@@ -3629,6 +3633,8 @@ dependencies = [
|
|||||||
"human_format",
|
"human_format",
|
||||||
"image",
|
"image",
|
||||||
"indexmap 2.9.0",
|
"indexmap 2.9.0",
|
||||||
|
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"ndk-context",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"notedeck",
|
"notedeck",
|
||||||
"notedeck_ui",
|
"notedeck_ui",
|
||||||
@@ -3663,7 +3669,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_dave"
|
name = "notedeck_dave"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-openai",
|
"async-openai",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -3678,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",
|
||||||
@@ -3688,7 +3694,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_notebook"
|
name = "notedeck_notebook"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"jsoncanvas",
|
"jsoncanvas",
|
||||||
@@ -3697,7 +3703,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_ui"
|
name = "notedeck_ui"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"eframe",
|
"eframe",
|
||||||
@@ -4598,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",
|
||||||
@@ -4652,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",
|
||||||
@@ -6273,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",
|
||||||
@@ -7447,10 +7453,10 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "winit"
|
name = "winit"
|
||||||
version = "0.30.8"
|
version = "0.30.8"
|
||||||
source = "git+https://github.com/damus-io/winit?rev=9e4ea9de75222d2523a20f18d3a0a108c573737d#9e4ea9de75222d2523a20f18d3a0a108c573737d"
|
source = "git+https://github.com/damus-io/winit?rev=a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d#a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=092a83b747937a2890ac219617a4252c001842ea)",
|
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805)",
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
|
|||||||
+10
-9
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
package.version = "0.6.0"
|
package.version = "0.7.1"
|
||||||
members = [
|
members = [
|
||||||
"crates/notedeck",
|
"crates/notedeck",
|
||||||
"crates/notedeck_chrome",
|
"crates/notedeck_chrome",
|
||||||
@@ -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"
|
||||||
@@ -27,7 +28,7 @@ egui = { version = "0.31.1", features = ["serde"] }
|
|||||||
egui-wgpu = "0.31.1"
|
egui-wgpu = "0.31.1"
|
||||||
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
||||||
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
||||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "de6e2d51892478fdd516df166f866e64dedbae07" }
|
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "e4231c19dda9e6791d2f7b5cd610b8db5ff9a7f9" }
|
||||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
||||||
#egui_virtual_list = "0.6.0"
|
#egui_virtual_list = "0.6.0"
|
||||||
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
||||||
@@ -88,7 +89,7 @@ openai-api-rs = "6.0.3"
|
|||||||
re_memory = "0.23.4"
|
re_memory = "0.23.4"
|
||||||
oot_bitset = "0.1.1"
|
oot_bitset = "0.1.1"
|
||||||
blurhash = "0.2.3"
|
blurhash = "0.2.3"
|
||||||
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "092a83b747937a2890ac219617a4252c001842ea", features = [ "game-activity" ] }
|
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "4ee16f1585e4a75031dc10785163d4b920f95805", features = [ "game-activity" ] }
|
||||||
|
|
||||||
[profile.small]
|
[profile.small]
|
||||||
inherits = 'release'
|
inherits = 'release'
|
||||||
@@ -106,12 +107,12 @@ strip = true # Strip symbols from binary*
|
|||||||
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
|
#egui_extras = { path = "/home/jb55/dev/github/emilk/egui/crates/egui_extras" }
|
||||||
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
|
#epaint = { path = "/home/jb55/dev/github/emilk/egui/crates/epaint" }
|
||||||
|
|
||||||
egui = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
|
egui = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
eframe = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
|
eframe = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
egui-winit = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
|
egui-winit = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
|
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
egui_extras = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
|
egui_extras = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
epaint = { git = "https://github.com/damus-io/egui", rev = "c1f9e565aa17a7a8b40736602b6ea8a52876f46f" }
|
epaint = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
||||||
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin = { git = "https://github.com/jb55/puffin", package = "puffin", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
puffin_egui = { git = "https://github.com/jb55/puffin", package = "puffin_egui", rev = "c6a6242adaf90b6292c0f462d2acd34d96d224d2" }
|
||||||
#winit = { git = "https://github.com/damus-io/winit", rev = "701a43d3c6479b0a3869acd2cebbfd410d399a59" }
|
#winit = { git = "https://github.com/damus-io/winit", rev = "701a43d3c6479b0a3869acd2cebbfd410d399a59" }
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.33337 5.33337V3.46671C5.33337 2.71997 5.33337 2.3466 5.4787 2.06139C5.60653 1.8105 5.8105 1.60653 6.06139 1.4787C6.3466 1.33337 6.71997 1.33337 7.46671 1.33337H12.5334C13.2801 1.33337 13.6535 1.33337 13.9387 1.4787C14.1896 1.60653 14.3936 1.8105 14.5214 2.06139C14.6667 2.3466 14.6667 2.71997 14.6667 3.46671V8.53337C14.6667 9.28011 14.6667 9.65351 14.5214 9.93871C14.3936 10.1896 14.1896 10.3936 13.9387 10.5214C13.6535 10.6667 13.2801 10.6667 12.5334 10.6667H10.6667M3.46671 14.6667H8.53337C9.28011 14.6667 9.65351 14.6667 9.93871 14.5214C10.1896 14.3936 10.3936 14.1896 10.5214 13.9387C10.6667 13.6535 10.6667 13.2801 10.6667 12.5334V7.46671C10.6667 6.71997 10.6667 6.3466 10.5214 6.06139C10.3936 5.8105 10.1896 5.60653 9.93871 5.4787C9.65351 5.33337 9.28011 5.33337 8.53337 5.33337H3.46671C2.71997 5.33337 2.3466 5.33337 2.06139 5.4787C1.8105 5.60653 1.60653 5.8105 1.4787 6.06139C1.33337 6.3466 1.33337 6.71997 1.33337 7.46671V12.5334C1.33337 13.2801 1.33337 13.6535 1.4787 13.9387C1.60653 14.1896 1.8105 14.3936 2.06139 14.5214C2.3466 14.6667 2.71997 14.6667 3.46671 14.6667Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -239,6 +239,8 @@ Notifications_ef56 = Benachrichtigungen
|
|||||||
now_2181 = Gerade eben
|
now_2181 = Gerade eben
|
||||||
# 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 = An
|
On_f412 = An
|
||||||
|
# Column title for finding users to follow
|
||||||
|
Onboarding_4a25 = Neue Leute finden
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = E-Mail öffnen
|
Open_Email_25e9 = E-Mail öffnen
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
@@ -315,6 +317,8 @@ Searching_for___query_5d18 = Suche nach '{ $query }'
|
|||||||
See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
|
See_notes_from_your_contacts_ac16 = Notizen von deinen Kontakten ansehen
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
|
See_the_whole_nostr_universe_7694 = Sieh dir das ganze Nostr-Universum an
|
||||||
|
# Button to select all profiles in follow pack
|
||||||
|
Select_All_a319 = Alle auswählen
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = Senden
|
Send_1ea4 = Senden
|
||||||
# Column title for app settings
|
# Column title for app settings
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ Add_Hashtag_Column_ebf4 = Add Hashtag Column
|
|||||||
# Column title for adding last notes column
|
# Column title for adding last notes column
|
||||||
Add_Last_Notes_Column_bbad = Add Last Notes Column
|
Add_Last_Notes_Column_bbad = Add Last Notes Column
|
||||||
|
|
||||||
|
# Tooltip text for adding a new deck button
|
||||||
|
Add_new_deck_f2fc = Add new deck
|
||||||
|
|
||||||
# Column title for adding notifications column
|
# Column title for adding notifications column
|
||||||
Add_Notifications_Column_79f8 = Add Notifications Column
|
Add_Notifications_Column_79f8 = Add Notifications Column
|
||||||
|
|
||||||
@@ -136,6 +139,9 @@ Copy_Note_ID_6b45 = Copy Note ID
|
|||||||
# Copy the raw note data in JSON format to clipboard
|
# Copy the raw note data in JSON format to clipboard
|
||||||
Copy_Note_JSON_9e4e = Copy Note JSON
|
Copy_Note_JSON_9e4e = Copy Note JSON
|
||||||
|
|
||||||
|
# Tooltip text for copying npub to clipboard
|
||||||
|
Copy_npub_to_clipboard_c105 = Copy npub to clipboard
|
||||||
|
|
||||||
# Copy the author's public key to clipboard
|
# Copy the author's public key to clipboard
|
||||||
Copy_Pubkey_9cc4 = Copy Pubkey
|
Copy_Pubkey_9cc4 = Copy Pubkey
|
||||||
|
|
||||||
@@ -208,6 +214,9 @@ Display_name_f9d9 = Display name
|
|||||||
# Domain identification message
|
# Domain identification message
|
||||||
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
|
domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification
|
||||||
|
|
||||||
|
# Button to indicate that the user is done going through the onboarding process.
|
||||||
|
Done_50dd = Done
|
||||||
|
|
||||||
# Column title for editing deck
|
# Column title for editing deck
|
||||||
Edit_Deck_4018 = Edit Deck
|
Edit_Deck_4018 = Edit Deck
|
||||||
|
|
||||||
@@ -283,6 +292,9 @@ 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
|
Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies
|
||||||
|
|
||||||
|
# label for keys setting section
|
||||||
|
Keys_435f = Keys
|
||||||
|
|
||||||
# Label for language, Appearance settings section
|
# Label for language, Appearance settings section
|
||||||
Language_e264 = Language:
|
Language_e264 = Language:
|
||||||
|
|
||||||
@@ -310,6 +322,18 @@ Moves_this_column_to_another_position_0d4b = Moves this column to another positi
|
|||||||
# Title for the user's deck
|
# Title for the user's deck
|
||||||
My_Deck_4ac5 = My Deck
|
My_Deck_4ac5 = My Deck
|
||||||
|
|
||||||
|
# reaction from user to a note you were tagged in
|
||||||
|
name__reacted_to_a_note_you_were_tagged_in_4b62 = {$name} reacted to a note you were tagged in
|
||||||
|
|
||||||
|
# reaction from user to your note
|
||||||
|
name__reacted_to_your_note_ead9 = {$name} reacted to your note
|
||||||
|
|
||||||
|
# repost from user
|
||||||
|
name__reposted_a_note_you_were_tagged_in_1379 = {$name} reposted a note you were tagged in
|
||||||
|
|
||||||
|
# repost from user
|
||||||
|
name__reposted_your_note_1379 = {$name} reposted your note
|
||||||
|
|
||||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||||
New_to_Nostr_a2fd = New to Nostr?
|
New_to_Nostr_a2fd = New to Nostr?
|
||||||
|
|
||||||
@@ -352,6 +376,9 @@ now_2181 = now
|
|||||||
# 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 = On
|
||||||
|
|
||||||
|
# Column title for finding users to follow
|
||||||
|
Onboarding_4a25 = Onboarding
|
||||||
|
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Open Email
|
Open_Email_25e9 = Open Email
|
||||||
|
|
||||||
@@ -382,6 +409,9 @@ Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard_
|
|||||||
# Profile picture URL field label
|
# Profile picture URL field label
|
||||||
Profile_picture_81ff = Profile picture
|
Profile_picture_81ff = Profile picture
|
||||||
|
|
||||||
|
# label describing public key
|
||||||
|
PUBLIC_ACCOUNT_ID_4394 = PUBLIC ACCOUNT ID
|
||||||
|
|
||||||
# Column title for quote composition
|
# Column title for quote composition
|
||||||
Quote_475c = Quote
|
Quote_475c = Quote
|
||||||
|
|
||||||
@@ -460,12 +490,18 @@ Search_notes_42a6 = Search notes...
|
|||||||
# Search in progress message
|
# Search in progress message
|
||||||
Searching_for___query_5d18 = Searching for '{$query}'
|
Searching_for___query_5d18 = Searching for '{$query}'
|
||||||
|
|
||||||
|
# label describing secret key
|
||||||
|
SECRET_ACCOUNT_LOGIN_KEY_8440 = SECRET ACCOUNT LOGIN KEY
|
||||||
|
|
||||||
# Description for Home column
|
# Description for Home column
|
||||||
See_notes_from_your_contacts_ac16 = See notes from your contacts
|
See_notes_from_your_contacts_ac16 = See notes from your contacts
|
||||||
|
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = See the whole nostr universe
|
See_the_whole_nostr_universe_7694 = See the whole nostr universe
|
||||||
|
|
||||||
|
# Button to select all profiles in follow pack
|
||||||
|
Select_All_a319 = Select All
|
||||||
|
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = Send
|
Send_1ea4 = Send
|
||||||
|
|
||||||
@@ -603,3 +639,35 @@ Got__count__results_for___query_85fb =
|
|||||||
[one] Got {$count} result for '{$query}'
|
[one] Got {$count} result for '{$query}'
|
||||||
*[other] Got {$count} results for '{$query}'
|
*[other] Got {$count} results for '{$query}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# amount of reactions a note you were tagged in received
|
||||||
|
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
|
||||||
|
{ $count ->
|
||||||
|
[one] {$name} and {$count} other reacted to a note you were tagged in
|
||||||
|
*[other] {$name} and {$count} others reacted to a note you were tagged in
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reactions your note received
|
||||||
|
name__and__count__others_reacted_to_your_note_0f6a =
|
||||||
|
{ $count ->
|
||||||
|
[one] {$name} and {$count} other reacted to your note
|
||||||
|
*[other] {$name} and {$count} others reacted to your note
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reposts a note you were tagged in received
|
||||||
|
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
|
||||||
|
{ $count ->
|
||||||
|
[one] {$name} and {$count} other reposted a note you were tagged in
|
||||||
|
*[other] {$name} and {$count} others reposted a note you were tagged in
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reposts your note received
|
||||||
|
name__and__count__others_reposted_your_note_70a0 =
|
||||||
|
{ $count ->
|
||||||
|
[one] {$name} and {$count} other reposted your note
|
||||||
|
*[other] {$name} and {$count} others reposted your note
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ Add_Hashtag_Column_ebf4 = {"["}Àdd Hàshtàg Çólúmñ{"]"}
|
|||||||
# Column title for adding last notes column
|
# Column title for adding last notes column
|
||||||
Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"}
|
Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"}
|
||||||
|
|
||||||
|
# Tooltip text for adding a new deck button
|
||||||
|
Add_new_deck_f2fc = {"["}Àdd ñéw déçk{"]"}
|
||||||
|
|
||||||
# Column title for adding notifications column
|
# Column title for adding notifications column
|
||||||
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
|
Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"}
|
||||||
|
|
||||||
@@ -136,6 +139,9 @@ Copy_Note_ID_6b45 = {"["}Çópy Ñóté ÍD{"]"}
|
|||||||
# Copy the raw note data in JSON format to clipboard
|
# Copy the raw note data in JSON format to clipboard
|
||||||
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
|
Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"}
|
||||||
|
|
||||||
|
# Tooltip text for copying npub to clipboard
|
||||||
|
Copy_npub_to_clipboard_c105 = {"["}Çópy ñpúb tó çlípbóàrd{"]"}
|
||||||
|
|
||||||
# Copy the author's public key to clipboard
|
# Copy the author's public key to clipboard
|
||||||
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
|
Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"}
|
||||||
|
|
||||||
@@ -208,6 +214,9 @@ Display_name_f9d9 = {"["}Dísplày ñàmé{"]"}
|
|||||||
# Domain identification message
|
# Domain identification message
|
||||||
domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
|
domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"}
|
||||||
|
|
||||||
|
# Button to indicate that the user is done going through the onboarding process.
|
||||||
|
Done_50dd = {"["}Dóñé{"]"}
|
||||||
|
|
||||||
# Column title for editing deck
|
# Column title for editing deck
|
||||||
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
|
Edit_Deck_4018 = {"["}Édít Déçk{"]"}
|
||||||
|
|
||||||
@@ -283,6 +292,9 @@ k_5K_f7e6 = {"["}5K{"]"}
|
|||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
|
Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"}
|
||||||
|
|
||||||
|
# label for keys setting section
|
||||||
|
Keys_435f = {"["}Kéys{"]"}
|
||||||
|
|
||||||
# Label for language, Appearance settings section
|
# Label for language, Appearance settings section
|
||||||
Language_e264 = {"["}Làñgúàgé:{"]"}
|
Language_e264 = {"["}Làñgúàgé:{"]"}
|
||||||
|
|
||||||
@@ -310,6 +322,18 @@ Moves_this_column_to_another_position_0d4b = {"["}Móvés thís çólúmñ tó
|
|||||||
# Title for the user's deck
|
# Title for the user's deck
|
||||||
My_Deck_4ac5 = {"["}My Déçk{"]"}
|
My_Deck_4ac5 = {"["}My Déçk{"]"}
|
||||||
|
|
||||||
|
# reaction from user to a note you were tagged in
|
||||||
|
name__reacted_to_a_note_you_were_tagged_in_4b62 = {"["}{$name} réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
|
||||||
|
# reaction from user to your note
|
||||||
|
name__reacted_to_your_note_ead9 = {"["}{$name} réàçtéd tó yóúr ñóté{"]"}
|
||||||
|
|
||||||
|
# repost from user
|
||||||
|
name__reposted_a_note_you_were_tagged_in_1379 = {"["}{$name} répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
|
||||||
|
# repost from user
|
||||||
|
name__reposted_your_note_1379 = {"["}{$name} répóstéd yóúr ñóté{"]"}
|
||||||
|
|
||||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
||||||
New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"}
|
New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"}
|
||||||
|
|
||||||
@@ -352,6 +376,9 @@ now_2181 = {"["}ñów{"]"}
|
|||||||
# 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_f412 = {"["}Óñ{"]"}
|
||||||
|
|
||||||
|
# Column title for finding users to follow
|
||||||
|
Onboarding_4a25 = {"["}Óñbóàrdíñg{"]"}
|
||||||
|
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
|
Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"}
|
||||||
|
|
||||||
@@ -382,6 +409,9 @@ Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard_
|
|||||||
# Profile picture URL field label
|
# Profile picture URL field label
|
||||||
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
|
Profile_picture_81ff = {"["}Prófílé píçtúré{"]"}
|
||||||
|
|
||||||
|
# label describing public key
|
||||||
|
PUBLIC_ACCOUNT_ID_4394 = {"["}PÚBLÍÇ ÀÇÇÓÚÑT ÍD{"]"}
|
||||||
|
|
||||||
# Column title for quote composition
|
# Column title for quote composition
|
||||||
Quote_475c = {"["}Qúóté{"]"}
|
Quote_475c = {"["}Qúóté{"]"}
|
||||||
|
|
||||||
@@ -460,12 +490,18 @@ Search_notes_42a6 = {"["}Séàrçh ñótés...{"]"}
|
|||||||
# Search in progress message
|
# Search in progress message
|
||||||
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
|
Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"}
|
||||||
|
|
||||||
|
# label describing secret key
|
||||||
|
SECRET_ACCOUNT_LOGIN_KEY_8440 = {"["}SÉÇRÉT ÀÇÇÓÚÑT LÓGÍÑ KÉY{"]"}
|
||||||
|
|
||||||
# Description for Home column
|
# Description for Home column
|
||||||
See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"}
|
See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"}
|
||||||
|
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérsé{"]"}
|
See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérsé{"]"}
|
||||||
|
|
||||||
|
# Button to select all profiles in follow pack
|
||||||
|
Select_All_a319 = {"["}Séléçt Àll{"]"}
|
||||||
|
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = {"["}Séñd{"]"}
|
Send_1ea4 = {"["}Séñd{"]"}
|
||||||
|
|
||||||
@@ -603,3 +639,35 @@ Got__count__results_for___query_85fb =
|
|||||||
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
|
[one] {"["}Gót {$count} résúlt fór '{$query}'{"]"}
|
||||||
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
|
*[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# amount of reactions a note you were tagged in received
|
||||||
|
name__and__count__others_reacted_to_a_note_you_were_tagged_in_181a =
|
||||||
|
{ $count ->
|
||||||
|
[one] {"["}{$name} àñd {$count} óthér réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
*[other] {"["}{$name} àñd {$count} óthérs réàçtéd tó à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reactions your note received
|
||||||
|
name__and__count__others_reacted_to_your_note_0f6a =
|
||||||
|
{ $count ->
|
||||||
|
[one] {"["}{$name} àñd {$count} óthér réàçtéd tó yóúr ñóté{"]"}
|
||||||
|
*[other] {"["}{$name} àñd {$count} óthérs réàçtéd tó yóúr ñóté{"]"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reposts a note you were tagged in received
|
||||||
|
name__and__count__others_reposted_a_note_you_were_tagged_in_08e1 =
|
||||||
|
{ $count ->
|
||||||
|
[one] {"["}{$name} àñd {$count} óthér répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
*[other] {"["}{$name} àñd {$count} óthérs répóstéd à ñóté yóú wéré tàggéd íñ{"]"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# describing the amount of reposts your note received
|
||||||
|
name__and__count__others_reposted_your_note_70a0 =
|
||||||
|
{ $count ->
|
||||||
|
[one] {"["}{$name} àñd {$count} óthér répóstéd yóúr ñóté{"]"}
|
||||||
|
*[other] {"["}{$name} àñd {$count} óthérs répóstéd yóúr ñóté{"]"}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -237,6 +253,8 @@ Notifications_ef56 = Notifications
|
|||||||
now_2181 = maintenant
|
now_2181 = maintenant
|
||||||
# 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 = Activé
|
On_f412 = Activé
|
||||||
|
# Column title for finding users to follow
|
||||||
|
Onboarding_4a25 = Utilisateurs recommandés
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Ouvrir Email
|
Open_Email_25e9 = Ouvrir Email
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
@@ -257,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
|
||||||
@@ -309,10 +329,14 @@ 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
|
||||||
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
|
See_the_whole_nostr_universe_7694 = Voir l'ensemble de l'univers nostr
|
||||||
|
# Button to select all profiles in follow pack
|
||||||
|
Select_All_a319 = Tout sélectionner
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = Envoyer
|
Send_1ea4 = Envoyer
|
||||||
# Column title for app settings
|
# Column title for app settings
|
||||||
@@ -408,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ Notifications_ef56 = Notificações
|
|||||||
now_2181 = Agora
|
now_2181 = Agora
|
||||||
# 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 = Ligar
|
On_f412 = Ligar
|
||||||
|
# Column title for finding users to follow
|
||||||
|
Onboarding_4a25 = Interação
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Abrir E-mail
|
Open_Email_25e9 = Abrir E-mail
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
@@ -313,6 +315,8 @@ Searching_for___query_5d18 = Pesquisando por '{ $query }'
|
|||||||
See_notes_from_your_contacts_ac16 = Veja notas dos seus contatos
|
See_notes_from_your_contacts_ac16 = Veja notas dos seus contatos
|
||||||
# Description for universe column
|
# Description for universe column
|
||||||
See_the_whole_nostr_universe_7694 = Veja todo o universo Nostr
|
See_the_whole_nostr_universe_7694 = Veja todo o universo Nostr
|
||||||
|
# Button to select all profiles in follow pack
|
||||||
|
Select_All_a319 = Selecionar todos
|
||||||
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -237,6 +253,8 @@ Notifications_ef56 = Notificações
|
|||||||
now_2181 = agora
|
now_2181 = agora
|
||||||
# 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 = Ativado
|
On_f412 = Ativado
|
||||||
|
# Column title for finding users to follow
|
||||||
|
Onboarding_4a25 = Introdução
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Abrir e-mail
|
Open_Email_25e9 = Abrir e-mail
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
@@ -257,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
|
||||||
@@ -309,10 +329,14 @@ 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
|
||||||
See_the_whole_nostr_universe_7694 = Ver notas de todo o universo nostr
|
See_the_whole_nostr_universe_7694 = Ver notas de todo o universo nostr
|
||||||
|
# Button to select all profiles in follow pack
|
||||||
|
Select_All_a319 = Selecionar todos
|
||||||
# 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
|
||||||
@@ -408,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -46,7 +48,7 @@ Algorithmic_feeds_to_aid_in_note_discovery_d344 = ฟีดแบบอัลก
|
|||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = จำนวน
|
Amount_70f0 = จำนวน
|
||||||
# Label for appearance settings section
|
# Label for appearance settings section
|
||||||
Appearance_4c7f = รูปลักษณ์
|
Appearance_4c7f = ลักษณะ
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = ถาม
|
Ask_b7f4 = ถาม
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -90,11 +92,13 @@ Copy_a688 = คัดลอก
|
|||||||
# Button to copy media link to clipboard
|
# Button to copy media link to clipboard
|
||||||
Copy_Link_dc7c = คัดลอกลิงก์
|
Copy_Link_dc7c = คัดลอกลิงก์
|
||||||
# Copy the unique note identifier to clipboard
|
# Copy the unique note identifier to clipboard
|
||||||
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 = คัดลอก Pubkey
|
Copy_Pubkey_9cc4 = คัดลอก npub
|
||||||
# Copy the text content of the note to clipboard
|
# Copy the text content of the note to clipboard
|
||||||
Copy_Text_f81c = คัดลอกข้อความ
|
Copy_Text_f81c = คัดลอกข้อความ
|
||||||
# Relative time in days
|
# Relative time in days
|
||||||
@@ -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
|
||||||
@@ -164,7 +170,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 = ค้นหาผู้ใช้
|
Find_User_bd12 = ค้นหาผู้ใช้
|
||||||
# Label for font size, Appearance settings section
|
# Label for font size, Appearance settings section
|
||||||
Font_size_dd73 = Font size:
|
Font_size_dd73 = ขนาดตัวอักษร:
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = แฮชแท็ก
|
Hashtags_f8e0 = แฮชแท็ก
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
@@ -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
|
||||||
@@ -238,7 +254,9 @@ Notifications_ef56 = การแจ้งเตือน
|
|||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = เมื่อสักครู่
|
now_2181 = เมื่อสักครู่
|
||||||
# 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 = เปิด
|
||||||
|
# Column title for finding users to follow
|
||||||
|
Onboarding_4a25 = เริ่มใช้
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = เปิดอีเมล
|
Open_Email_25e9 = เปิดอีเมล
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
@@ -254,11 +272,13 @@ Please_create_a_name_for_the_deck_and_select_an_icon_0add = กรุณาต
|
|||||||
# Error message for missing deck icon
|
# Error message for missing deck icon
|
||||||
Please_select_an_icon_655b = กรุณาเลือกไอคอน
|
Please_select_an_icon_655b = กรุณาเลือกไอคอน
|
||||||
# Button label to post a note
|
# Button label to post a note
|
||||||
Post_now_8a49 = โพสต์เลย
|
Post_now_8a49 = โพสต์
|
||||||
# Instruction for copying logs
|
# Instruction for copying logs
|
||||||
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
|
||||||
@@ -292,7 +312,7 @@ Repost_this_note_8e56 = รีโพสต์โน้ตนี้
|
|||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = รีโพสต์แล้ว
|
Reposted_61c8 = รีโพสต์แล้ว
|
||||||
# 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 = รีเซ็ต
|
||||||
# Label for reset zoom level, Appearance settings section
|
# Label for reset zoom level, Appearance settings section
|
||||||
Reset_62d4 = รีเซ็ต
|
Reset_62d4 = รีเซ็ต
|
||||||
# Heading for support section
|
# Heading for support section
|
||||||
@@ -311,10 +331,14 @@ 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
|
||||||
See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ทั้งหมด
|
See_the_whole_nostr_universe_7694 = ท่องจักรวาล Nostr ทั้งหมด
|
||||||
|
# Button to select all profiles in follow pack
|
||||||
|
Select_All_a319 = เลือกทั้งหมด
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = ส่ง
|
Send_1ea4 = ส่ง
|
||||||
# Column title for app settings
|
# Column title for app settings
|
||||||
@@ -328,7 +352,7 @@ Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
|
|||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
|
Someone_else_s_Notifications_82e6 = การแจ้งเตือนของผู้อื่น
|
||||||
# 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 = เรียงการตอบกลับจากใหม่ที่สุดไปเก่าที่สุด
|
||||||
# Description for contact list column
|
# Description for contact list column
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = ดึงโน้ตล่าสุดของผู้ใช้แต่ละคนในรายชื่อผู้ติดต่อ
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = ดึงโน้ตล่าสุดของผู้ใช้แต่ละคนในรายชื่อผู้ติดต่อ
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -354,7 +378,7 @@ Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของ
|
|||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
|
Subscribe_to_someone_s_notes_b3c8 = ติดตามโน้ตของผู้อื่น
|
||||||
# Support email address
|
# Support email address
|
||||||
Support_email_44d9 = Support email:
|
Support_email_44d9 = อีเมลฝ่ายสนับสนุน:
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
|
Switch_to_dark_mode_4dec = เปลี่ยนเป็นโหมดมืด
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -376,7 +400,7 @@ Universe_ffaa = จักรวาล
|
|||||||
# Checkbox label for using wallet only for current account
|
# Checkbox label for using wallet only for current account
|
||||||
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
|
Use_this_wallet_for_the_current_account_only_61dc = ใช้วอลเล็ตนี้สำหรับบัญชีปัจจุบันเท่านั้น
|
||||||
# Username and domain identification message
|
# Username and domain identification message
|
||||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" ที่ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
|
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" @ "{ $domain }" จะถูกใช้สำหรับการระบุตัวตน
|
||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = ชื่อผู้ใช้
|
Username_daa7 = ชื่อผู้ใช้
|
||||||
# Label for view folder button, Storage settings section
|
# Label for view folder button, Storage settings section
|
||||||
@@ -388,7 +412,7 @@ We_recommend_short_names_083e = เราแนะนำให้ใช้ชื
|
|||||||
# Profile website field label
|
# Profile website field label
|
||||||
Website_7980 = เว็บไซต์
|
Website_7980 = เว็บไซต์
|
||||||
# Placeholder for note input field
|
# Placeholder for note input field
|
||||||
Write_a_banger_note_here_bad2 = เขียนโน้ตปังๆ ที่นี่...
|
Write_a_banger_note_here_bad2 = เขียนโน้ตที่นี่...
|
||||||
# Placeholder text for key input field
|
# Placeholder text for key input field
|
||||||
Your_key_here_81bd = ใส่คีย์ของคุณที่นี่...
|
Your_key_here_81bd = ใส่คีย์ของคุณที่นี่...
|
||||||
# Title for your notes column
|
# Title for your notes column
|
||||||
@@ -410,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 } คน รีโพสต์โน้ตของคุณ
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,""]"#,
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ md5 = { workspace = true }
|
|||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
indexmap = {workspace = true}
|
||||||
|
rand = {workspace = true}
|
||||||
|
crossbeam-channel = "0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
@@ -58,6 +61,7 @@ tokio = { workspace = true }
|
|||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
jni = { workspace = true }
|
jni = { workspace = true }
|
||||||
android-activity = { workspace = true }
|
android-activity = { workspace = true }
|
||||||
|
ndk-context = "0.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
puffin = ["puffin_egui", "dep:puffin"]
|
puffin = ["puffin_egui", "dep:puffin"]
|
||||||
|
|||||||
@@ -267,6 +267,11 @@ impl Accounts {
|
|||||||
Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
|
Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mute(&self) -> Box<Arc<crate::Muted>> {
|
||||||
|
let account_data = self.get_selected_account_data();
|
||||||
|
Box::new(Arc::clone(&account_data.muted.muted))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
|
pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
|
||||||
let data = &self.get_selected_account().data;
|
let data = &self.get_selected_account().data;
|
||||||
// send the active account's relay list subscription
|
// send the active account's relay list subscription
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,15 +33,26 @@ pub enum ZapError {
|
|||||||
#[error("invalid lud16")]
|
#[error("invalid lud16")]
|
||||||
InvalidLud16(String),
|
InvalidLud16(String),
|
||||||
#[error("invalid endpoint response")]
|
#[error("invalid endpoint response")]
|
||||||
EndpointError(String),
|
EndpointError(EndpointError),
|
||||||
#[error("bech encoding/decoding error")]
|
#[error("bech encoding/decoding error")]
|
||||||
Bech(String),
|
Bech(String),
|
||||||
#[error("serialization/deserialization problem")]
|
#[error("serialization/deserialization problem")]
|
||||||
Serialization(String),
|
Serialization(String),
|
||||||
#[error("nwc error")]
|
#[error("nwc error")]
|
||||||
NWC(String),
|
NWC(String),
|
||||||
|
#[error("ndb error")]
|
||||||
|
Ndb(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ZapError {
|
||||||
|
pub fn endpoint_error(error: String) -> ZapError {
|
||||||
|
ZapError::EndpointError(EndpointError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EndpointError(pub String);
|
||||||
|
|
||||||
impl From<String> for Error {
|
impl From<String> for Error {
|
||||||
fn from(s: String) -> Self {
|
fn from(s: String) -> Self {
|
||||||
Error::Generic(s)
|
Error::Generic(s)
|
||||||
|
|||||||
+118
-52
@@ -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)
|
||||||
@@ -183,21 +177,24 @@ pub fn should_since_optimize(limit: u64, num_notes: usize) -> bool {
|
|||||||
limit as usize <= num_notes
|
limit as usize <= num_notes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn since_optimize_filter_with(filter: Filter, notes: &[NoteRef], since_gap: u64) -> Filter {
|
pub fn since_optimize_filter_with(
|
||||||
|
filter: Filter,
|
||||||
|
latest_note: Option<&NoteRef>,
|
||||||
|
since_gap: u64,
|
||||||
|
) -> Filter {
|
||||||
// Get the latest entry in the events
|
// Get the latest entry in the events
|
||||||
if notes.is_empty() {
|
let Some(latest) = latest_note else {
|
||||||
return filter;
|
return filter;
|
||||||
}
|
};
|
||||||
|
|
||||||
// get the latest note
|
// get the latest note
|
||||||
let latest = notes[0];
|
|
||||||
let since = latest.created_at - since_gap;
|
let since = latest.created_at - since_gap;
|
||||||
|
|
||||||
filter.since_mut(since)
|
filter.since_mut(since)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn since_optimize_filter(filter: Filter, notes: &[NoteRef]) -> Filter {
|
pub fn since_optimize_filter(filter: Filter, latest: Option<&NoteRef>) -> Filter {
|
||||||
since_optimize_filter_with(filter, notes, 60)
|
since_optimize_filter_with(filter, latest, 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_limit() -> u64 {
|
pub fn default_limit() -> u64 {
|
||||||
@@ -216,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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,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,
|
||||||
|
}],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,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
|
||||||
if filters.len() == 15 {
|
const LIMIT: usize = 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() {
|
|
||||||
author
|
|
||||||
} else {
|
|
||||||
continue;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,4 +80,8 @@ impl Muted {
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_pk_muted(&self, pk: &[u8; 32]) -> bool {
|
||||||
|
self.pubkeys.contains(pk)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use enostr::{Pubkey, RelayPool};
|
use enostr::{Pubkey, RelayPool};
|
||||||
|
use indexmap::IndexMap;
|
||||||
use nostrdb::{Filter, Ndb, Note, Transaction};
|
use nostrdb::{Filter, Ndb, Note, Transaction};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ use crate::{UnifiedSubscription, UnknownIds};
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Nip51SetCache {
|
pub struct Nip51SetCache {
|
||||||
pub sub: UnifiedSubscription,
|
pub sub: UnifiedSubscription,
|
||||||
cached_notes: HashMap<PackId, Nip51Set>,
|
cached_notes: IndexMap<PackId, Nip51Set>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type PackId = String;
|
type PackId = String;
|
||||||
@@ -24,7 +23,7 @@ impl Nip51SetCache {
|
|||||||
nip51_set_filter: Vec<Filter>,
|
nip51_set_filter: Vec<Filter>,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let subid = Uuid::new_v4().to_string();
|
let subid = Uuid::new_v4().to_string();
|
||||||
let mut cached_notes = HashMap::default();
|
let mut cached_notes = IndexMap::default();
|
||||||
|
|
||||||
let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) {
|
let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) {
|
||||||
Some(results.into_iter().map(|r| r.note).collect())
|
Some(results.into_iter().map(|r| r.note).collect())
|
||||||
@@ -73,11 +72,23 @@ impl Nip51SetCache {
|
|||||||
pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> {
|
pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> {
|
||||||
self.cached_notes.values()
|
self.cached_notes.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.cached_notes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.cached_notes.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn at_index(&self, index: usize) -> Option<&Nip51Set> {
|
||||||
|
self.cached_notes.get_index(index).map(|(_, s)| s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add(
|
fn add(
|
||||||
notes: Vec<Note>,
|
notes: Vec<Note>,
|
||||||
cache: &mut HashMap<PackId, Nip51Set>,
|
cache: &mut IndexMap<PackId, Nip51Set>,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
unknown_ids: &mut UnknownIds,
|
unknown_ids: &mut UnknownIds,
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
use crate::platform::{file::emit_selected_file, SelectedMedia};
|
||||||
|
use jni::{
|
||||||
|
objects::{JByteArray, JClass, JObject, JObjectArray, JString},
|
||||||
|
JNIEnv,
|
||||||
|
};
|
||||||
use std::sync::atomic::{AtomicI32, Ordering};
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
use tracing::debug;
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
pub fn get_jvm() -> jni::JavaVM {
|
||||||
|
unsafe { jni::JavaVM::from_raw(ndk_context::android_context().vm().cast()) }.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
// Thread-safe static global
|
// Thread-safe static global
|
||||||
static KEYBOARD_HEIGHT: AtomicI32 = AtomicI32::new(0);
|
static KEYBOARD_HEIGHT: AtomicI32 = AtomicI32::new(0);
|
||||||
@@ -24,3 +33,80 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
|
|||||||
pub fn virtual_keyboard_height() -> i32 {
|
pub fn virtual_keyboard_height() -> i32 {
|
||||||
KEYBOARD_HEIGHT.load(Ordering::SeqCst)
|
KEYBOARD_HEIGHT.load(Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_com_damus_notedeck_MainActivity_nativeOnFilePickedFailed(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
juri: JString,
|
||||||
|
je: JString,
|
||||||
|
) {
|
||||||
|
let _uri: String = env.get_string(&juri).unwrap().into();
|
||||||
|
let _error: String = env.get_string(&je).unwrap().into();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_com_damus_notedeck_MainActivity_nativeOnFilePickedWithContent(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
// [display_name, size, mime_type]
|
||||||
|
juri_info: JObjectArray,
|
||||||
|
jcontent: JByteArray,
|
||||||
|
) {
|
||||||
|
debug!("File picked with content");
|
||||||
|
|
||||||
|
let display_name: Option<String> = {
|
||||||
|
let obj = env.get_object_array_element(&juri_info, 0).unwrap();
|
||||||
|
if obj.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(env.get_string(&JString::from(obj)).unwrap().into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(display_name) = display_name {
|
||||||
|
let length = env.get_array_length(&jcontent).unwrap() as usize;
|
||||||
|
let mut content: Vec<i8> = vec![0; length];
|
||||||
|
env.get_byte_array_region(&jcontent, 0, &mut content)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
debug!("selected file: {display_name:?} ({length:?} bytes)",);
|
||||||
|
|
||||||
|
emit_selected_file(SelectedMedia::from_bytes(
|
||||||
|
display_name,
|
||||||
|
content.into_iter().map(|b| b as u8).collect(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
error!("Received null file name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_open_file_picker() {
|
||||||
|
match open_file_picker() {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("File picker opened successfully");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to open file picker: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_file_picker() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Get the Java VM from AndroidApp
|
||||||
|
let vm = get_jvm();
|
||||||
|
|
||||||
|
// Attach current thread to get JNI environment
|
||||||
|
let mut env = vm.attach_current_thread()?;
|
||||||
|
|
||||||
|
let context = unsafe { JObject::from_raw(ndk_context::android_context().context().cast()) };
|
||||||
|
// Call the openFilePicker method on the MainActivity
|
||||||
|
env.call_method(
|
||||||
|
context,
|
||||||
|
"openFilePicker",
|
||||||
|
"()V", // Method signature: no parameters, void return
|
||||||
|
&[], // No arguments
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::{path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::{Error, SupportedMimeType};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MediaFrom {
|
||||||
|
PathBuf(PathBuf),
|
||||||
|
Memory(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SelectedMedia {
|
||||||
|
pub from: MediaFrom,
|
||||||
|
pub file_name: String,
|
||||||
|
pub media_type: SupportedMimeType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectedMedia {
|
||||||
|
pub fn from_path(path: PathBuf) -> Result<Self, Error> {
|
||||||
|
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
|
||||||
|
let media_type = SupportedMimeType::from_extension(ex)?;
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or(&format!("file.{ex}"))
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
Ok(SelectedMedia {
|
||||||
|
from: MediaFrom::PathBuf(path),
|
||||||
|
file_name,
|
||||||
|
media_type,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(Error::Generic(format!(
|
||||||
|
"{path:?} does not have an extension"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(file_name: String, content: Vec<u8>) -> Result<Self, Error> {
|
||||||
|
if let Some(ex) = PathBuf::from_str(&file_name)
|
||||||
|
.unwrap()
|
||||||
|
.extension()
|
||||||
|
.and_then(|f| f.to_str())
|
||||||
|
{
|
||||||
|
let media_type = SupportedMimeType::from_extension(ex)?;
|
||||||
|
|
||||||
|
Ok(SelectedMedia {
|
||||||
|
from: MediaFrom::Memory(content),
|
||||||
|
file_name,
|
||||||
|
media_type,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(Error::Generic(format!(
|
||||||
|
"{file_name:?} does not have an extension"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SelectedMediaChannel {
|
||||||
|
sender: Sender<Result<SelectedMedia, Error>>,
|
||||||
|
receiver: Receiver<Result<SelectedMedia, Error>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SelectedMediaChannel {
|
||||||
|
fn default() -> Self {
|
||||||
|
let (sender, receiver) = unbounded();
|
||||||
|
Self { sender, receiver }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectedMediaChannel {
|
||||||
|
pub fn new_selected_file(&self, media: Result<SelectedMedia, Error>) {
|
||||||
|
let _ = self.sender.send(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_receive(&self) -> Option<Result<SelectedMedia, Error>> {
|
||||||
|
self.receiver.try_recv().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive(&self) -> Option<Result<SelectedMedia, Error>> {
|
||||||
|
self.receiver.recv().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static SELECTED_MEDIA_CHANNEL: Lazy<SelectedMediaChannel> =
|
||||||
|
Lazy::new(SelectedMediaChannel::default);
|
||||||
|
|
||||||
|
pub fn emit_selected_file(media: Result<SelectedMedia, Error>) {
|
||||||
|
SELECTED_MEDIA_CHANNEL.new_selected_file(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_next_selected_file() -> Option<Result<SelectedMedia, Error>> {
|
||||||
|
SELECTED_MEDIA_CHANNEL.try_receive()
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
|
use crate::{platform::file::SelectedMedia, Error};
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub mod android;
|
pub mod android;
|
||||||
|
pub mod file;
|
||||||
|
|
||||||
|
pub fn get_next_selected_file() -> Option<Result<SelectedMedia, Error>> {
|
||||||
|
file::get_next_selected_file()
|
||||||
|
}
|
||||||
|
|
||||||
const VIRT_HEIGHT: i32 = 400;
|
const VIRT_HEIGHT: i32 = 400;
|
||||||
|
|
||||||
|
|||||||
+57
-55
@@ -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 {
|
||||||
|
|||||||
@@ -195,13 +195,13 @@ impl UnknownIds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &Pubkey) {
|
pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &[u8; 32]) {
|
||||||
// we already have this profile, skip
|
// we already have this profile, skip
|
||||||
if ndb.get_profile_by_pubkey(txn, pubkey).is_ok() {
|
if ndb.get_profile_by_pubkey(txn, pubkey).is_ok() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let unknown_id = UnknownId::Pubkey(*pubkey);
|
let unknown_id = UnknownId::Pubkey(Pubkey::new(*pubkey));
|
||||||
if self.ids.contains_key(&unknown_id) {
|
if self.ids.contains_key(&unknown_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,7 +238,9 @@ impl SupportedMimeType {
|
|||||||
{
|
{
|
||||||
Ok(Self { mime })
|
Ok(Self { mime })
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Generic("Unsupported mime type".to_owned()))
|
Err(Error::Generic(
|
||||||
|
format!("{extension} Unsupported mime type",),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use enostr::{NoteId, Pubkey};
|
use enostr::{NoteId, Pubkey};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use nwc::nostr::nips::nip47::PayInvoiceResponse;
|
use nwc::nostr::nips::nip47::PayInvoiceResponse;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
use tokio::task::JoinError;
|
use tokio::task::JoinError;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{get_wallet_for, Accounts, GlobalWallet, ZapError};
|
use crate::{
|
||||||
|
get_wallet_for,
|
||||||
use super::{
|
zaps::{
|
||||||
networking::{fetch_invoice_lnurl, fetch_invoice_lud16, FetchedInvoice, FetchingInvoice},
|
get_users_zap_address,
|
||||||
zap::Zap,
|
networking::{fetch_invoice_promise, FetchedInvoiceResponse, LNUrlPayResponse, PayEntry},
|
||||||
|
},
|
||||||
|
Accounts, GlobalWallet, ZapError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{networking::FetchingInvoice, zap::Zap};
|
||||||
|
|
||||||
type ZapId = u32;
|
type ZapId = u32;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -23,11 +30,31 @@ pub struct Zaps {
|
|||||||
zaps: std::collections::HashMap<ZapId, ZapState>,
|
zaps: std::collections::HashMap<ZapId, ZapState>,
|
||||||
in_flight: Vec<ZapPromise>,
|
in_flight: Vec<ZapPromise>,
|
||||||
events: Vec<EventResponse>,
|
events: Vec<EventResponse>,
|
||||||
|
|
||||||
|
pay_cache: PayCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cache to hold LNURL payRequest responses from the desired LNURL endpoint
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PayCache {
|
||||||
|
// endpoint URL to response
|
||||||
|
pub pay_responses: HashMap<Url, LNUrlPayResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PayCache {
|
||||||
|
pub fn get_response(&self, url: &Url) -> Option<&LNUrlPayResponse> {
|
||||||
|
self.pay_responses.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, entry: PayEntry) {
|
||||||
|
self.pay_responses.insert(entry.url, entry.response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_event(
|
fn process_event(
|
||||||
id: ZapId,
|
id: ZapId,
|
||||||
event: ZapEvent,
|
event: ZapEvent,
|
||||||
|
cache: &PayCache,
|
||||||
accounts: &mut Accounts,
|
accounts: &mut Accounts,
|
||||||
global_wallet: &mut GlobalWallet,
|
global_wallet: &mut GlobalWallet,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
@@ -37,7 +64,7 @@ fn process_event(
|
|||||||
ZapEvent::FetchInvoice {
|
ZapEvent::FetchInvoice {
|
||||||
zap_ctx,
|
zap_ctx,
|
||||||
sender_relays,
|
sender_relays,
|
||||||
} => process_new_zap_event(zap_ctx, accounts, ndb, txn, sender_relays),
|
} => process_new_zap_event(cache, zap_ctx, accounts, ndb, txn, sender_relays),
|
||||||
ZapEvent::SendNWC {
|
ZapEvent::SendNWC {
|
||||||
zap_ctx,
|
zap_ctx,
|
||||||
req_noteid,
|
req_noteid,
|
||||||
@@ -74,6 +101,7 @@ fn process_event(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn process_new_zap_event(
|
fn process_new_zap_event(
|
||||||
|
cache: &PayCache,
|
||||||
zap_ctx: ZapCtx,
|
zap_ctx: ZapCtx,
|
||||||
accounts: &Accounts,
|
accounts: &Accounts,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
@@ -96,7 +124,8 @@ fn process_new_zap_event(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let id = zap_ctx.id;
|
let id = zap_ctx.id;
|
||||||
let promise = send_note_zap(
|
let m_promise = send_note_zap(
|
||||||
|
cache,
|
||||||
ndb,
|
ndb,
|
||||||
txn,
|
txn,
|
||||||
note_target,
|
note_target,
|
||||||
@@ -106,55 +135,41 @@ fn process_new_zap_event(
|
|||||||
)
|
)
|
||||||
.map(|promise| ZapPromise::FetchingInvoice {
|
.map(|promise| ZapPromise::FetchingInvoice {
|
||||||
ctx: zap_ctx,
|
ctx: zap_ctx,
|
||||||
promise,
|
promise: Box::new(promise),
|
||||||
});
|
});
|
||||||
let Some(promise) = promise else {
|
|
||||||
|
let promise = match m_promise {
|
||||||
|
Ok(promise) => promise,
|
||||||
|
Err(e) => {
|
||||||
return NextState::Event(EventResponse {
|
return NextState::Event(EventResponse {
|
||||||
id,
|
id,
|
||||||
event: Err(ZappingError::InvalidZapAddress),
|
event: Err(ZappingError::InvoiceFetchFailed(e)),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
NextState::Transition(promise)
|
NextState::Transition(promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_note_zap(
|
fn send_note_zap(
|
||||||
|
cache: &PayCache,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
note_target: NoteZapTargetOwned,
|
note_target: NoteZapTargetOwned,
|
||||||
msats: u64,
|
msats: u64,
|
||||||
nsec: &[u8; 32],
|
nsec: &[u8; 32],
|
||||||
relays: Vec<String>,
|
relays: Vec<String>,
|
||||||
) -> Option<FetchingInvoice> {
|
) -> Result<FetchingInvoice, ZapError> {
|
||||||
let address = get_users_zap_endpoint(txn, ndb, ¬e_target.zap_recipient)?;
|
let address = get_users_zap_address(txn, ndb, ¬e_target.zap_recipient)?;
|
||||||
|
|
||||||
let promise = match address {
|
fetch_invoice_promise(
|
||||||
ZapAddress::Lud16(s) => {
|
cache,
|
||||||
fetch_invoice_lud16(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays)
|
address,
|
||||||
}
|
msats,
|
||||||
ZapAddress::Lud06(s) => {
|
*nsec,
|
||||||
fetch_invoice_lnurl(s, msats, *nsec, ZapTargetOwned::Note(note_target), relays)
|
ZapTargetOwned::Note(note_target),
|
||||||
}
|
relays,
|
||||||
};
|
)
|
||||||
Some(promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ZapAddress {
|
|
||||||
Lud16(String),
|
|
||||||
Lud06(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_users_zap_endpoint(txn: &Transaction, ndb: &Ndb, receiver: &Pubkey) -> Option<ZapAddress> {
|
|
||||||
let profile = ndb
|
|
||||||
.get_profile_by_pubkey(txn, receiver.bytes())
|
|
||||||
.ok()?
|
|
||||||
.record()
|
|
||||||
.profile()?;
|
|
||||||
|
|
||||||
profile
|
|
||||||
.lud06()
|
|
||||||
.map(|l| ZapAddress::Lud06(l.to_string()))
|
|
||||||
.or(profile.lud16().map(|l| ZapAddress::Lud16(l.to_string())))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_get_promise_response(
|
fn try_get_promise_response(
|
||||||
@@ -169,7 +184,7 @@ fn try_get_promise_response(
|
|||||||
|
|
||||||
match promise {
|
match promise {
|
||||||
ZapPromise::FetchingInvoice { ctx, promise } => {
|
ZapPromise::FetchingInvoice { ctx, promise } => {
|
||||||
let result = promise.block_and_take();
|
let result = Box::new(promise.block_and_take());
|
||||||
|
|
||||||
Some(PromiseResponse::FetchingInvoice { ctx, result })
|
Some(PromiseResponse::FetchingInvoice { ctx, result })
|
||||||
}
|
}
|
||||||
@@ -272,6 +287,16 @@ impl Zaps {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let PromiseResponse::FetchingInvoice { ctx: _, result } = &resp {
|
||||||
|
if let Ok(resp) = &**result {
|
||||||
|
if let Some(entry) = &resp.pay_entry {
|
||||||
|
let url = &entry.url;
|
||||||
|
tracing::info!("inserting {url} in pay cache");
|
||||||
|
self.pay_cache.insert(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.events.push(resp.take_as_event_response());
|
self.events.push(resp.take_as_event_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +311,15 @@ impl Zaps {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let txn = nostrdb::Transaction::new(ndb).expect("txn");
|
let txn = nostrdb::Transaction::new(ndb).expect("txn");
|
||||||
match process_event(event_resp.id, event, accounts, global_wallet, ndb, &txn) {
|
match process_event(
|
||||||
|
event_resp.id,
|
||||||
|
event,
|
||||||
|
&self.pay_cache,
|
||||||
|
accounts,
|
||||||
|
global_wallet,
|
||||||
|
ndb,
|
||||||
|
&txn,
|
||||||
|
) {
|
||||||
NextState::Event(event_resp) => {
|
NextState::Event(event_resp) => {
|
||||||
self.zaps
|
self.zaps
|
||||||
.insert(event_resp.id, ZapState::Pending(event_resp.event));
|
.insert(event_resp.id, ZapState::Pending(event_resp.event));
|
||||||
@@ -483,7 +516,7 @@ impl std::fmt::Display for ZappingError {
|
|||||||
enum ZapPromise {
|
enum ZapPromise {
|
||||||
FetchingInvoice {
|
FetchingInvoice {
|
||||||
ctx: ZapCtx,
|
ctx: ZapCtx,
|
||||||
promise: Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>,
|
promise: Box<Promise<Result<FetchedInvoiceResponse, JoinError>>>,
|
||||||
},
|
},
|
||||||
SendingNWCInvoice {
|
SendingNWCInvoice {
|
||||||
ctx: SendingNWCInvoiceContext,
|
ctx: SendingNWCInvoiceContext,
|
||||||
@@ -494,7 +527,7 @@ enum ZapPromise {
|
|||||||
enum PromiseResponse {
|
enum PromiseResponse {
|
||||||
FetchingInvoice {
|
FetchingInvoice {
|
||||||
ctx: ZapCtx,
|
ctx: ZapCtx,
|
||||||
result: Result<Result<FetchedInvoice, ZapError>, JoinError>,
|
result: Box<Result<FetchedInvoiceResponse, JoinError>>,
|
||||||
},
|
},
|
||||||
SendingNWCInvoice {
|
SendingNWCInvoice {
|
||||||
ctx: SendingNWCInvoiceContext,
|
ctx: SendingNWCInvoiceContext,
|
||||||
@@ -507,8 +540,8 @@ impl PromiseResponse {
|
|||||||
match self {
|
match self {
|
||||||
PromiseResponse::FetchingInvoice { ctx, result } => {
|
PromiseResponse::FetchingInvoice { ctx, result } => {
|
||||||
let id = ctx.id;
|
let id = ctx.id;
|
||||||
let event = match result {
|
let event = match *result {
|
||||||
Ok(r) => match r {
|
Ok(r) => match r.invoice {
|
||||||
Ok(invoice) => Ok(ZapEvent::SendNWC {
|
Ok(invoice) => Ok(ZapEvent::SendNWC {
|
||||||
zap_ctx: ctx,
|
zap_ctx: ctx,
|
||||||
req_noteid: invoice.request_noteid,
|
req_noteid: invoice.request_noteid,
|
||||||
|
|||||||
@@ -11,3 +11,39 @@ pub use default_zap::{
|
|||||||
get_current_default_msats, DefaultZapError, DefaultZapMsats, PendingDefaultZapState,
|
get_current_default_msats, DefaultZapError, DefaultZapMsats, PendingDefaultZapState,
|
||||||
UserZapMsats,
|
UserZapMsats,
|
||||||
};
|
};
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::{Ndb, Transaction};
|
||||||
|
|
||||||
|
use crate::ZapError;
|
||||||
|
|
||||||
|
pub enum ZapAddress {
|
||||||
|
Lud16(String),
|
||||||
|
Lud06(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_users_zap_address(
|
||||||
|
txn: &Transaction,
|
||||||
|
ndb: &Ndb,
|
||||||
|
receiver: &Pubkey,
|
||||||
|
) -> Result<ZapAddress, ZapError> {
|
||||||
|
let Some(profile) = ndb
|
||||||
|
.get_profile_by_pubkey(txn, receiver.bytes())
|
||||||
|
.map_err(|e| ZapError::Ndb(e.to_string()))?
|
||||||
|
.record()
|
||||||
|
.profile()
|
||||||
|
else {
|
||||||
|
return Err(ZapError::Ndb(format!("No profile for {receiver}")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(address) = profile
|
||||||
|
.lud06()
|
||||||
|
.map(|l| ZapAddress::Lud06(l.to_string()))
|
||||||
|
.or(profile.lud16().map(|l| ZapAddress::Lud16(l.to_string())))
|
||||||
|
else {
|
||||||
|
return Err(ZapError::Ndb(format!(
|
||||||
|
"profile for {receiver} doesn't have lud06 or lud16"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(address)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use crate::{zaps::ZapTargetOwned, ZapError};
|
use crate::{
|
||||||
use enostr::NoteId;
|
error::EndpointError,
|
||||||
|
zaps::{cache::PayCache, ZapAddress, ZapTargetOwned},
|
||||||
|
ZapError,
|
||||||
|
};
|
||||||
|
use enostr::{NoteId, Pubkey};
|
||||||
use nostrdb::NoteBuilder;
|
use nostrdb::NoteBuilder;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -11,15 +15,20 @@ pub struct FetchedInvoice {
|
|||||||
pub request_noteid: NoteId, // note id of kind 9734 request
|
pub request_noteid: NoteId, // note id of kind 9734 request
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FetchingInvoice = Promise<Result<Result<FetchedInvoice, ZapError>, JoinError>>;
|
pub struct FetchedInvoiceResponse {
|
||||||
|
pub invoice: Result<FetchedInvoice, ZapError>,
|
||||||
|
pub pay_entry: Option<PayEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayRequest, ZapError> {
|
pub type FetchingInvoice = Promise<Result<FetchedInvoiceResponse, JoinError>>;
|
||||||
|
|
||||||
|
async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayResponseRaw, ZapError> {
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
|
|
||||||
let on_done = move |response: Result<ehttp::Response, String>| {
|
let on_done = move |response: Result<ehttp::Response, String>| {
|
||||||
let handle = response.map_err(ZapError::EndpointError).and_then(|resp| {
|
let handle = response.map_err(ZapError::endpoint_error).and_then(|resp| {
|
||||||
if !resp.ok {
|
if !resp.ok {
|
||||||
return Err(ZapError::EndpointError(format!(
|
return Err(ZapError::endpoint_error(format!(
|
||||||
"bad http response: {}",
|
"bad http response: {}",
|
||||||
resp.status_text
|
resp.status_text
|
||||||
)));
|
)));
|
||||||
@@ -36,20 +45,9 @@ async fn fetch_pay_req_async(url: &Url) -> Result<LNUrlPayRequest, ZapError> {
|
|||||||
tokio::task::block_in_place(|| promise.block_and_take())
|
tokio::task::block_in_place(|| promise.block_and_take())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_pay_req_from_lud16(lud16: &str) -> Result<LNUrlPayRequest, ZapError> {
|
|
||||||
let url = match generate_endpoint_url(lud16) {
|
|
||||||
Ok(url) => url,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch_pay_req_async(&url).await
|
|
||||||
}
|
|
||||||
|
|
||||||
static HRP_LNURL: bech32::Hrp = bech32::Hrp::parse_unchecked("lnurl");
|
static HRP_LNURL: bech32::Hrp = bech32::Hrp::parse_unchecked("lnurl");
|
||||||
|
|
||||||
fn lud16_to_lnurl(lud16: &str) -> Result<String, ZapError> {
|
fn endpoint_url_to_lnurl(endpoint_url: &Url) -> Result<String, ZapError> {
|
||||||
let endpoint_url = generate_endpoint_url(lud16)?;
|
|
||||||
|
|
||||||
let url_str = endpoint_url.to_string();
|
let url_str = endpoint_url.to_string();
|
||||||
let data = url_str.as_bytes();
|
let data = url_str.as_bytes();
|
||||||
|
|
||||||
@@ -100,7 +98,7 @@ fn make_kind_9734<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct LNUrlPayRequest {
|
pub struct LNUrlPayResponseRaw {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[serde(rename = "allowsNostr")]
|
#[serde(rename = "allowsNostr")]
|
||||||
allow_nostr: bool,
|
allow_nostr: bool,
|
||||||
@@ -121,57 +119,117 @@ pub struct LNUrlPayRequest {
|
|||||||
max_sendable: u64,
|
max_sendable: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<LNUrlPayResponseRaw> for LNUrlPayResponse {
|
||||||
|
fn from(value: LNUrlPayResponseRaw) -> Self {
|
||||||
|
let nostr_pubkey = Pubkey::from_hex(&value.nostr_pubkey)
|
||||||
|
.map_err(|e: enostr::Error| EndpointError(e.to_string()));
|
||||||
|
|
||||||
|
let callback_url = Url::parse(&value.callback_url)
|
||||||
|
.map_err(|e| EndpointError(format!("invalid callback url: {e}")));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
allow_nostr: value.allow_nostr,
|
||||||
|
nostr_pubkey,
|
||||||
|
callback_url,
|
||||||
|
min_sendable: value.min_sendable,
|
||||||
|
max_sendable: value.max_sendable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LNUrlPayResponse {
|
||||||
|
pub allow_nostr: bool,
|
||||||
|
pub nostr_pubkey: Result<Pubkey, EndpointError>,
|
||||||
|
pub callback_url: Result<Url, EndpointError>,
|
||||||
|
pub min_sendable: u64,
|
||||||
|
pub max_sendable: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PayEntry {
|
||||||
|
pub url: Url,
|
||||||
|
pub response: LNUrlPayResponse,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct LNInvoice {
|
struct LNInvoice {
|
||||||
#[serde(rename = "pr")]
|
#[serde(rename = "pr")]
|
||||||
invoice: String,
|
invoice: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn endpoint_query_for_invoice<'a>(
|
fn endpoint_query_for_invoice(
|
||||||
endpoint_base_url: &'a mut Url,
|
endpoint_base_url: &Url,
|
||||||
msats: u64,
|
msats: u64,
|
||||||
lnurl: &str,
|
lnurl: &str,
|
||||||
note: nostrdb::Note,
|
note: nostrdb::Note,
|
||||||
) -> Result<&'a Url, ZapError> {
|
) -> Result<Url, ZapError> {
|
||||||
|
let mut new_url = endpoint_base_url.clone();
|
||||||
let nostr = note
|
let nostr = note
|
||||||
.json()
|
.json()
|
||||||
.map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?;
|
.map_err(|e| ZapError::Serialization(format!("failed note to json: {e}")))?;
|
||||||
|
|
||||||
Ok(endpoint_base_url
|
new_url
|
||||||
.query_pairs_mut()
|
.query_pairs_mut()
|
||||||
.append_pair("amount", &msats.to_string())
|
.append_pair("amount", &msats.to_string())
|
||||||
.append_pair("lnurl", lnurl)
|
.append_pair("lnurl", lnurl)
|
||||||
.append_pair("nostr", &nostr)
|
.append_pair("nostr", &nostr)
|
||||||
.finish())
|
.finish();
|
||||||
|
|
||||||
|
Ok(new_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_invoice_lud16(
|
pub fn fetch_invoice_promise(
|
||||||
lud16: String,
|
cache: &PayCache,
|
||||||
|
zap_address: ZapAddress,
|
||||||
msats: u64,
|
msats: u64,
|
||||||
sender_nsec: [u8; 32],
|
sender_nsec: [u8; 32],
|
||||||
target: ZapTargetOwned,
|
target: ZapTargetOwned,
|
||||||
relays: Vec<String>,
|
relays: Vec<String>,
|
||||||
) -> FetchingInvoice {
|
) -> Result<FetchingInvoice, ZapError> {
|
||||||
Promise::spawn_async(tokio::spawn(async move {
|
let (url, lnurl) = match zap_address {
|
||||||
fetch_invoice_lud16_async(&lud16, msats, &sender_nsec, target, relays).await
|
ZapAddress::Lud16(lud16) => {
|
||||||
}))
|
let url = generate_endpoint_url(&lud16)?;
|
||||||
}
|
let lnurl = endpoint_url_to_lnurl(&url)?;
|
||||||
|
(url, lnurl)
|
||||||
pub fn fetch_invoice_lnurl(
|
}
|
||||||
lnurl: String,
|
ZapAddress::Lud06(lnurl) => (convert_lnurl_to_endpoint_url(&lnurl)?, lnurl),
|
||||||
msats: u64,
|
|
||||||
sender_nsec: [u8; 32],
|
|
||||||
target: ZapTargetOwned,
|
|
||||||
relays: Vec<String>,
|
|
||||||
) -> FetchingInvoice {
|
|
||||||
Promise::spawn_async(tokio::spawn(async move {
|
|
||||||
let pay_req = match fetch_pay_req_from_lnurl_async(&lnurl).await {
|
|
||||||
Ok(req) => req,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, &sender_nsec, relays, target).await
|
match cache.get_response(&url) {
|
||||||
}))
|
Some(endpoint_resp) => {
|
||||||
|
tracing::info!("Using existing endpoint response for {url}");
|
||||||
|
let response = endpoint_resp.clone();
|
||||||
|
Ok(Promise::spawn_async(tokio::spawn(async move {
|
||||||
|
fetch_invoice_lnurl_async(
|
||||||
|
&lnurl,
|
||||||
|
PayEntry { url, response },
|
||||||
|
msats,
|
||||||
|
&sender_nsec,
|
||||||
|
relays,
|
||||||
|
target,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
None => Ok(Promise::spawn_async(tokio::spawn(async move {
|
||||||
|
tracing::info!("querying ln endpoint: {url}");
|
||||||
|
let pay_req = match fetch_pay_req_async(&url).await {
|
||||||
|
Ok(p) => PayEntry {
|
||||||
|
url,
|
||||||
|
response: p.into(),
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(e),
|
||||||
|
pay_entry: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch_invoice_lnurl_async(&lnurl, pay_req, msats, &sender_nsec, relays, target).await
|
||||||
|
}))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
|
fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
|
||||||
@@ -181,68 +239,96 @@ fn convert_lnurl_to_endpoint_url(lnurl: &str) -> Result<Url, ZapError> {
|
|||||||
String::from_utf8(data).map_err(|e| ZapError::Bech(format!("string conversion: {e}")))?;
|
String::from_utf8(data).map_err(|e| ZapError::Bech(format!("string conversion: {e}")))?;
|
||||||
|
|
||||||
Url::parse(&url_str)
|
Url::parse(&url_str)
|
||||||
.map_err(|e| ZapError::EndpointError(format!("endpoint url from lnurl is invalid: {e}")))
|
.map_err(|e| ZapError::endpoint_error(format!("endpoint url from lnurl is invalid: {e}")))
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_pay_req_from_lnurl_async(lnurl: &str) -> Result<LNUrlPayRequest, ZapError> {
|
|
||||||
let url = match convert_lnurl_to_endpoint_url(lnurl) {
|
|
||||||
Ok(u) => u,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch_pay_req_async(&url).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_invoice_lnurl_async(
|
async fn fetch_invoice_lnurl_async(
|
||||||
lnurl: &str,
|
lnurl: &str,
|
||||||
pay_req: &LNUrlPayRequest,
|
pay_entry: PayEntry,
|
||||||
msats: u64,
|
msats: u64,
|
||||||
sender_nsec: &[u8; 32],
|
sender_nsec: &[u8; 32],
|
||||||
relays: Vec<String>,
|
relays: Vec<String>,
|
||||||
target: ZapTargetOwned,
|
target: ZapTargetOwned,
|
||||||
) -> Result<FetchedInvoice, ZapError> {
|
) -> FetchedInvoiceResponse {
|
||||||
//let recipient = Pubkey::from_hex(&pay_req.nostr_pubkey)
|
if !pay_entry.response.allow_nostr {
|
||||||
//.map_err(|e| ZapError::EndpointError(format!("invalid pubkey hex from endpoint: {e}")))?;
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::endpoint_error(
|
||||||
|
"endpoint does not allow nostr".to_owned(),
|
||||||
|
)),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let mut base_url = Url::parse(&pay_req.callback_url)
|
if let Err(e) = &pay_entry.response.nostr_pubkey {
|
||||||
.map_err(|e| ZapError::EndpointError(format!("invalid callback url from endpoint: {e}")))?;
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::EndpointError(e.clone())),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let min_sendable = pay_entry.response.min_sendable;
|
||||||
|
if msats < min_sendable {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::endpoint_error(format!(
|
||||||
|
"zap amount {msats} is less than minimum sendable: {min_sendable} (in msats)"
|
||||||
|
))),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_sendable = pay_entry.response.max_sendable;
|
||||||
|
if msats > max_sendable {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::endpoint_error(format!(
|
||||||
|
"zap amount {msats} is greater than maximum sendable: {max_sendable} (in msats)"
|
||||||
|
))),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_url = match &pay_entry.response.callback_url {
|
||||||
|
Ok(url) => url.clone(),
|
||||||
|
Err(error) => {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(ZapError::EndpointError(error.clone())),
|
||||||
|
pay_entry: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let (query, noteid) = {
|
let (query, noteid) = {
|
||||||
let comment: &str = "";
|
let comment: &str = "";
|
||||||
let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target);
|
let note = make_kind_9734(lnurl, msats, comment, sender_nsec, relays, target);
|
||||||
let noteid = NoteId::new(*note.id());
|
let noteid = NoteId::new(*note.id());
|
||||||
let query = endpoint_query_for_invoice(&mut base_url, msats, lnurl, note)?;
|
let query = match endpoint_query_for_invoice(&base_url, msats, lnurl, note) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
return FetchedInvoiceResponse {
|
||||||
|
invoice: Err(e),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
(query, noteid)
|
(query, noteid)
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = fetch_invoice(query).await;
|
let res = fetch_ln_invoice(&query).await;
|
||||||
res.map(|i| FetchedInvoice {
|
FetchedInvoiceResponse {
|
||||||
invoice: i.invoice,
|
invoice: res.map(|r| FetchedInvoice {
|
||||||
|
invoice: r.invoice,
|
||||||
request_noteid: noteid,
|
request_noteid: noteid,
|
||||||
})
|
}),
|
||||||
|
pay_entry: Some(pay_entry),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_invoice_lud16_async(
|
async fn fetch_ln_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
|
||||||
lud16: &str,
|
|
||||||
msats: u64,
|
|
||||||
sender_nsec: &[u8; 32],
|
|
||||||
target: ZapTargetOwned,
|
|
||||||
relays: Vec<String>,
|
|
||||||
) -> Result<FetchedInvoice, ZapError> {
|
|
||||||
let pay_req = fetch_pay_req_from_lud16(lud16).await?;
|
|
||||||
|
|
||||||
let lnurl = lud16_to_lnurl(lud16)?;
|
|
||||||
|
|
||||||
fetch_invoice_lnurl_async(&lnurl, &pay_req, msats, sender_nsec, relays, target).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_invoice(req: &Url) -> Result<LNInvoice, ZapError> {
|
|
||||||
let request = ehttp::Request::get(req);
|
let request = ehttp::Request::get(req);
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
let on_done = move |response: Result<ehttp::Response, String>| {
|
let on_done = move |response: Result<ehttp::Response, String>| {
|
||||||
let handle = response.map_err(ZapError::EndpointError).and_then(|resp| {
|
let handle = response.map_err(ZapError::endpoint_error).and_then(|resp| {
|
||||||
if !resp.ok {
|
if !resp.ok {
|
||||||
return Err(ZapError::EndpointError(format!(
|
return Err(ZapError::endpoint_error(format!(
|
||||||
"invalid http response: {}",
|
"invalid http response: {}",
|
||||||
resp.status_text
|
resp.status_text
|
||||||
)));
|
)));
|
||||||
@@ -290,25 +376,32 @@ fn generate_endpoint_url(lud16: &str) -> Result<Url, ZapError> {
|
|||||||
if use_http { "" } else { "s" }
|
if use_http { "" } else { "s" }
|
||||||
);
|
);
|
||||||
|
|
||||||
Url::parse(&url_str).map_err(|e| ZapError::EndpointError(e.to_string()))
|
Url::parse(&url_str).map_err(|e| ZapError::endpoint_error(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use enostr::{FullKeypair, NoteId};
|
use enostr::{FullKeypair, NoteId};
|
||||||
|
|
||||||
use crate::zaps::networking::convert_lnurl_to_endpoint_url;
|
use crate::zaps::{
|
||||||
|
cache::PayCache,
|
||||||
use super::{
|
networking::{
|
||||||
fetch_invoice_lnurl, fetch_invoice_lud16, fetch_pay_req_from_lud16, lud16_to_lnurl,
|
convert_lnurl_to_endpoint_url, endpoint_url_to_lnurl, fetch_pay_req_async,
|
||||||
|
generate_endpoint_url,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::fetch_invoice_promise;
|
||||||
|
|
||||||
#[ignore] // don't run this test automatically since it sends real http
|
#[ignore] // don't run this test automatically since it sends real http
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_get_pay_req() {
|
async fn test_get_pay_req() {
|
||||||
let lud16 = "jb55@sendsats.lol";
|
let lud16 = "jb55@sendsats.lol";
|
||||||
|
|
||||||
let maybe_res = fetch_pay_req_from_lud16(lud16).await;
|
let url = generate_endpoint_url(lud16);
|
||||||
|
assert!(url.is_ok());
|
||||||
|
|
||||||
|
let maybe_res = fetch_pay_req_async(&url.unwrap()).await;
|
||||||
|
|
||||||
assert!(maybe_res.is_ok());
|
assert!(maybe_res.is_ok());
|
||||||
|
|
||||||
@@ -328,7 +421,10 @@ mod tests {
|
|||||||
fn test_lnurl() {
|
fn test_lnurl() {
|
||||||
let lud16 = "jb55@sendsats.lol";
|
let lud16 = "jb55@sendsats.lol";
|
||||||
|
|
||||||
let maybe_lnurl = lud16_to_lnurl(lud16);
|
let url = generate_endpoint_url(lud16);
|
||||||
|
assert!(url.is_ok());
|
||||||
|
|
||||||
|
let maybe_lnurl = endpoint_url_to_lnurl(&url.unwrap());
|
||||||
assert!(maybe_lnurl.is_ok());
|
assert!(maybe_lnurl.is_ok());
|
||||||
|
|
||||||
let lnurl = maybe_lnurl.unwrap();
|
let lnurl = maybe_lnurl.unwrap();
|
||||||
@@ -344,9 +440,11 @@ mod tests {
|
|||||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
|
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
|
||||||
|
|
||||||
let kp = FullKeypair::generate();
|
let kp = FullKeypair::generate();
|
||||||
|
let mut cache = PayCache::default();
|
||||||
let maybe_invoice = rt.block_on(async {
|
let maybe_invoice = rt.block_on(async {
|
||||||
fetch_invoice_lud16(
|
fetch_invoice_promise(
|
||||||
"jb55@sendsats.lol".to_owned(),
|
&mut cache,
|
||||||
|
crate::zaps::ZapAddress::Lud16("jb55@sendsats.lol".to_owned()),
|
||||||
1000,
|
1000,
|
||||||
FullKeypair::generate().secret_key.to_secret_bytes(),
|
FullKeypair::generate().secret_key.to_secret_bytes(),
|
||||||
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
||||||
@@ -355,14 +453,18 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
vec!["wss://relay.damus.io".to_owned()],
|
vec!["wss://relay.damus.io".to_owned()],
|
||||||
)
|
)
|
||||||
.block_and_take()
|
.map(|p| p.block_and_take())
|
||||||
});
|
});
|
||||||
|
|
||||||
assert!(maybe_invoice.is_ok());
|
assert!(maybe_invoice.is_ok());
|
||||||
let inner = maybe_invoice.unwrap();
|
let inner = maybe_invoice.unwrap();
|
||||||
assert!(inner.is_ok());
|
assert!(inner.is_ok());
|
||||||
let invoice = inner.unwrap();
|
let inner = inner.unwrap().invoice;
|
||||||
assert!(invoice.invoice.starts_with("lnbc"));
|
assert!(inner.is_ok());
|
||||||
|
|
||||||
|
let inner = inner.unwrap();
|
||||||
|
|
||||||
|
assert!(inner.invoice.starts_with("lnbc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -385,9 +487,11 @@ mod tests {
|
|||||||
|
|
||||||
let kp = FullKeypair::generate();
|
let kp = FullKeypair::generate();
|
||||||
|
|
||||||
|
let mut cache = PayCache::default();
|
||||||
let maybe_invoice = rt.block_on(async {
|
let maybe_invoice = rt.block_on(async {
|
||||||
fetch_invoice_lnurl(
|
fetch_invoice_promise(
|
||||||
lnurl.to_owned(),
|
&mut cache,
|
||||||
|
crate::zaps::ZapAddress::Lud06(lnurl.to_owned()),
|
||||||
1000,
|
1000,
|
||||||
kp.secret_key.to_secret_bytes(),
|
kp.secret_key.to_secret_bytes(),
|
||||||
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
crate::zaps::ZapTargetOwned::Note(crate::NoteZapTargetOwned {
|
||||||
@@ -396,11 +500,17 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
[relay.to_owned()].to_vec(),
|
[relay.to_owned()].to_vec(),
|
||||||
)
|
)
|
||||||
.block_and_take()
|
.map(|p| p.block_and_take())
|
||||||
});
|
});
|
||||||
|
|
||||||
assert!(maybe_invoice.is_ok());
|
assert!(maybe_invoice.is_ok());
|
||||||
|
let inner = maybe_invoice.unwrap();
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
let inner = inner.unwrap().invoice;
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
|
||||||
assert!(maybe_invoice.unwrap().unwrap().invoice.starts_with("lnbc"));
|
let inner = inner.unwrap();
|
||||||
|
|
||||||
|
assert!(inner.invoice.starts_with("lnbc"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
package com.damus.notedeck;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
public class KeyboardHeightHelper {
|
|
||||||
private static final String TAG = "KeyboardHeightHelper";
|
|
||||||
private KeyboardHeightProvider keyboardHeightProvider;
|
|
||||||
private Activity activity;
|
|
||||||
|
|
||||||
// Static JNI method not tied to any specific activity
|
|
||||||
private static native void nativeKeyboardHeightChanged(int height);
|
|
||||||
|
|
||||||
public KeyboardHeightHelper(Activity activity) {
|
|
||||||
this.activity = activity;
|
|
||||||
keyboardHeightProvider = new KeyboardHeightProvider(activity);
|
|
||||||
|
|
||||||
// Create observer implementation
|
|
||||||
KeyboardHeightObserver observer = (height, orientation) -> {
|
|
||||||
Log.d(TAG, "Keyboard height: " + height + "px, orientation: " +
|
|
||||||
(orientation == Configuration.ORIENTATION_PORTRAIT ? "portrait" : "landscape"));
|
|
||||||
|
|
||||||
// Call the generic native method
|
|
||||||
nativeKeyboardHeightChanged(height);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set up the provider
|
|
||||||
keyboardHeightProvider.setKeyboardHeightObserver(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
// Start the keyboard height provider after the view is ready
|
|
||||||
final View contentView = activity.findViewById(android.R.id.content);
|
|
||||||
contentView.post(() -> {
|
|
||||||
keyboardHeightProvider.start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
keyboardHeightProvider.setKeyboardHeightObserver(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
keyboardHeightProvider.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of Siebe Projects samples.
|
|
||||||
*
|
|
||||||
* Siebe Projects samples is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the Lesser GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Siebe Projects samples is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* Lesser GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the Lesser GNU General Public License
|
|
||||||
* along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.damus.notedeck;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The observer that will be notified when the height of
|
|
||||||
* the keyboard has changed
|
|
||||||
*/
|
|
||||||
public interface KeyboardHeightObserver {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the keyboard height has changed, 0 means keyboard is closed,
|
|
||||||
* >= 1 means keyboard is opened.
|
|
||||||
*
|
|
||||||
* @param height The height of the keyboard in pixels
|
|
||||||
* @param orientation The orientation either: Configuration.ORIENTATION_PORTRAIT or
|
|
||||||
* Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
*/
|
|
||||||
void onKeyboardHeightChanged(int height, int orientation);
|
|
||||||
}
|
|
||||||
-174
@@ -1,174 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of Siebe Projects samples.
|
|
||||||
*
|
|
||||||
* Siebe Projects samples is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the Lesser GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Siebe Projects samples is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* Lesser GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the Lesser GNU General Public License
|
|
||||||
* along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.damus.notedeck;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.graphics.Point;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
|
||||||
|
|
||||||
import android.view.WindowManager.LayoutParams;
|
|
||||||
|
|
||||||
import android.widget.PopupWindow;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The keyboard height provider, this class uses a PopupWindow
|
|
||||||
* to calculate the window height when the floating keyboard is opened and closed.
|
|
||||||
*/
|
|
||||||
public class KeyboardHeightProvider extends PopupWindow {
|
|
||||||
|
|
||||||
/** The tag for logging purposes */
|
|
||||||
private final static String TAG = "sample_KeyboardHeightProvider";
|
|
||||||
|
|
||||||
/** The keyboard height observer */
|
|
||||||
private KeyboardHeightObserver observer;
|
|
||||||
|
|
||||||
/** The cached landscape height of the keyboard */
|
|
||||||
private int keyboardLandscapeHeight;
|
|
||||||
|
|
||||||
/** The cached portrait height of the keyboard */
|
|
||||||
private int keyboardPortraitHeight;
|
|
||||||
|
|
||||||
/** The view that is used to calculate the keyboard height */
|
|
||||||
private View popupView;
|
|
||||||
|
|
||||||
/** The parent view */
|
|
||||||
private View parentView;
|
|
||||||
|
|
||||||
/** The root activity that uses this KeyboardHeightProvider */
|
|
||||||
private Activity activity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a new KeyboardHeightProvider
|
|
||||||
*
|
|
||||||
* @param activity The parent activity
|
|
||||||
*/
|
|
||||||
public KeyboardHeightProvider(Activity activity) {
|
|
||||||
super(activity);
|
|
||||||
this.activity = activity;
|
|
||||||
|
|
||||||
//LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
|
|
||||||
//this.popupView = inflator.inflate(android.R.layout.popupwindow, null, false);
|
|
||||||
this.popupView = new View(activity);
|
|
||||||
setContentView(popupView);
|
|
||||||
|
|
||||||
setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_RESIZE | LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
|
||||||
setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
|
|
||||||
|
|
||||||
parentView = activity.findViewById(android.R.id.content);
|
|
||||||
|
|
||||||
setWidth(0);
|
|
||||||
setHeight(LayoutParams.MATCH_PARENT);
|
|
||||||
|
|
||||||
popupView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onGlobalLayout() {
|
|
||||||
if (popupView != null) {
|
|
||||||
handleOnGlobalLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the KeyboardHeightProvider, this must be called after the onResume of the Activity.
|
|
||||||
* PopupWindows are not allowed to be registered before the onResume has finished
|
|
||||||
* of the Activity.
|
|
||||||
*/
|
|
||||||
public void start() {
|
|
||||||
|
|
||||||
if (!isShowing() && parentView.getWindowToken() != null) {
|
|
||||||
setBackgroundDrawable(new ColorDrawable(0));
|
|
||||||
showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the keyboard height provider,
|
|
||||||
* this provider will not be used anymore.
|
|
||||||
*/
|
|
||||||
public void close() {
|
|
||||||
this.observer = null;
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the keyboard height observer to this provider. The
|
|
||||||
* observer will be notified when the keyboard height has changed.
|
|
||||||
* For example when the keyboard is opened or closed.
|
|
||||||
*
|
|
||||||
* @param observer The observer to be added to this provider.
|
|
||||||
*/
|
|
||||||
public void setKeyboardHeightObserver(KeyboardHeightObserver observer) {
|
|
||||||
this.observer = observer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Popup window itself is as big as the window of the Activity.
|
|
||||||
* The keyboard can then be calculated by extracting the popup view bottom
|
|
||||||
* from the activity window height.
|
|
||||||
*/
|
|
||||||
private void handleOnGlobalLayout() {
|
|
||||||
|
|
||||||
Point screenSize = new Point();
|
|
||||||
activity.getWindowManager().getDefaultDisplay().getSize(screenSize);
|
|
||||||
|
|
||||||
Rect rect = new Rect();
|
|
||||||
popupView.getWindowVisibleDisplayFrame(rect);
|
|
||||||
|
|
||||||
// REMIND, you may like to change this using the fullscreen size of the phone
|
|
||||||
// and also using the status bar and navigation bar heights of the phone to calculate
|
|
||||||
// the keyboard height. But this worked fine on a Nexus.
|
|
||||||
int orientation = getScreenOrientation();
|
|
||||||
int keyboardHeight = screenSize.y - rect.bottom;
|
|
||||||
|
|
||||||
if (keyboardHeight == 0) {
|
|
||||||
notifyKeyboardHeightChanged(0, orientation);
|
|
||||||
}
|
|
||||||
else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
||||||
this.keyboardPortraitHeight = keyboardHeight;
|
|
||||||
notifyKeyboardHeightChanged(keyboardPortraitHeight, orientation);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.keyboardLandscapeHeight = keyboardHeight;
|
|
||||||
notifyKeyboardHeightChanged(keyboardLandscapeHeight, orientation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getScreenOrientation() {
|
|
||||||
return activity.getResources().getConfiguration().orientation;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyKeyboardHeightChanged(int height, int orientation) {
|
|
||||||
if (observer != null) {
|
|
||||||
observer.onKeyboardHeightChanged(height, orientation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+161
-49
@@ -1,13 +1,18 @@
|
|||||||
package com.damus.notedeck;
|
package com.damus.notedeck;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.DisplayCutoutCompat;
|
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowCompat;
|
import androidx.core.view.WindowCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
@@ -15,52 +20,23 @@ import androidx.core.view.WindowInsetsControllerCompat;
|
|||||||
|
|
||||||
import com.google.androidgamesdk.GameActivity;
|
import com.google.androidgamesdk.GameActivity;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
public class MainActivity extends GameActivity {
|
public class MainActivity extends GameActivity {
|
||||||
static {
|
static final int REQUEST_CODE_PICK_FILE = 420;
|
||||||
System.loadLibrary("notedeck_chrome");
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void nativeOnKeyboardHeightChanged(int height);
|
private native void nativeOnFilePickedFailed(String uri, String e);
|
||||||
private KeyboardHeightHelper keyboardHelper;
|
private native void nativeOnFilePickedWithContent(Object[] uri_info, byte[] content);
|
||||||
|
|
||||||
@Override
|
public void openFilePicker() {
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
// Shrink view so it does not get covered by insets.
|
intent.setType("*/*");
|
||||||
super.onCreate(savedInstanceState);
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
setupInsets();
|
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
|
||||||
|
|
||||||
//setupFullscreen()
|
|
||||||
|
|
||||||
//keyboardHelper = new KeyboardHeightHelper(this);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupFullscreen() {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
|
||||||
|
|
||||||
WindowInsetsControllerCompat controller =
|
|
||||||
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
|
||||||
if (controller != null) {
|
|
||||||
controller.setSystemBarsBehavior(
|
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
);
|
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
|
||||||
}
|
|
||||||
|
|
||||||
//focus(getContent())
|
|
||||||
}
|
|
||||||
|
|
||||||
// not sure if this does anything
|
|
||||||
private void focus(View content) {
|
|
||||||
content.setFocusable(true);
|
|
||||||
content.setFocusableInTouchMode(true);
|
|
||||||
content.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private View getContent() {
|
|
||||||
return getWindow().getDecorView().findViewById(android.R.id.content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupInsets() {
|
private void setupInsets() {
|
||||||
@@ -93,25 +69,161 @@ public class MainActivity extends GameActivity {
|
|||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
private void processSelectedFile(Uri uri) {
|
||||||
|
try {
|
||||||
|
nativeOnFilePickedWithContent(this.getUriInfo(uri), readUriContent(uri));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("MainActivity", "Error processing file: " + uri.toString(), e);
|
||||||
|
|
||||||
|
nativeOnFilePickedFailed(uri.toString(), e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object[] getUriInfo(Uri uri) throws Exception {
|
||||||
|
if (!uri.getScheme().equals("content")) {
|
||||||
|
throw new Exception("uri should start with content://");
|
||||||
|
}
|
||||||
|
|
||||||
|
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
Object[] info = new Object[3];
|
||||||
|
|
||||||
|
int col_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||||
|
info[0] = cursor.getString(col_idx);
|
||||||
|
|
||||||
|
col_idx = cursor.getColumnIndex(OpenableColumns.SIZE);
|
||||||
|
info[1] = cursor.getLong(col_idx);
|
||||||
|
|
||||||
|
col_idx = cursor.getColumnIndex("mime_type");
|
||||||
|
info[2] = cursor.getString(col_idx);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] readUriContent(Uri uri) {
|
||||||
|
InputStream inputStream = null;
|
||||||
|
ByteArrayOutputStream buffer = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
inputStream = getContentResolver().openInputStream(uri);
|
||||||
|
if (inputStream == null) {
|
||||||
|
Log.e("MainActivity", "Could not open input stream for URI: " + uri);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = new ByteArrayOutputStream();
|
||||||
|
byte[] data = new byte[8192]; // 8KB buffer
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
while ((bytesRead = inputStream.read(data)) != -1) {
|
||||||
|
buffer.write(data, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] result = buffer.toByteArray();
|
||||||
|
Log.d("MainActivity", "Successfully read " + result.length + " bytes");
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("MainActivity", "IOException while reading URI: " + uri, e);
|
||||||
|
return null;
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
Log.e("MainActivity", "SecurityException while reading URI: " + uri, e);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// Close streams
|
||||||
|
if (inputStream != null) {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("MainActivity", "Error closing input stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buffer != null) {
|
||||||
|
try {
|
||||||
|
buffer.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("MainActivity", "Error closing buffer", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
// Shrink view so it does not get covered by insets.
|
||||||
|
|
||||||
|
setupInsets();
|
||||||
|
//setupFullscreen()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
|
if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == RESULT_OK) {
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
if (data.getClipData() != null) {
|
||||||
|
// Multiple files selected
|
||||||
|
ClipData clipData = data.getClipData();
|
||||||
|
for (int i = 0; i < clipData.getItemCount(); i++) {
|
||||||
|
Uri uri = clipData.getItemAt(i).getUri();
|
||||||
|
processSelectedFile(uri);
|
||||||
|
}
|
||||||
|
} else if (data.getData() != null) {
|
||||||
|
// Single file selected
|
||||||
|
Uri uri = data.getData();
|
||||||
|
processSelectedFile(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupFullscreen() {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
|
||||||
|
WindowInsetsControllerCompat controller =
|
||||||
|
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
||||||
|
if (controller != null) {
|
||||||
|
controller.setSystemBarsBehavior(
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
);
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||||
|
}
|
||||||
|
|
||||||
|
//focus(getContent())
|
||||||
|
}
|
||||||
|
|
||||||
|
// not sure if this does anything
|
||||||
|
private void focus(View content) {
|
||||||
|
content.setFocusable(true);
|
||||||
|
content.setFocusableInTouchMode(true);
|
||||||
|
content.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private View getContent() {
|
||||||
|
return getWindow().getDecorView().findViewById(android.R.id.content);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
keyboardHelper.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
keyboardHelper.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
keyboardHelper.close();
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ use notedeck::Notedeck;
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn android_main(app: AndroidApp) {
|
pub async fn android_main(android_app: AndroidApp) {
|
||||||
//use tracing_logcat::{LogcatMakeWriter, LogcatTag};
|
//use tracing_logcat::{LogcatMakeWriter, LogcatTag};
|
||||||
use tracing_subscriber::{prelude::*, EnvFilter};
|
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||||
|
|
||||||
std::env::set_var("RUST_BACKTRACE", "full");
|
std::env::set_var("RUST_BACKTRACE", "full");
|
||||||
//std::env::set_var("DAVE_ENDPOINT", "http://ollama.jb55.com/v1");
|
|
||||||
//std::env::set_var("DAVE_MODEL", "hhao/qwen2.5-coder-tools:latest");
|
//std::env::set_var("DAVE_MODEL", "hhao/qwen2.5-coder-tools:latest");
|
||||||
std::env::set_var(
|
std::env::set_var(
|
||||||
"RUST_LOG",
|
"RUST_LOG",
|
||||||
@@ -42,7 +41,7 @@ pub async fn android_main(app: AndroidApp) {
|
|||||||
.with(fmt_layer)
|
.with(fmt_layer)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let path = app.internal_data_path().expect("data path");
|
let path = android_app.internal_data_path().expect("data path");
|
||||||
let mut options = eframe::NativeOptions {
|
let mut options = eframe::NativeOptions {
|
||||||
depth_buffer: 24,
|
depth_buffer: 24,
|
||||||
..eframe::NativeOptions::default()
|
..eframe::NativeOptions::default()
|
||||||
@@ -55,17 +54,18 @@ pub async fn android_main(app: AndroidApp) {
|
|||||||
// builder.with_android_app(app_clone_for_event_loop);
|
// builder.with_android_app(app_clone_for_event_loop);
|
||||||
//}));
|
//}));
|
||||||
|
|
||||||
options.android_app = Some(app.clone());
|
options.android_app = Some(android_app.clone());
|
||||||
|
|
||||||
let app_args = get_app_args(app.clone());
|
let app_args = get_app_args();
|
||||||
|
|
||||||
let _res = eframe::run_native(
|
let _res = eframe::run_native(
|
||||||
"Damus Notedeck",
|
"Damus Notedeck",
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
let ctx = &cc.egui_ctx;
|
let ctx = &cc.egui_ctx;
|
||||||
|
|
||||||
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
||||||
notedeck.set_android_context(app.clone());
|
notedeck.set_android_context(android_app);
|
||||||
notedeck.setup(ctx);
|
notedeck.setup(ctx);
|
||||||
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
|
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
|
||||||
notedeck.set_app(chrome);
|
notedeck.set_app(chrome);
|
||||||
@@ -104,7 +104,7 @@ Using internal storage would be better but it seems hard to get the config file
|
|||||||
the device ...
|
the device ...
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fn get_app_args(_app: AndroidApp) -> Vec<String> {
|
fn get_app_args() -> Vec<String> {
|
||||||
vec!["argv0-placeholder".to_string()]
|
vec!["argv0-placeholder".to_string()]
|
||||||
/*
|
/*
|
||||||
use serde_json::value;
|
use serde_json::value;
|
||||||
|
|||||||
@@ -307,7 +307,6 @@ impl Chrome {
|
|||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
// keyboard-visibility virtual keyboard
|
// keyboard-visibility virtual keyboard
|
||||||
if virtual_keyboard && keyboard_height > 0.0 {
|
if virtual_keyboard && keyboard_height > 0.0 {
|
||||||
tracing::debug!("got here");
|
|
||||||
virtual_keyboard_ui(ui, ui.available_rect_before_wrap())
|
virtual_keyboard_ui(ui, ui.available_rect_before_wrap())
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -777,14 +776,14 @@ fn bottomup_sidebar(
|
|||||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
chrome.show_memory_debug = !chrome.show_memory_debug;
|
chrome.options.toggle(ChromeOptions::MemoryDebug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(resident) = mem_use.resident {
|
if let Some(resident) = mem_use.resident {
|
||||||
ui.weak(format!("{}", format_bytes(resident as f64)));
|
ui.weak(format!("{}", format_bytes(resident as f64)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if chrome.show_memory_debug {
|
if chrome.options.contains(ChromeOptions::MemoryDebug) {
|
||||||
egui::Window::new("Memory Debug").show(ui.ctx(), memory_debug_ui);
|
egui::Window::new("Memory Debug").show(ui.ctx(), memory_debug_ui);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,6 @@ mod tests {
|
|||||||
|
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
let mut notedeck = Notedeck::new(&ctx, &tmpdir, &args);
|
let mut notedeck = Notedeck::new(&ctx, &tmpdir, &args);
|
||||||
let unrecognized_args = notedeck.unrecognized_args().clone();
|
|
||||||
let mut app_ctx = notedeck.app_context();
|
let mut app_ctx = notedeck.app_context();
|
||||||
let app = Damus::new(&mut app_ctx, &args);
|
let app = Damus::new(&mut app_ctx, &args);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ description = "A tweetdeck-style notedeck app"
|
|||||||
[lib]
|
[lib]
|
||||||
crate-type = ["lib", "cdylib"]
|
crate-type = ["lib", "cdylib"]
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
jni = { workspace = true }
|
||||||
|
ndk-context = "0.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
opener = { workspace = true }
|
opener = { workspace = true }
|
||||||
rmpv = { workspace = true }
|
rmpv = { workspace = true }
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ pub fn render_accounts_route(
|
|||||||
app_ctx: &mut AppContext,
|
app_ctx: &mut AppContext,
|
||||||
jobs: &mut JobsCache,
|
jobs: &mut JobsCache,
|
||||||
login_state: &mut AcquireKeyState,
|
login_state: &mut AcquireKeyState,
|
||||||
onboarding: &Onboarding,
|
onboarding: &mut Onboarding,
|
||||||
follow_packs_ui: &mut Nip51SetUiCache,
|
follow_packs_ui: &mut Nip51SetUiCache,
|
||||||
route: AccountsRoute,
|
route: AccountsRoute,
|
||||||
) -> Option<AccountsResponse> {
|
) -> Option<AccountsResponse> {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
column::Columns,
|
column::Columns,
|
||||||
nav::{RouterAction, RouterType},
|
nav::{RouterAction, RouterType},
|
||||||
route::Route,
|
route::Route,
|
||||||
timeline::{
|
timeline::{
|
||||||
thread::{
|
thread::{selected_has_at_least_n_replies, NoteSeenFlags, ThreadNode, Threads},
|
||||||
selected_has_at_least_n_replies, InsertionResponse, NoteSeenFlags, ThreadNode, Threads,
|
InsertionResponse, ThreadSelection, TimelineCache, TimelineKind,
|
||||||
},
|
|
||||||
ThreadSelection, TimelineCache, TimelineKind,
|
|
||||||
},
|
},
|
||||||
view_state::ViewState,
|
view_state::ViewState,
|
||||||
};
|
};
|
||||||
@@ -30,8 +30,9 @@ pub enum NotesOpenResult {
|
|||||||
Thread(NewThreadNotes),
|
Thread(NewThreadNotes),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum TimelineOpenResult {
|
pub struct TimelineOpenResult {
|
||||||
NewNotes(NewNotes),
|
new_notes: Option<NewNotes>,
|
||||||
|
new_pks: Option<HashSet<Pubkey>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoteActionResponse {
|
struct NoteActionResponse {
|
||||||
@@ -270,7 +271,24 @@ fn clear_zap_error(sender: &Pubkey, zaps: &mut Zaps, target: &NoteZapTargetOwned
|
|||||||
|
|
||||||
impl TimelineOpenResult {
|
impl TimelineOpenResult {
|
||||||
pub fn new_notes(notes: Vec<NoteKey>, id: TimelineKind) -> Self {
|
pub fn new_notes(notes: Vec<NoteKey>, id: TimelineKind) -> Self {
|
||||||
Self::NewNotes(NewNotes::new(notes, id))
|
Self {
|
||||||
|
new_notes: Some(NewNotes { id, notes }),
|
||||||
|
new_pks: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_pks(pks: HashSet<Pubkey>) -> Self {
|
||||||
|
Self {
|
||||||
|
new_notes: None,
|
||||||
|
new_pks: Some(pks),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_pks(&mut self, pks: HashSet<Pubkey>) {
|
||||||
|
match &mut self.new_pks {
|
||||||
|
Some(cur_pks) => cur_pks.extend(pks),
|
||||||
|
None => self.new_pks = Some(pks),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process(
|
pub fn process(
|
||||||
@@ -281,11 +299,17 @@ impl TimelineOpenResult {
|
|||||||
storage: &mut TimelineCache,
|
storage: &mut TimelineCache,
|
||||||
unknown_ids: &mut UnknownIds,
|
unknown_ids: &mut UnknownIds,
|
||||||
) {
|
) {
|
||||||
match self {
|
|
||||||
// update the thread for next render if we have new notes
|
// update the thread for next render if we have new notes
|
||||||
TimelineOpenResult::NewNotes(new_notes) => {
|
if let Some(new_notes) = &self.new_notes {
|
||||||
new_notes.process(storage, ndb, txn, unknown_ids, note_cache);
|
new_notes.process(storage, ndb, txn, unknown_ids, note_cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(pks) = &self.new_pks else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for pk in pks {
|
||||||
|
unknown_ids.add_pubkey_if_missing(ndb, txn, pk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,7 +411,7 @@ pub fn process_thread_notes(
|
|||||||
created_at,
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
if thread.replies.contains(¬e_ref) {
|
if thread.replies.contains_key(¬e_ref.key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ fn try_process_event(
|
|||||||
app_ctx.note_cache,
|
app_ctx.note_cache,
|
||||||
timeline,
|
timeline,
|
||||||
app_ctx.accounts,
|
app_ctx.accounts,
|
||||||
|
app_ctx.unknown_ids,
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_ready {
|
if is_ready {
|
||||||
@@ -222,6 +223,7 @@ fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Con
|
|||||||
app_ctx.ndb,
|
app_ctx.ndb,
|
||||||
app_ctx.note_cache,
|
app_ctx.note_cache,
|
||||||
&mut damus.timeline_cache,
|
&mut damus.timeline_cache,
|
||||||
|
app_ctx.unknown_ids,
|
||||||
) {
|
) {
|
||||||
warn!("update_damus init: {err}");
|
warn!("update_damus init: {err}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,16 @@ impl ColumnsArgs {
|
|||||||
} else if column_name == "universe" {
|
} else if column_name == "universe" {
|
||||||
debug!("got universe column");
|
debug!("got universe column");
|
||||||
res.columns
|
res.columns
|
||||||
.push(ArgColumn::Timeline(TimelineKind::Universe))
|
.push(ArgColumn::Timeline(TimelineKind::Universe));
|
||||||
|
} else if let Some(hashtag) = column_name.strip_prefix("hashtag:") {
|
||||||
|
let hashtags: Vec<String> = hashtag
|
||||||
|
.split(",")
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|p| !p.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect();
|
||||||
|
res.columns
|
||||||
|
.push(ArgColumn::Timeline(TimelineKind::Hashtag(hashtags)));
|
||||||
} else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") {
|
} else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") {
|
||||||
if let Ok(pubkey) = Pubkey::parse(profile_pk_str) {
|
if let Ok(pubkey) = Pubkey::parse(profile_pk_str) {
|
||||||
info!("got profile column for user {}", pubkey.hex());
|
info!("got profile column for user {}", pubkey.hex());
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
#![cfg_attr(target_os = "android", allow(dead_code, unused_variables))]
|
#![cfg_attr(target_os = "android", allow(dead_code, unused_variables))]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use crate::Error;
|
||||||
|
|
||||||
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||||
use ehttp::Request;
|
use ehttp::Request;
|
||||||
use nostrdb::{Note, NoteBuilder};
|
use nostrdb::{Note, NoteBuilder};
|
||||||
use notedeck::SupportedMimeType;
|
use notedeck::{
|
||||||
|
media::images::fetch_binary_from_disk,
|
||||||
|
platform::file::{MediaFrom, SelectedMedia},
|
||||||
|
};
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
use notedeck::media::images::fetch_binary_from_disk;
|
|
||||||
|
|
||||||
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
|
pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
|
||||||
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
|
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";
|
||||||
|
|
||||||
@@ -94,15 +93,15 @@ fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String
|
|||||||
|
|
||||||
fn create_nip96_request(
|
fn create_nip96_request(
|
||||||
upload_url: &str,
|
upload_url: &str,
|
||||||
media_path: MediaPath,
|
file_name: &str,
|
||||||
|
media_type: &str,
|
||||||
file_contents: Vec<u8>,
|
file_contents: Vec<u8>,
|
||||||
nip98_base64: &str,
|
nip98_base64: &str,
|
||||||
) -> ehttp::Request {
|
) -> ehttp::Request {
|
||||||
let boundary = "----boundary";
|
let boundary = "----boundary";
|
||||||
|
|
||||||
let mut body = format!(
|
let mut body = format!(
|
||||||
"--{}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
|
"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{file_name}\"\r\nContent-Type: {media_type}\r\n\r\n",
|
||||||
boundary, media_path.file_name, media_path.media_type.to_mime()
|
|
||||||
)
|
)
|
||||||
.into_bytes();
|
.into_bytes();
|
||||||
body.extend(file_contents);
|
body.extend(file_contents);
|
||||||
@@ -134,25 +133,14 @@ fn sha256_hex(contents: &Vec<u8>) -> String {
|
|||||||
pub fn nip96_upload(
|
pub fn nip96_upload(
|
||||||
seckey: [u8; 32],
|
seckey: [u8; 32],
|
||||||
upload_url: String,
|
upload_url: String,
|
||||||
media_path: MediaPath,
|
selected_media: SelectedMedia,
|
||||||
) -> Promise<Result<Nip94Event, Error>> {
|
) -> Promise<Result<Nip94Event, Error>> {
|
||||||
let bytes_res = fetch_binary_from_disk(media_path.full_path.clone());
|
internal_nip96_upload(seckey, upload_url, selected_media)
|
||||||
|
|
||||||
let file_bytes = match bytes_res {
|
|
||||||
Ok(bytes) => bytes,
|
|
||||||
Err(e) => {
|
|
||||||
return Promise::from_ready(Err(Error::Generic(format!(
|
|
||||||
"could not read contents of file to upload: {e}"
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
internal_nip96_upload(seckey, upload_url, media_path, file_bytes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nostrbuild_nip96_upload(
|
pub fn nostrbuild_nip96_upload(
|
||||||
seckey: [u8; 32],
|
seckey: [u8; 32],
|
||||||
media_path: MediaPath,
|
selected_media: SelectedMedia,
|
||||||
) -> Promise<Result<Nip94Event, Error>> {
|
) -> Promise<Result<Nip94Event, Error>> {
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@@ -166,7 +154,7 @@ pub fn nostrbuild_nip96_upload(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = nip96_upload(seckey, upload_url, media_path).block_and_take();
|
let res = nip96_upload(seckey, upload_url, selected_media).block_and_take();
|
||||||
sender.send(res);
|
sender.send(res);
|
||||||
});
|
});
|
||||||
promise
|
promise
|
||||||
@@ -175,9 +163,21 @@ pub fn nostrbuild_nip96_upload(
|
|||||||
fn internal_nip96_upload(
|
fn internal_nip96_upload(
|
||||||
seckey: [u8; 32],
|
seckey: [u8; 32],
|
||||||
upload_url: String,
|
upload_url: String,
|
||||||
media_path: MediaPath,
|
selected_media: SelectedMedia,
|
||||||
file_contents: Vec<u8>,
|
|
||||||
) -> Promise<Result<Nip94Event, Error>> {
|
) -> Promise<Result<Nip94Event, Error>> {
|
||||||
|
let file_name = selected_media.file_name;
|
||||||
|
let mime_type = selected_media.media_type.to_mime();
|
||||||
|
let bytes_res = bytes_from_media(selected_media.from);
|
||||||
|
|
||||||
|
let file_contents = match bytes_res {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(e) => {
|
||||||
|
return Promise::from_ready(Err(Error::Generic(format!(
|
||||||
|
"could not read contents of file to upload: {e}"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let file_hash = sha256_hex(&file_contents);
|
let file_hash = sha256_hex(&file_contents);
|
||||||
let nip98_note = create_nip98_note(&seckey, upload_url.to_owned(), file_hash);
|
let nip98_note = create_nip98_note(&seckey, upload_url.to_owned(), file_hash);
|
||||||
|
|
||||||
@@ -186,7 +186,13 @@ fn internal_nip96_upload(
|
|||||||
Err(e) => return Promise::from_ready(Err(Error::Generic(e.to_string()))),
|
Err(e) => return Promise::from_ready(Err(Error::Generic(e.to_string()))),
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = create_nip96_request(&upload_url, media_path, file_contents, &nip98_base64);
|
let request = create_nip96_request(
|
||||||
|
&upload_url,
|
||||||
|
&file_name,
|
||||||
|
mime_type,
|
||||||
|
file_contents,
|
||||||
|
&nip98_base64,
|
||||||
|
);
|
||||||
|
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
|
|
||||||
@@ -232,33 +238,10 @@ fn find_nip94_ev_in_json(json: String) -> Result<Nip94Event, Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub fn bytes_from_media(media: MediaFrom) -> Result<Vec<u8>, notedeck::Error> {
|
||||||
pub struct MediaPath {
|
match media {
|
||||||
full_path: PathBuf,
|
MediaFrom::PathBuf(full_path) => fetch_binary_from_disk(full_path.clone()),
|
||||||
file_name: String,
|
MediaFrom::Memory(bytes) => Ok(bytes),
|
||||||
media_type: SupportedMimeType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MediaPath {
|
|
||||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
|
||||||
if let Some(ex) = path.extension().and_then(|f| f.to_str()) {
|
|
||||||
let media_type = SupportedMimeType::from_extension(ex)?;
|
|
||||||
let file_name = path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.unwrap_or(&format!("file.{ex}"))
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
Ok(MediaPath {
|
|
||||||
full_path: path,
|
|
||||||
file_name,
|
|
||||||
media_type,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(Error::Generic(format!(
|
|
||||||
"{path:?} does not have an extension"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +332,7 @@ mod tests {
|
|||||||
use enostr::FullKeypair;
|
use enostr::FullKeypair;
|
||||||
|
|
||||||
use crate::media_upload::{
|
use crate::media_upload::{
|
||||||
get_upload_url_from_provider, nostrbuild_nip96_upload, MediaPath, NOSTR_BUILD_URL,
|
get_upload_url_from_provider, nostrbuild_nip96_upload, SelectedMedia, NOSTR_BUILD_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::internal_nip96_upload;
|
use super::internal_nip96_upload;
|
||||||
@@ -368,7 +351,7 @@ mod tests {
|
|||||||
fn test_internal_nip96() {
|
fn test_internal_nip96() {
|
||||||
// just a random image to test image upload
|
// just a random image to test image upload
|
||||||
let file_path = PathBuf::from_str("../../../assets/damus_rounded_80.png").unwrap();
|
let file_path = PathBuf::from_str("../../../assets/damus_rounded_80.png").unwrap();
|
||||||
let media_path = MediaPath::new(file_path).unwrap();
|
let selected_media = SelectedMedia::from_path(file_path).unwrap();
|
||||||
let img_bytes = include_bytes!("../../../assets/damus_rounded_80.png");
|
let img_bytes = include_bytes!("../../../assets/damus_rounded_80.png");
|
||||||
let promise = get_upload_url_from_provider(NOSTR_BUILD_URL());
|
let promise = get_upload_url_from_provider(NOSTR_BUILD_URL());
|
||||||
let kp = FullKeypair::generate();
|
let kp = FullKeypair::generate();
|
||||||
@@ -378,8 +361,7 @@ mod tests {
|
|||||||
let promise = internal_nip96_upload(
|
let promise = internal_nip96_upload(
|
||||||
kp.secret_key.secret_bytes(),
|
kp.secret_key.secret_bytes(),
|
||||||
upload_url.to_string(),
|
upload_url.to_string(),
|
||||||
media_path,
|
selected_media,
|
||||||
img_bytes.to_vec(),
|
|
||||||
);
|
);
|
||||||
let res = promise.block_until_ready();
|
let res = promise.block_until_ready();
|
||||||
assert!(res.is_ok())
|
assert!(res.is_ok())
|
||||||
@@ -395,11 +377,11 @@ mod tests {
|
|||||||
let file_path =
|
let file_path =
|
||||||
fs::canonicalize(PathBuf::from_str("../../assets/damus_rounded_80.png").unwrap())
|
fs::canonicalize(PathBuf::from_str("../../assets/damus_rounded_80.png").unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let media_path = MediaPath::new(file_path).unwrap();
|
let selected_media = SelectedMedia::from_path(file_path).unwrap();
|
||||||
let kp = FullKeypair::generate();
|
let kp = FullKeypair::generate();
|
||||||
println!("Using pubkey: {:?}", kp.pubkey);
|
println!("Using pubkey: {:?}", kp.pubkey);
|
||||||
|
|
||||||
let promise = nostrbuild_nip96_upload(kp.secret_key.secret_bytes(), media_path);
|
let promise = nostrbuild_nip96_upload(kp.secret_key.secret_bytes(), selected_media);
|
||||||
|
|
||||||
let out = promise.block_and_take();
|
let out = promise.block_and_take();
|
||||||
assert!(out.is_ok());
|
assert!(out.is_ok());
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ fn render_nav_body(
|
|||||||
ctx,
|
ctx,
|
||||||
&mut app.jobs,
|
&mut app.jobs,
|
||||||
&mut app.view_state.login,
|
&mut app.view_state.login,
|
||||||
&app.onboarding,
|
&mut app.onboarding,
|
||||||
&mut app.view_state.follow_packs,
|
&mut app.view_state.follow_packs,
|
||||||
*amr,
|
*amr,
|
||||||
) else {
|
) else {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use egui_virtual_list::VirtualList;
|
||||||
use enostr::{Pubkey, RelayPool};
|
use enostr::{Pubkey, RelayPool};
|
||||||
use nostrdb::{Filter, Ndb, NoteKey, Transaction};
|
use nostrdb::{Filter, Ndb, NoteKey, Transaction};
|
||||||
use notedeck::{create_nip51_set, filter::default_limit, Nip51SetCache, UnknownIds};
|
use notedeck::{create_nip51_set, filter::default_limit, Nip51SetCache, UnknownIds};
|
||||||
@@ -16,6 +19,7 @@ enum OnboardingState {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Onboarding {
|
pub struct Onboarding {
|
||||||
state: Option<Result<OnboardingState, OnboardingError>>,
|
state: Option<Result<OnboardingState, OnboardingError>>,
|
||||||
|
pub list: Rc<RefCell<VirtualList>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Onboarding {
|
impl Onboarding {
|
||||||
@@ -107,8 +111,8 @@ pub enum OnboardingError {
|
|||||||
|
|
||||||
// author providing the list of trusted follow pack authors
|
// author providing the list of trusted follow pack authors
|
||||||
const FOLLOW_PACK_AUTHOR: [u8; 32] = [
|
const FOLLOW_PACK_AUTHOR: [u8; 32] = [
|
||||||
0x34, 0x27, 0x76, 0x21, 0x61, 0x20, 0x15, 0x65, 0x49, 0x7d, 0xd9, 0x9c, 0x7a, 0x81, 0xd6, 0x11,
|
0x89, 0x5c, 0x2a, 0x90, 0xa8, 0x60, 0xac, 0x18, 0x43, 0x4a, 0xa6, 0x9e, 0x7b, 0x0d, 0xa8, 0x46,
|
||||||
0x8f, 0x46, 0xf6, 0x19, 0xc9, 0xec, 0x56, 0x32, 0x87, 0x05, 0xcc, 0x85, 0x07, 0x17, 0xa5, 0x4a,
|
0x57, 0x21, 0x21, 0x6f, 0xa3, 0x6e, 0x42, 0xc0, 0x22, 0xe3, 0x93, 0x57, 0x9c, 0x48, 0x6c, 0xba,
|
||||||
];
|
];
|
||||||
|
|
||||||
fn trusted_pks_list_filter() -> Filter {
|
fn trusted_pks_list_filter() -> Filter {
|
||||||
@@ -116,7 +120,7 @@ fn trusted_pks_list_filter() -> Filter {
|
|||||||
.kinds([30000])
|
.kinds([30000])
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.authors(&[FOLLOW_PACK_AUTHOR])
|
.authors(&[FOLLOW_PACK_AUTHOR])
|
||||||
.tags(["trusted-follow-pack-authors"], 'd') // TODO(kernelkind): replace with actual d tag
|
.tags(["trusted-follow-pack-authors"], 'd')
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
actionbar::TimelineOpenResult,
|
actionbar::TimelineOpenResult,
|
||||||
error::Error,
|
error::Error,
|
||||||
timeline::{Timeline, TimelineKind},
|
timeline::{Timeline, TimelineKind, UnknownPksOwned},
|
||||||
};
|
};
|
||||||
|
|
||||||
use notedeck::{filter, FilterState, NoteCache, NoteRef};
|
use notedeck::{filter, FilterState, NoteCache, NoteRef};
|
||||||
@@ -90,17 +90,19 @@ impl TimelineCache {
|
|||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
notes: &[NoteRef],
|
notes: &[NoteRef],
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
) {
|
) -> Option<UnknownPksOwned> {
|
||||||
let mut timeline = if let Some(timeline) = id.clone().into_timeline(txn, ndb) {
|
let mut timeline = if let Some(timeline) = id.clone().into_timeline(txn, ndb) {
|
||||||
timeline
|
timeline
|
||||||
} else {
|
} else {
|
||||||
error!("Error creating timeline from {:?}", &id);
|
error!("Error creating timeline from {:?}", &id);
|
||||||
return;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
// insert initial notes into timeline
|
// insert initial notes into timeline
|
||||||
timeline.insert_new(txn, ndb, note_cache, notes);
|
let res = timeline.insert_new(txn, ndb, note_cache, notes);
|
||||||
self.timelines.insert(id, timeline);
|
self.timelines.insert(id, timeline);
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, id: TimelineKind, timeline: Timeline) {
|
pub fn insert(&mut self, id: TimelineKind, timeline: Timeline) {
|
||||||
@@ -113,31 +115,41 @@ impl TimelineCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get and/or update the notes associated with this timeline
|
/// Get and/or update the notes associated with this timeline
|
||||||
pub fn notes<'a>(
|
fn notes<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
id: &TimelineKind,
|
id: &TimelineKind,
|
||||||
) -> Vitality<'a, Timeline> {
|
) -> GetNotesResponse<'a> {
|
||||||
// we can't use the naive hashmap entry API here because lookups
|
// we can't use the naive hashmap entry API here because lookups
|
||||||
// require a copy, wait until we have a raw entry api. We could
|
// require a copy, wait until we have a raw entry api. We could
|
||||||
// also use hashbrown?
|
// also use hashbrown?
|
||||||
|
|
||||||
if self.timelines.contains_key(id) {
|
if self.timelines.contains_key(id) {
|
||||||
return Vitality::Stale(self.get_expected_mut(id));
|
return GetNotesResponse {
|
||||||
|
vitality: Vitality::Stale(self.get_expected_mut(id)),
|
||||||
|
unknown_pks: None,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
|
for package in filters.local().packages {
|
||||||
|
if let Ok(results) = ndb.query(txn, package.filters, 1000) {
|
||||||
|
let cur_notes: Vec<NoteRef> = results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(NoteRef::from_query_result)
|
.map(NoteRef::from_query_result)
|
||||||
.collect()
|
.collect();
|
||||||
|
|
||||||
|
notes.extend(cur_notes);
|
||||||
} else {
|
} else {
|
||||||
debug!("got no results from TimelineCache lookup for {:?}", id);
|
debug!("got no results from TimelineCache lookup for {:?}", id);
|
||||||
vec![]
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notes
|
||||||
} else {
|
} else {
|
||||||
// filter is not ready yet
|
// filter is not ready yet
|
||||||
vec![]
|
vec![]
|
||||||
@@ -149,9 +161,12 @@ impl TimelineCache {
|
|||||||
info!("found NotesHolder with {} notes", notes.len());
|
info!("found NotesHolder with {} notes", notes.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.insert_new(id.to_owned(), txn, ndb, ¬es, note_cache);
|
let unknown_pks = self.insert_new(id.to_owned(), txn, ndb, ¬es, note_cache);
|
||||||
|
|
||||||
Vitality::Fresh(self.get_expected_mut(id))
|
GetNotesResponse {
|
||||||
|
vitality: Vitality::Fresh(self.get_expected_mut(id)),
|
||||||
|
unknown_pks,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a timeline, this is another way of saying insert a timeline
|
/// Open a timeline, this is another way of saying insert a timeline
|
||||||
@@ -166,15 +181,24 @@ impl TimelineCache {
|
|||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
id: &TimelineKind,
|
id: &TimelineKind,
|
||||||
) -> Option<TimelineOpenResult> {
|
) -> Option<TimelineOpenResult> {
|
||||||
let (open_result, timeline) = match self.notes(ndb, note_cache, txn, id) {
|
let notes_resp = self.notes(ndb, note_cache, txn, id);
|
||||||
|
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_notes(),
|
let mut notes = Vec::new();
|
||||||
timeline.subscription.get_filter()?.local(),
|
for package in timeline.subscription.get_filter()?.local().packages {
|
||||||
|
let cur_notes = find_new_notes(
|
||||||
|
timeline.all_or_any_entries().latest(),
|
||||||
|
package.filters,
|
||||||
txn,
|
txn,
|
||||||
ndb,
|
ndb,
|
||||||
);
|
);
|
||||||
|
notes.extend(cur_notes);
|
||||||
|
}
|
||||||
|
notes
|
||||||
|
};
|
||||||
|
|
||||||
let open_result = if notes.is_empty() {
|
let open_result = if notes.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -207,6 +231,13 @@ impl TimelineCache {
|
|||||||
|
|
||||||
timeline.subscription.increment();
|
timeline.subscription.increment();
|
||||||
|
|
||||||
|
if let Some(unknowns) = notes_resp.unknown_pks {
|
||||||
|
match &mut open_result {
|
||||||
|
Some(o) => o.insert_pks(unknowns.pks),
|
||||||
|
None => open_result = Some(TimelineOpenResult::new_pks(unknowns.pks)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open_result
|
open_result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,18 +262,22 @@ impl TimelineCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct GetNotesResponse<'a> {
|
||||||
|
vitality: Vitality<'a, Timeline>,
|
||||||
|
unknown_pks: Option<UnknownPksOwned>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Look for new thread notes since our last fetch
|
/// Look for new thread notes since our last fetch
|
||||||
fn find_new_notes(
|
fn find_new_notes(
|
||||||
notes: &[NoteRef],
|
latest: Option<&NoteRef>,
|
||||||
filters: &[Filter],
|
filters: &[Filter],
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
) -> Vec<NoteRef> {
|
) -> Vec<NoteRef> {
|
||||||
if notes.is_empty() {
|
let Some(last_note) = latest else {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
};
|
||||||
|
|
||||||
let last_note = notes[0];
|
|
||||||
let filters = filter::make_filters_since(filters, last_note.created_at + 1);
|
let filters = filter::make_filters_since(filters, last_note.created_at + 1);
|
||||||
|
|
||||||
if let Ok(results) = ndb.query(txn, &filters, 1000) {
|
if let Ok(results) = ndb.query(txn, &filters, 1000) {
|
||||||
|
|||||||
@@ -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},
|
||||||
@@ -625,7 +626,7 @@ impl TimelineKind {
|
|||||||
pub fn notifications_filter(pk: &Pubkey) -> Filter {
|
pub fn notifications_filter(pk: &Pubkey) -> Filter {
|
||||||
Filter::new()
|
Filter::new()
|
||||||
.pubkeys([pk.bytes()])
|
.pubkeys([pk.bytes()])
|
||||||
.kinds([1])
|
.kinds([1, 7, 6])
|
||||||
.limit(default_limit())
|
.limit(default_limit())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
HybridFilter::split(
|
let local = vec![
|
||||||
vec![Filter::new()
|
NdbQueryPackage {
|
||||||
|
filters: vec![Filter::new()
|
||||||
.authors([pk])
|
.authors([pk])
|
||||||
.kinds([1])
|
.kinds([1])
|
||||||
.limit(default_limit())
|
.limit(default_limit())
|
||||||
.build()],
|
.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()
|
vec![Filter::new()
|
||||||
.authors([pk])
|
.authors([pk])
|
||||||
.kinds([1, 0])
|
.kinds([1, 6, 0])
|
||||||
.limit(default_remote_limit())
|
.limit(default_remote_limit())
|
||||||
.build()],
|
.build()],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{
|
|||||||
error::Error,
|
error::Error,
|
||||||
multi_subscriber::TimelineSub,
|
multi_subscriber::TimelineSub,
|
||||||
subscriptions::{self, SubKind, Subscriptions},
|
subscriptions::{self, SubKind, Subscriptions},
|
||||||
timeline::kind::ListKind,
|
timeline::{kind::ListKind, note_units::InsertManyResponse, timeline_units::NotePayload},
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ use enostr::{PoolRelay, Pubkey, RelayPool};
|
|||||||
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
|
use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
collections::HashSet,
|
||||||
time::{Duration, UNIX_EPOCH},
|
time::{Duration, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
use std::{rc::Rc, time::SystemTime};
|
use std::{rc::Rc, time::SystemTime};
|
||||||
@@ -27,37 +28,17 @@ use tracing::{debug, error, info, warn};
|
|||||||
|
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod kind;
|
pub mod kind;
|
||||||
|
mod note_units;
|
||||||
pub mod route;
|
pub mod route;
|
||||||
pub mod thread;
|
pub mod thread;
|
||||||
|
mod timeline_units;
|
||||||
|
mod unit;
|
||||||
|
|
||||||
pub use cache::TimelineCache;
|
pub use cache::TimelineCache;
|
||||||
pub use kind::{ColumnTitle, PubkeySource, ThreadSelection, TimelineKind};
|
pub use kind::{ColumnTitle, PubkeySource, ThreadSelection, TimelineKind};
|
||||||
|
pub use note_units::{CompositeType, InsertionResponse, NoteUnits};
|
||||||
//#[derive(Debug, Hash, Clone, Eq, PartialEq)]
|
pub use timeline_units::{TimelineUnits, UnknownPks};
|
||||||
//pub type TimelineId = TimelineKind;
|
pub use unit::{CompositeUnit, NoteUnit, ReactionUnit, RepostUnit};
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
impl TimelineId {
|
|
||||||
pub fn kind(&self) -> &TimelineKind {
|
|
||||||
&self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(id: TimelineKind) -> Self {
|
|
||||||
TimelineId(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn profile(pubkey: Pubkey) -> Self {
|
|
||||||
TimelineId::new(TimelineKind::Profile(PubkeySource::pubkey(pubkey)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for TimelineId {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "TimelineId({})", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||||||
pub enum ViewFilter {
|
pub enum ViewFilter {
|
||||||
@@ -82,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 {
|
||||||
@@ -103,7 +84,7 @@ impl ViewFilter {
|
|||||||
/// be captured by a Filter itself.
|
/// be captured by a Filter itself.
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct TimelineTab {
|
pub struct TimelineTab {
|
||||||
pub notes: Vec<NoteRef>,
|
pub units: TimelineUnits,
|
||||||
pub selection: i32,
|
pub selection: i32,
|
||||||
pub filter: ViewFilter,
|
pub filter: ViewFilter,
|
||||||
pub list: Rc<RefCell<VirtualList>>,
|
pub list: Rc<RefCell<VirtualList>>,
|
||||||
@@ -136,10 +117,9 @@ impl TimelineTab {
|
|||||||
list.hide_on_resize(None);
|
list.hide_on_resize(None);
|
||||||
list.over_scan(50.0);
|
list.over_scan(50.0);
|
||||||
let list = Rc::new(RefCell::new(list));
|
let list = Rc::new(RefCell::new(list));
|
||||||
let notes: Vec<NoteRef> = Vec::with_capacity(cap);
|
|
||||||
|
|
||||||
TimelineTab {
|
TimelineTab {
|
||||||
notes,
|
units: TimelineUnits::with_capacity(cap),
|
||||||
selection,
|
selection,
|
||||||
filter,
|
filter,
|
||||||
list,
|
list,
|
||||||
@@ -147,45 +127,59 @@ impl TimelineTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) {
|
fn insert<'a>(
|
||||||
if new_refs.is_empty() {
|
&mut self,
|
||||||
return;
|
payloads: Vec<&'a NotePayload>,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
reversed: bool,
|
||||||
|
use_front_insert: bool,
|
||||||
|
) -> Option<UnknownPks<'a>> {
|
||||||
|
if payloads.is_empty() {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
let num_prev_items = self.notes.len();
|
|
||||||
let (notes, merge_kind) = crate::timeline::merge_sorted_vecs(&self.notes, new_refs);
|
|
||||||
|
|
||||||
self.notes = notes;
|
let num_refs = payloads.len();
|
||||||
let new_items = self.notes.len() - num_prev_items;
|
|
||||||
|
let resp = self.units.merge_new_notes(payloads, ndb, txn);
|
||||||
|
|
||||||
|
let InsertManyResponse::Some {
|
||||||
|
entries_merged,
|
||||||
|
merge_kind,
|
||||||
|
} = resp.insertion_response
|
||||||
|
else {
|
||||||
|
return resp.tl_response;
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: technically items could have been added inbetween
|
|
||||||
if new_items > 0 {
|
|
||||||
let mut list = self.list.borrow_mut();
|
let mut list = self.list.borrow_mut();
|
||||||
|
|
||||||
match merge_kind {
|
match merge_kind {
|
||||||
// TODO: update egui_virtual_list to support spliced inserts
|
// TODO: update egui_virtual_list to support spliced inserts
|
||||||
MergeKind::Spliced => {
|
MergeKind::Spliced => {
|
||||||
debug!(
|
debug!("spliced when inserting {num_refs} new notes, resetting virtual list",);
|
||||||
"spliced when inserting {} new notes, resetting virtual list",
|
|
||||||
new_refs.len()
|
|
||||||
);
|
|
||||||
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.
|
||||||
if !reversed {
|
if !reversed {
|
||||||
debug!("inserting {} new notes at start", new_refs.len());
|
debug!("inserting {num_refs} new notes at start");
|
||||||
list.items_inserted_at_start(new_items);
|
list.items_inserted_at_start(entries_merged);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resp.tl_response
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_down(&mut self) {
|
pub fn select_down(&mut self) {
|
||||||
debug!("select_down {}", self.selection + 1);
|
debug!("select_down {}", self.selection + 1);
|
||||||
if self.selection + 1 > self.notes.len() as i32 {
|
if self.selection + 1 > self.units.len() as i32 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +196,14 @@ impl TimelineTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> UnknownPks<'a> {
|
||||||
|
pub fn process(&self, unknown_ids: &mut UnknownIds, ndb: &Ndb, txn: &Transaction) {
|
||||||
|
for pk in &self.unknown_pks {
|
||||||
|
unknown_ids.add_pubkey_if_missing(ndb, txn, pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A column in a deck. Holds navigation state, loaded notes, column kind, etc.
|
/// A column in a deck. Holds navigation state, loaded notes, column kind, etc.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Timeline {
|
pub struct Timeline {
|
||||||
@@ -213,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 {
|
||||||
@@ -274,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,15 +300,20 @@ impl Timeline {
|
|||||||
|
|
||||||
/// Get the note refs for NotesAndReplies. If we only have Notes, then
|
/// Get the note refs for NotesAndReplies. If we only have Notes, then
|
||||||
/// just return that instead
|
/// just return that instead
|
||||||
pub fn all_or_any_notes(&self) -> &[NoteRef] {
|
pub fn all_or_any_entries(&self) -> &TimelineUnits {
|
||||||
self.notes(ViewFilter::NotesAndReplies).unwrap_or_else(|| {
|
self.entries(ViewFilter::NotesAndReplies)
|
||||||
self.notes(ViewFilter::Notes)
|
.unwrap_or_else(|| {
|
||||||
|
self.entries(ViewFilter::Notes)
|
||||||
.expect("should have at least notes")
|
.expect("should have at least notes")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notes(&self, view: ViewFilter) -> Option<&[NoteRef]> {
|
pub fn entries(&self, view: ViewFilter) -> Option<&TimelineUnits> {
|
||||||
self.view(view).map(|v| &*v.notes)
|
self.view(view).map(|v| &v.units)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_note(&self, view: ViewFilter) -> Option<&NoteRef> {
|
||||||
|
self.view(view).and_then(|v| v.units.latest())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self, view: ViewFilter) -> Option<&TimelineTab> {
|
pub fn view(&self, view: ViewFilter) -> Option<&TimelineTab> {
|
||||||
@@ -320,7 +332,7 @@ impl Timeline {
|
|||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
notes: &[NoteRef],
|
notes: &[NoteRef],
|
||||||
) {
|
) -> Option<UnknownPksOwned> {
|
||||||
let filters = {
|
let filters = {
|
||||||
let views = &self.views;
|
let views = &self.views;
|
||||||
let filters: Vec<fn(&CachedNote, &Note) -> bool> =
|
let filters: Vec<fn(&CachedNote, &Note) -> bool> =
|
||||||
@@ -328,6 +340,7 @@ impl Timeline {
|
|||||||
filters
|
filters
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut unknown_pks = HashSet::new();
|
||||||
for note_ref in notes {
|
for note_ref in notes {
|
||||||
for (view, filter) in filters.iter().enumerate() {
|
for (view, filter) in filters.iter().enumerate() {
|
||||||
if let Ok(note) = ndb.get_note_by_key(txn, note_ref.key) {
|
if let Ok(note) = ndb.get_note_by_key(txn, note_ref.key) {
|
||||||
@@ -335,13 +348,34 @@ impl Timeline {
|
|||||||
note_cache.cached_note_or_insert_mut(note_ref.key, ¬e),
|
note_cache.cached_note_or_insert_mut(note_ref.key, ¬e),
|
||||||
¬e,
|
¬e,
|
||||||
) {
|
) {
|
||||||
self.views[view].notes.push(*note_ref)
|
if let Some(resp) = self.views[view]
|
||||||
|
.units
|
||||||
|
.merge_new_notes(
|
||||||
|
vec![&NotePayload {
|
||||||
|
note,
|
||||||
|
key: note_ref.key,
|
||||||
|
}],
|
||||||
|
ndb,
|
||||||
|
txn,
|
||||||
|
)
|
||||||
|
.tl_response
|
||||||
|
{
|
||||||
|
let pks: HashSet<Pubkey> = resp
|
||||||
|
.unknown_pks
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| Pubkey::new(*r))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
unknown_pks.extend(pks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(UnknownPksOwned { pks: unknown_pks })
|
||||||
|
}
|
||||||
|
|
||||||
/// The main function used for inserting notes into timelines. Handles
|
/// The main function used for inserting notes into timelines. Handles
|
||||||
/// inserting into multiple views if we have them. All timeline note
|
/// inserting into multiple views if we have them. All timeline note
|
||||||
/// insertions should use this function.
|
/// insertions should use this function.
|
||||||
@@ -354,7 +388,7 @@ impl Timeline {
|
|||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
reversed: bool,
|
reversed: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len());
|
let mut payloads: Vec<NotePayload> = Vec::with_capacity(new_note_ids.len());
|
||||||
|
|
||||||
for key in new_note_ids {
|
for key in new_note_ids {
|
||||||
let note = if let Ok(note) = ndb.get_note_by_key(txn, *key) {
|
let note = if let Ok(note) = ndb.get_note_by_key(txn, *key) {
|
||||||
@@ -371,35 +405,40 @@ impl Timeline {
|
|||||||
// into the timeline
|
// into the timeline
|
||||||
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e);
|
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e);
|
||||||
|
|
||||||
let created_at = note.created_at();
|
payloads.push(NotePayload { note, key: *key });
|
||||||
new_refs.push((
|
|
||||||
note,
|
|
||||||
NoteRef {
|
|
||||||
key: *key,
|
|
||||||
created_at,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for view in &mut self.views {
|
for view in &mut self.views {
|
||||||
match view.filter {
|
match view.filter {
|
||||||
ViewFilter::NotesAndReplies => {
|
ViewFilter::NotesAndReplies => {
|
||||||
let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
|
let res: Vec<&NotePayload<'_>> = payloads.iter().collect();
|
||||||
|
if let Some(res) =
|
||||||
view.insert(&refs, reversed);
|
view.insert(res, ndb, txn, reversed, self.enable_front_insert)
|
||||||
|
{
|
||||||
|
res.process(unknown_ids, ndb, txn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewFilter::Notes => {
|
ViewFilter::Notes => {
|
||||||
let mut filtered_refs = Vec::with_capacity(new_refs.len());
|
let mut filtered_payloads = Vec::with_capacity(payloads.len());
|
||||||
for (note, nr) in &new_refs {
|
for payload in &payloads {
|
||||||
let cached_note = note_cache.cached_note_or_insert(nr.key, note);
|
let cached_note =
|
||||||
|
note_cache.cached_note_or_insert(payload.key, &payload.note);
|
||||||
|
|
||||||
if ViewFilter::filter_notes(cached_note, note) {
|
if ViewFilter::filter_notes(cached_note, &payload.note) {
|
||||||
filtered_refs.push(*nr);
|
filtered_payloads.push(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.insert(&filtered_refs, reversed);
|
if let Some(res) = view.insert(
|
||||||
|
filtered_payloads,
|
||||||
|
ndb,
|
||||||
|
txn,
|
||||||
|
reversed,
|
||||||
|
self.enable_front_insert,
|
||||||
|
) {
|
||||||
|
res.process(unknown_ids, ndb, txn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,6 +475,18 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct UnknownPksOwned {
|
||||||
|
pub pks: HashSet<Pubkey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnknownPksOwned {
|
||||||
|
pub fn process(&self, ndb: &Ndb, txn: &Transaction, unknown_ids: &mut UnknownIds) {
|
||||||
|
self.pks
|
||||||
|
.iter()
|
||||||
|
.for_each(|p| unknown_ids.add_pubkey_if_missing(ndb, txn, p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum MergeKind {
|
pub enum MergeKind {
|
||||||
FrontInsert,
|
FrontInsert,
|
||||||
Spliced,
|
Spliced,
|
||||||
@@ -492,10 +543,11 @@ pub fn setup_new_timeline(
|
|||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
since_optimize: bool,
|
since_optimize: bool,
|
||||||
accounts: &Accounts,
|
accounts: &Accounts,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
) {
|
) {
|
||||||
// if we're ready, setup local subs
|
// if we're ready, setup local subs
|
||||||
if is_timeline_ready(ndb, pool, note_cache, timeline, accounts) {
|
if is_timeline_ready(ndb, pool, note_cache, timeline, accounts, unknown_ids) {
|
||||||
if let Err(err) = setup_timeline_nostrdb_sub(ndb, txn, note_cache, timeline) {
|
if let Err(err) = setup_timeline_nostrdb_sub(ndb, txn, note_cache, timeline, unknown_ids) {
|
||||||
error!("setup_new_timeline: {err}");
|
error!("setup_new_timeline: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +616,7 @@ pub fn send_initial_timeline_filter(
|
|||||||
filter = filter.limit_mut(lim);
|
filter = filter.limit_mut(lim);
|
||||||
}
|
}
|
||||||
|
|
||||||
let notes = timeline.all_or_any_notes();
|
let entries = timeline.all_or_any_entries();
|
||||||
|
|
||||||
// Should we since optimize? Not always. For example
|
// Should we since optimize? Not always. For example
|
||||||
// if we only have a few notes locally. One way to
|
// if we only have a few notes locally. One way to
|
||||||
@@ -572,8 +624,8 @@ pub fn send_initial_timeline_filter(
|
|||||||
// and seeing what its limit is. If we have less
|
// and seeing what its limit is. If we have less
|
||||||
// notes than the limit, we might want to backfill
|
// notes than the limit, we might want to backfill
|
||||||
// older notes
|
// older notes
|
||||||
if can_since_optimize && filter::should_since_optimize(lim, notes.len()) {
|
if can_since_optimize && filter::should_since_optimize(lim, entries.len()) {
|
||||||
filter = filter::since_optimize_filter(filter, notes);
|
filter = filter::since_optimize_filter(filter, entries.latest());
|
||||||
} else {
|
} else {
|
||||||
warn!("Skipping since optimization for {:?}: number of local notes is less than limit, attempting to backfill.", &timeline.kind);
|
warn!("Skipping since optimization for {:?}: number of local notes is less than limit, attempting to backfill.", &timeline.kind);
|
||||||
}
|
}
|
||||||
@@ -629,6 +681,7 @@ fn setup_initial_timeline(
|
|||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
timeline: &mut Timeline,
|
timeline: &mut Timeline,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
filters: &HybridFilter,
|
filters: &HybridFilter,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// some timelines are one-shot and a refreshed, like last_per_pubkey algo feed
|
// some timelines are one-shot and a refreshed, like last_per_pubkey algo feed
|
||||||
@@ -641,20 +694,36 @@ fn setup_initial_timeline(
|
|||||||
timeline.subscription, timeline.filter
|
timeline.subscription, timeline.filter
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let notes = {
|
||||||
|
let mut notes = Vec::new();
|
||||||
|
|
||||||
|
for package in filters.local().packages {
|
||||||
let mut lim = 0i32;
|
let mut lim = 0i32;
|
||||||
for filter in filters.local() {
|
for filter in package.filters {
|
||||||
lim += filter.limit().unwrap_or(1) as i32;
|
lim += filter.limit().unwrap_or(1) as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("setup_initial_timeline: limit for local filter is {}", lim);
|
debug!("setup_initial_timeline: limit for local filter is {}", lim);
|
||||||
|
|
||||||
let notes: Vec<NoteRef> = ndb
|
let cur_notes: Vec<NoteRef> = ndb
|
||||||
.query(txn, filters.local(), lim)?
|
.query(txn, package.filters, lim)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(NoteRef::from_query_result)
|
.map(NoteRef::from_query_result)
|
||||||
.collect();
|
.collect();
|
||||||
|
tracing::debug!(
|
||||||
|
"Found {} notes for kind: {:?}",
|
||||||
|
cur_notes.len(),
|
||||||
|
package.kind
|
||||||
|
);
|
||||||
|
notes.extend(&cur_notes);
|
||||||
|
}
|
||||||
|
|
||||||
timeline.insert_new(txn, ndb, note_cache, ¬es);
|
notes
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(pks) = timeline.insert_new(txn, ndb, note_cache, ¬es) {
|
||||||
|
pks.process(ndb, txn, unknown_ids);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -663,10 +732,11 @@ pub fn setup_initial_nostrdb_subs(
|
|||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for (_kind, timeline) in timeline_cache {
|
for (_kind, timeline) in timeline_cache {
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
if let Err(err) = setup_timeline_nostrdb_sub(ndb, &txn, note_cache, timeline) {
|
if let Err(err) = setup_timeline_nostrdb_sub(ndb, &txn, note_cache, timeline, unknown_ids) {
|
||||||
error!("setup_initial_nostrdb_subs: {err}");
|
error!("setup_initial_nostrdb_subs: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,6 +749,7 @@ fn setup_timeline_nostrdb_sub(
|
|||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
timeline: &mut Timeline,
|
timeline: &mut Timeline,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let filter_state = timeline
|
let filter_state = timeline
|
||||||
.filter
|
.filter
|
||||||
@@ -686,7 +757,7 @@ fn setup_timeline_nostrdb_sub(
|
|||||||
.ok_or(Error::App(notedeck::Error::empty_contact_list()))?
|
.ok_or(Error::App(notedeck::Error::empty_contact_list()))?
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
setup_initial_timeline(ndb, txn, timeline, note_cache, &filter_state)?;
|
setup_initial_timeline(ndb, txn, timeline, note_cache, unknown_ids, &filter_state)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -701,6 +772,7 @@ pub fn is_timeline_ready(
|
|||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
timeline: &mut Timeline,
|
timeline: &mut Timeline,
|
||||||
accounts: &Accounts,
|
accounts: &Accounts,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// TODO: we should debounce the filter states a bit to make sure we have
|
// TODO: we should debounce the filter states a bit to make sure we have
|
||||||
// seen all of the different contact lists from each relay
|
// seen all of the different contact lists from each relay
|
||||||
@@ -774,7 +846,8 @@ pub fn is_timeline_ready(
|
|||||||
// queries and setup the local subscription
|
// queries and setup the local subscription
|
||||||
info!("Found contact list! Setting up local and remote contact list query");
|
info!("Found contact list! Setting up local and remote contact list query");
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
setup_initial_timeline(ndb, &txn, timeline, note_cache, &filter).expect("setup init");
|
setup_initial_timeline(ndb, &txn, timeline, note_cache, unknown_ids, &filter)
|
||||||
|
.expect("setup init");
|
||||||
timeline
|
timeline
|
||||||
.filter
|
.filter
|
||||||
.set_relay_state(relay_id, FilterState::ready_hybrid(filter.clone()));
|
.set_relay_state(relay_id, FilterState::ready_hybrid(filter.clone()));
|
||||||
|
|||||||
@@ -0,0 +1,677 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use nostrdb::NoteKey;
|
||||||
|
use notedeck::NoteRef;
|
||||||
|
|
||||||
|
use crate::timeline::{
|
||||||
|
unit::{CompositeUnit, NoteUnit, NoteUnitFragment},
|
||||||
|
MergeKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
type StorageIndex = usize;
|
||||||
|
|
||||||
|
/// Provides efficient access to `NoteUnit`s
|
||||||
|
/// Useful for threads and timelines
|
||||||
|
/// when reversed=false, sorts from newest to oldest
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct NoteUnits {
|
||||||
|
reversed: bool,
|
||||||
|
storage: Vec<NoteUnit>,
|
||||||
|
lookup: HashMap<UnitKey, StorageIndex>, // the key to index in `NoteUnits::storage`
|
||||||
|
order: Vec<StorageIndex>, // the sorted order of the `NoteUnit`s in `NoteUnits::storage`
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteUnits {
|
||||||
|
pub fn values(&self) -> Values<'_> {
|
||||||
|
Values {
|
||||||
|
set: self,
|
||||||
|
front: 0,
|
||||||
|
back: self.order.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_key(&self, k: &UnitKey) -> bool {
|
||||||
|
self.lookup.contains_key(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_cap(cap: usize, reversed: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
reversed,
|
||||||
|
storage: Vec::with_capacity(cap),
|
||||||
|
lookup: HashMap::with_capacity(cap),
|
||||||
|
order: Vec::with_capacity(cap),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.storage.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.storage.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the kth index from 0..Self::len
|
||||||
|
pub fn kth(&self, k: usize) -> Option<&NoteUnit> {
|
||||||
|
if k >= self.order.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let idx = if self.reversed {
|
||||||
|
self.order[self.order.len() - 1 - k]
|
||||||
|
} else {
|
||||||
|
self.order[k]
|
||||||
|
};
|
||||||
|
Some(&self.storage[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core bulk insert for already-built `NoteUnit`s
|
||||||
|
/// Merges new `NoteUnit`s into `Self::storage`
|
||||||
|
/// Updates `Self::order`
|
||||||
|
fn merge_many_internal(
|
||||||
|
&mut self,
|
||||||
|
mut units: Vec<NoteUnit>,
|
||||||
|
touched_indices: &[usize],
|
||||||
|
) -> InsertManyResponse {
|
||||||
|
units.retain(|e| !self.lookup.contains_key(&e.key()));
|
||||||
|
if units.is_empty() && touched_indices.is_empty() {
|
||||||
|
return InsertManyResponse::Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut touched = Vec::new();
|
||||||
|
if !touched_indices.is_empty() {
|
||||||
|
touched = touched_indices.to_vec();
|
||||||
|
touched.sort_unstable(); // sort for later reinsertion
|
||||||
|
touched.dedup();
|
||||||
|
self.order.retain(|i| touched.binary_search(i).is_err()); // temporarily remove touched from Self::order
|
||||||
|
}
|
||||||
|
|
||||||
|
units.sort_unstable();
|
||||||
|
units.dedup_by_key(|u| u.key());
|
||||||
|
|
||||||
|
let base = self.storage.len();
|
||||||
|
let mut new_order = Vec::with_capacity(units.len());
|
||||||
|
self.storage.reserve(units.len());
|
||||||
|
for (i, unit) in units.into_iter().enumerate() {
|
||||||
|
let idx = base + i;
|
||||||
|
let key = unit.key();
|
||||||
|
self.storage.push(unit);
|
||||||
|
self.lookup.insert(key, idx);
|
||||||
|
new_order.push(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inserted_new = new_order.len();
|
||||||
|
|
||||||
|
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_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 {
|
||||||
|
merged.push(index_right);
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.extend_from_slice(&self.order[i..]);
|
||||||
|
merged.extend_from_slice(&new_order[j..]);
|
||||||
|
|
||||||
|
// reinsert touched
|
||||||
|
for touched_index in touched {
|
||||||
|
let pos = merged
|
||||||
|
.binary_search_by(|&i2| self.storage[i2].cmp(&self.storage[touched_index]))
|
||||||
|
.unwrap_or_else(|p| p);
|
||||||
|
merged.insert(pos, touched_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.order = merged;
|
||||||
|
|
||||||
|
if inserted_new == 0 {
|
||||||
|
InsertManyResponse::Zero
|
||||||
|
} else if front_insertion {
|
||||||
|
InsertManyResponse::Some {
|
||||||
|
entries_merged: inserted_new,
|
||||||
|
merge_kind: MergeKind::FrontInsert,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
InsertManyResponse::Some {
|
||||||
|
entries_merged: inserted_new,
|
||||||
|
merge_kind: MergeKind::Spliced,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges `NoteUnitFragment`s
|
||||||
|
/// `NoteUnitFragment::Single` is added normally
|
||||||
|
/// if `NoteUnitFragment::Composite` exists already, it will fold the fragment into the `CompositeUnit`
|
||||||
|
/// otherwise, it will generate the `NoteUnit::CompositeUnit` from the `NoteUnitFragment::Composite`
|
||||||
|
pub fn merge_fragments(&mut self, frags: Vec<NoteUnitFragment>) -> InsertManyResponse {
|
||||||
|
let mut to_build: HashMap<CompositeKey, CompositeUnit> = HashMap::new(); // new composites by key
|
||||||
|
let mut singles_to_build: Vec<NoteRef> = Vec::new();
|
||||||
|
let mut singles_seen: HashSet<NoteKey> = HashSet::new();
|
||||||
|
|
||||||
|
let mut touched = Vec::new();
|
||||||
|
for frag in frags {
|
||||||
|
match frag {
|
||||||
|
NoteUnitFragment::Single(note_ref) => {
|
||||||
|
let key = note_ref.key;
|
||||||
|
if self.lookup.contains_key(&UnitKey::Single(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if singles_seen.insert(key) {
|
||||||
|
singles_to_build.push(note_ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NoteUnitFragment::Composite(c_frag) => {
|
||||||
|
let key = c_frag.get_underlying_noteref().key;
|
||||||
|
let composite_type = c_frag.get_type();
|
||||||
|
|
||||||
|
if let Some(&storage_idx) = self.lookup.get(&UnitKey::Composite(c_frag.key())) {
|
||||||
|
if let Some(NoteUnit::Composite(c_unit)) = self.storage.get_mut(storage_idx)
|
||||||
|
{
|
||||||
|
if c_frag.get_latest_ref() < c_unit.get_latest_ref() {
|
||||||
|
touched.push(storage_idx);
|
||||||
|
}
|
||||||
|
c_frag.fold_into(c_unit);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// aggregate for new composite
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
match to_build.entry(CompositeKey {
|
||||||
|
key,
|
||||||
|
composite_type,
|
||||||
|
}) {
|
||||||
|
Entry::Occupied(mut o) => {
|
||||||
|
c_frag.fold_into(o.get_mut());
|
||||||
|
}
|
||||||
|
Entry::Vacant(v) => {
|
||||||
|
v.insert(c_frag.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut items: Vec<NoteUnit> = Vec::with_capacity(singles_to_build.len() + to_build.len());
|
||||||
|
items.extend(singles_to_build.into_iter().map(NoteUnit::Single));
|
||||||
|
items.extend(to_build.into_values().map(NoteUnit::Composite));
|
||||||
|
|
||||||
|
self.merge_many_internal(items, &touched)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convienience method to merge a single note
|
||||||
|
pub fn merge_single_unit(&mut self, note_ref: NoteRef) -> InsertionResponse {
|
||||||
|
match self.merge_many_internal(vec![NoteUnit::Single(note_ref)], &[]) {
|
||||||
|
InsertManyResponse::Zero => InsertionResponse::AlreadyExists,
|
||||||
|
InsertManyResponse::Some {
|
||||||
|
entries_merged: _,
|
||||||
|
merge_kind,
|
||||||
|
} => InsertionResponse::Merged(merge_kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_ref(&self) -> Option<&NoteRef> {
|
||||||
|
if self.reversed {
|
||||||
|
self.order.last().map(|&i| &self.storage[i])
|
||||||
|
} else {
|
||||||
|
self.order.first().map(|&i| &self.storage[i])
|
||||||
|
}
|
||||||
|
.map(NoteUnit::get_latest_ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||||
|
pub struct CompositeKey {
|
||||||
|
pub key: NoteKey,
|
||||||
|
pub composite_type: CompositeType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||||
|
pub enum CompositeType {
|
||||||
|
Reaction,
|
||||||
|
Repost,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Hash, PartialEq, Eq, Debug)]
|
||||||
|
pub enum UnitKey {
|
||||||
|
Single(NoteKey),
|
||||||
|
Composite(CompositeKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InsertManyResponse {
|
||||||
|
Zero,
|
||||||
|
Some {
|
||||||
|
entries_merged: usize,
|
||||||
|
merge_kind: MergeKind,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Values<'a> {
|
||||||
|
set: &'a NoteUnits,
|
||||||
|
front: usize,
|
||||||
|
back: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for Values<'a> {
|
||||||
|
type Item = &'a NoteUnit;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.front >= self.back {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let idx = if !self.set.reversed {
|
||||||
|
let i = self.front;
|
||||||
|
self.front += 1;
|
||||||
|
self.set.order[i]
|
||||||
|
} else {
|
||||||
|
self.back -= 1;
|
||||||
|
self.set.order[self.back]
|
||||||
|
};
|
||||||
|
Some(&self.set.storage[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DoubleEndedIterator for Values<'a> {
|
||||||
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.front >= self.back {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let idx = if !self.set.reversed {
|
||||||
|
self.back -= 1;
|
||||||
|
self.set.order[self.back]
|
||||||
|
} else {
|
||||||
|
let i = self.front;
|
||||||
|
self.front += 1;
|
||||||
|
self.set.order[i]
|
||||||
|
};
|
||||||
|
Some(&self.set.storage[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InsertionResponse {
|
||||||
|
AlreadyExists,
|
||||||
|
Merged(MergeKind),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
use egui::ahash::HashMap;
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::NoteKey;
|
||||||
|
use notedeck::NoteRef;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::timeline::{
|
||||||
|
unit::{
|
||||||
|
CompositeFragment, CompositeUnit, NoteUnit, NoteUnitFragment, Reaction,
|
||||||
|
ReactionFragment, ReactionUnit, RepostFragment,
|
||||||
|
},
|
||||||
|
NoteUnits, RepostUnit,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct UnitBuilder {
|
||||||
|
counter: u64,
|
||||||
|
frags: HashMap<String, NoteUnitFragment>,
|
||||||
|
units: NoteUnits,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnitBuilder {
|
||||||
|
fn counter(&mut self) -> u64 {
|
||||||
|
let res = self.counter;
|
||||||
|
self.counter += 1;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_sender(&mut self) -> Pubkey {
|
||||||
|
let mut out = [0u8; 32];
|
||||||
|
out[..8].copy_from_slice(&self.counter().to_le_bytes());
|
||||||
|
|
||||||
|
Pubkey::new(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_reac_frag(&mut self, reacted_to: NoteRef) -> NoteUnitFragment {
|
||||||
|
NoteUnitFragment::Composite(CompositeFragment::Reaction(ReactionFragment {
|
||||||
|
noteref_reacted_to: reacted_to,
|
||||||
|
reaction_note_ref: NoteRef {
|
||||||
|
key: NoteKey::new(self.counter()),
|
||||||
|
created_at: self.counter(),
|
||||||
|
},
|
||||||
|
reaction: Reaction {
|
||||||
|
reaction: "+".to_owned(),
|
||||||
|
sender: self.random_sender(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_repost_frag(&mut self, reposting: NoteRef) -> NoteUnitFragment {
|
||||||
|
NoteUnitFragment::Composite(CompositeFragment::Repost(RepostFragment {
|
||||||
|
reposted_noteref: reposting,
|
||||||
|
repost_noteref: self.new_noteref(),
|
||||||
|
reposter: self.random_sender(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_repost(&mut self, reposting: NoteRef) -> String {
|
||||||
|
let repost = self.build_repost_frag(reposting);
|
||||||
|
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
self.frags.insert(id.clone(), repost.clone());
|
||||||
|
|
||||||
|
self.units.merge_fragments(vec![repost]);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_reac_frag(&mut self, reacted_to: NoteRef) -> String {
|
||||||
|
let frag = self.build_reac_frag(reacted_to);
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
self.frags.insert(id.clone(), frag.clone());
|
||||||
|
|
||||||
|
self.units.merge_fragments(vec![frag]);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_reac_frag_pair(&mut self, reacted_to: NoteRef) -> (String, String) {
|
||||||
|
let frag1 = self.build_reac_frag(reacted_to);
|
||||||
|
let frag2 = self.build_reac_frag(reacted_to);
|
||||||
|
|
||||||
|
self.units
|
||||||
|
.merge_fragments(vec![frag1.clone(), frag2.clone()]);
|
||||||
|
|
||||||
|
let id1 = Uuid::new_v4().to_string();
|
||||||
|
self.frags.insert(id1.clone(), frag1);
|
||||||
|
let id2 = Uuid::new_v4().to_string();
|
||||||
|
self.frags.insert(id2.clone(), frag2);
|
||||||
|
|
||||||
|
(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_noteref(&mut self) -> NoteRef {
|
||||||
|
NoteRef {
|
||||||
|
key: NoteKey::new(self.counter()),
|
||||||
|
created_at: self.counter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_note(&mut self) -> String {
|
||||||
|
let note_ref = NoteRef {
|
||||||
|
key: NoteKey::new(self.counter()),
|
||||||
|
created_at: self.counter(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
self.frags
|
||||||
|
.insert(id.clone(), NoteUnitFragment::Single(note_ref.clone()));
|
||||||
|
|
||||||
|
self.units.merge_single_unit(note_ref);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_reactions(&mut self, ids: Vec<&String>) -> NoteUnit {
|
||||||
|
let mut reactions = BTreeMap::new();
|
||||||
|
let mut reaction_id = None;
|
||||||
|
let mut senders = HashSet::new();
|
||||||
|
for id in ids {
|
||||||
|
let NoteUnitFragment::Composite(CompositeFragment::Reaction(reac)) =
|
||||||
|
self.frags.get(id).unwrap()
|
||||||
|
else {
|
||||||
|
panic!("got something other than reaction");
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(prev_reac_id) = reaction_id {
|
||||||
|
if prev_reac_id != reac.noteref_reacted_to {
|
||||||
|
panic!("internal error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reaction_id = Some(reac.noteref_reacted_to);
|
||||||
|
|
||||||
|
reactions.insert(reac.reaction_note_ref, reac.reaction.clone());
|
||||||
|
senders.insert(reac.reaction.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteUnit::Composite(CompositeUnit::Reaction(ReactionUnit {
|
||||||
|
note_reacted_to: reaction_id.unwrap(),
|
||||||
|
reactions,
|
||||||
|
senders: senders,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_reposts(&mut self, ids: Vec<&String>) -> NoteUnit {
|
||||||
|
let mut reposts = BTreeMap::new();
|
||||||
|
let mut reposted_id = None;
|
||||||
|
let mut senders = HashSet::new();
|
||||||
|
for id in ids {
|
||||||
|
let NoteUnitFragment::Composite(CompositeFragment::Repost(repost)) =
|
||||||
|
self.frags.get(id).unwrap()
|
||||||
|
else {
|
||||||
|
panic!("got something other than repost");
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(prev_reposted_id) = reposted_id {
|
||||||
|
if prev_reposted_id != repost.reposted_noteref {
|
||||||
|
panic!("internal error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reposted_id = Some(repost.reposted_noteref);
|
||||||
|
|
||||||
|
reposts.insert(repost.repost_noteref, repost.reposter);
|
||||||
|
senders.insert(repost.reposter);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteUnit::Composite(CompositeUnit::Repost(RepostUnit {
|
||||||
|
note_reposted: reposted_id.unwrap(),
|
||||||
|
reposts,
|
||||||
|
senders,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_single(&mut self, id: &String) -> NoteUnit {
|
||||||
|
let Some(NoteUnitFragment::Single(note_ref)) = self.frags.get(id) else {
|
||||||
|
panic!("fail");
|
||||||
|
};
|
||||||
|
|
||||||
|
NoteUnit::Single(*note_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn asserted_at(&self, index: usize) -> NoteUnit {
|
||||||
|
self.units.kth(index).unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aeq(&mut self, units_kth: usize, expect: Expect) {
|
||||||
|
assert_eq!(
|
||||||
|
self.asserted_at(units_kth),
|
||||||
|
match expect {
|
||||||
|
Expect::Single(id) => self.expected_single(id),
|
||||||
|
Expect::Reaction(items) => self.expected_reactions(items),
|
||||||
|
Expect::Repost(items) => self.expected_reposts(items),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Expect<'a> {
|
||||||
|
Single(&'a String),
|
||||||
|
Reaction(Vec<&'a String>),
|
||||||
|
Repost(Vec<&'a String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reactions1() {
|
||||||
|
let mut builder = UnitBuilder::default();
|
||||||
|
let reaction_note = builder.new_noteref();
|
||||||
|
|
||||||
|
let single0 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac1 = builder.insert_reac_frag(reaction_note);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1]));
|
||||||
|
builder.aeq(1, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let single1 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single1));
|
||||||
|
builder.aeq(1, Expect::Reaction(vec![&reac1]));
|
||||||
|
builder.aeq(2, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac2 = builder.insert_reac_frag(reaction_note);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac2, &reac1]));
|
||||||
|
builder.aeq(1, Expect::Single(&single1));
|
||||||
|
builder.aeq(2, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let single2 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single2));
|
||||||
|
builder.aeq(1, Expect::Reaction(vec![&reac2, &reac1]));
|
||||||
|
builder.aeq(2, Expect::Single(&single1));
|
||||||
|
builder.aeq(3, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac3 = builder.insert_reac_frag(reaction_note);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1, &reac2, &reac3]));
|
||||||
|
builder.aeq(1, Expect::Single(&single2));
|
||||||
|
builder.aeq(2, Expect::Single(&single1));
|
||||||
|
builder.aeq(3, Expect::Single(&single0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reactions2() {
|
||||||
|
let mut builder = UnitBuilder::default();
|
||||||
|
let reaction_note1 = builder.new_noteref();
|
||||||
|
let reaction_note2 = builder.new_noteref();
|
||||||
|
|
||||||
|
let single0 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac1_1 = builder.insert_reac_frag(reaction_note1);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1_1]));
|
||||||
|
builder.aeq(1, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac2_1 = builder.insert_reac_frag(reaction_note2);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac2_1]));
|
||||||
|
builder.aeq(1, Expect::Reaction(vec![&reac1_1]));
|
||||||
|
builder.aeq(2, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let single1 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single1));
|
||||||
|
builder.aeq(1, Expect::Reaction(vec![&reac2_1]));
|
||||||
|
builder.aeq(2, Expect::Reaction(vec![&reac1_1]));
|
||||||
|
builder.aeq(3, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac1_2 = builder.insert_reac_frag(reaction_note1);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1_2, &reac1_1]));
|
||||||
|
builder.aeq(1, Expect::Single(&single1));
|
||||||
|
builder.aeq(2, Expect::Reaction(vec![&reac2_1]));
|
||||||
|
builder.aeq(3, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let single2 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single2));
|
||||||
|
builder.aeq(1, Expect::Reaction(vec![&reac1_2, &reac1_1]));
|
||||||
|
builder.aeq(2, Expect::Single(&single1));
|
||||||
|
builder.aeq(3, Expect::Reaction(vec![&reac2_1]));
|
||||||
|
builder.aeq(4, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac1_3 = builder.insert_reac_frag(reaction_note1);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1_2, &reac1_1, &reac1_3]));
|
||||||
|
builder.aeq(1, Expect::Single(&single2));
|
||||||
|
builder.aeq(2, Expect::Single(&single1));
|
||||||
|
builder.aeq(3, Expect::Reaction(vec![&reac2_1]));
|
||||||
|
builder.aeq(4, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac2_2 = builder.insert_reac_frag(reaction_note2);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac2_1, &reac2_2]));
|
||||||
|
builder.aeq(1, Expect::Reaction(vec![&reac1_2, &reac1_1, &reac1_3]));
|
||||||
|
builder.aeq(2, Expect::Single(&single2));
|
||||||
|
builder.aeq(3, Expect::Single(&single1));
|
||||||
|
builder.aeq(4, Expect::Single(&single0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reactions3() {
|
||||||
|
let mut builder = UnitBuilder::default();
|
||||||
|
let reaction_note1 = builder.new_noteref();
|
||||||
|
|
||||||
|
let single1 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let reac0 = builder.insert_reac_frag(reaction_note1);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac0]));
|
||||||
|
builder.aeq(1, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let (reac1, reac2) = builder.insert_reac_frag_pair(reaction_note1);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac0, &reac1, &reac2]));
|
||||||
|
builder.aeq(1, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let single2 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single2));
|
||||||
|
builder.aeq(1, Expect::Reaction(vec![&reac0, &reac1, &reac2]));
|
||||||
|
builder.aeq(2, Expect::Single(&single1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_repost() {
|
||||||
|
let mut builder = UnitBuilder::default();
|
||||||
|
let repost_note = builder.new_noteref();
|
||||||
|
|
||||||
|
let single1 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let repost1 = builder.insert_repost(repost_note);
|
||||||
|
builder.aeq(0, Expect::Repost(vec![&repost1]));
|
||||||
|
builder.aeq(1, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let single2 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single2));
|
||||||
|
builder.aeq(1, Expect::Repost(vec![&repost1]));
|
||||||
|
builder.aeq(2, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let reac1 = builder.insert_reac_frag(repost_note);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1]));
|
||||||
|
builder.aeq(1, Expect::Single(&single2));
|
||||||
|
builder.aeq(2, Expect::Repost(vec![&repost1]));
|
||||||
|
builder.aeq(3, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let repost2 = builder.insert_repost(repost_note);
|
||||||
|
builder.aeq(0, Expect::Repost(vec![&repost1, &repost2]));
|
||||||
|
builder.aeq(1, Expect::Reaction(vec![&reac1]));
|
||||||
|
builder.aeq(2, Expect::Single(&single2));
|
||||||
|
builder.aeq(3, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let reac2 = builder.insert_reac_frag(repost_note);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1, &reac2]));
|
||||||
|
builder.aeq(1, Expect::Repost(vec![&repost1, &repost2]));
|
||||||
|
builder.aeq(2, Expect::Single(&single2));
|
||||||
|
builder.aeq(3, Expect::Single(&single1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
use std::{
|
|
||||||
collections::{BTreeSet, HashSet},
|
|
||||||
hash::Hash,
|
|
||||||
};
|
|
||||||
|
|
||||||
use egui_nav::ReturnType;
|
use egui_nav::ReturnType;
|
||||||
use egui_virtual_list::VirtualList;
|
use egui_virtual_list::VirtualList;
|
||||||
use enostr::{NoteId, RelayPool};
|
use enostr::{NoteId, RelayPool};
|
||||||
@@ -13,13 +8,17 @@ use notedeck::{NoteCache, NoteRef, UnknownIds};
|
|||||||
use crate::{
|
use crate::{
|
||||||
actionbar::{process_thread_notes, NewThreadNotes},
|
actionbar::{process_thread_notes, NewThreadNotes},
|
||||||
multi_subscriber::ThreadSubs,
|
multi_subscriber::ThreadSubs,
|
||||||
timeline::MergeKind,
|
timeline::{
|
||||||
|
note_units::{NoteUnits, UnitKey},
|
||||||
|
unit::NoteUnit,
|
||||||
|
InsertionResponse,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::ThreadSelection;
|
use super::ThreadSelection;
|
||||||
|
|
||||||
pub struct ThreadNode {
|
pub struct ThreadNode {
|
||||||
pub replies: HybridSet<NoteRef>,
|
pub replies: SingleNoteUnits,
|
||||||
pub prev: ParentState,
|
pub prev: ParentState,
|
||||||
pub have_all_ancestors: bool,
|
pub have_all_ancestors: bool,
|
||||||
pub list: VirtualList,
|
pub list: VirtualList,
|
||||||
@@ -33,103 +32,10 @@ pub enum ParentState {
|
|||||||
Parent(NoteId),
|
Parent(NoteId),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Affords:
|
|
||||||
/// - O(1) contains
|
|
||||||
/// - O(log n) sorted insertion
|
|
||||||
pub struct HybridSet<T> {
|
|
||||||
reversed: bool,
|
|
||||||
lookup: HashSet<T>, // fast deduplication
|
|
||||||
ordered: BTreeSet<T>, // sorted iteration
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for HybridSet<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
reversed: Default::default(),
|
|
||||||
lookup: Default::default(),
|
|
||||||
ordered: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum InsertionResponse {
|
|
||||||
AlreadyExists,
|
|
||||||
Merged(MergeKind),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Copy + Ord + Eq + Hash> HybridSet<T> {
|
|
||||||
pub fn insert(&mut self, val: T) -> InsertionResponse {
|
|
||||||
if !self.lookup.insert(val) {
|
|
||||||
return InsertionResponse::AlreadyExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
let front_insertion = match self.ordered.iter().next() {
|
|
||||||
Some(first) => (val >= *first) == self.reversed,
|
|
||||||
None => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.ordered.insert(val); // O(log n)
|
|
||||||
|
|
||||||
InsertionResponse::Merged(if front_insertion {
|
|
||||||
MergeKind::FrontInsert
|
|
||||||
} else {
|
|
||||||
MergeKind::Spliced
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Eq + Hash> HybridSet<T> {
|
|
||||||
pub fn contains(&self, val: &T) -> bool {
|
|
||||||
self.lookup.contains(val) // O(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> HybridSet<T> {
|
|
||||||
pub fn iter(&self) -> HybridIter<'_, T> {
|
|
||||||
HybridIter {
|
|
||||||
inner: self.ordered.iter(),
|
|
||||||
reversed: self.reversed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(reversed: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
reversed,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> IntoIterator for &'a HybridSet<T> {
|
|
||||||
type Item = &'a T;
|
|
||||||
type IntoIter = HybridIter<'a, T>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HybridIter<'a, T> {
|
|
||||||
inner: std::collections::btree_set::Iter<'a, T>,
|
|
||||||
reversed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> Iterator for HybridIter<'a, T> {
|
|
||||||
type Item = &'a T;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if self.reversed {
|
|
||||||
self.inner.next_back()
|
|
||||||
} else {
|
|
||||||
self.inner.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ThreadNode {
|
impl ThreadNode {
|
||||||
pub fn new(parent: ParentState) -> Self {
|
pub fn new(parent: ParentState) -> Self {
|
||||||
Self {
|
Self {
|
||||||
replies: HybridSet::new(true),
|
replies: SingleNoteUnits::new(true),
|
||||||
prev: parent,
|
prev: parent,
|
||||||
have_all_ancestors: false,
|
have_all_ancestors: false,
|
||||||
list: VirtualList::new(),
|
list: VirtualList::new(),
|
||||||
@@ -487,3 +393,34 @@ impl NoteSeenFlags {
|
|||||||
self.flags.contains_key(¬e_id)
|
self.flags.contains_key(¬e_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SingleNoteUnits {
|
||||||
|
units: NoteUnits,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingleNoteUnits {
|
||||||
|
pub fn new(reversed: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
units: NoteUnits::new_with_cap(0, reversed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, note_ref: NoteRef) -> InsertionResponse {
|
||||||
|
self.units.merge_single_unit(note_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn values(&self) -> impl Iterator<Item = &NoteRef> {
|
||||||
|
self.units.values().filter_map(|entry| {
|
||||||
|
if let NoteUnit::Single(note_ref) = entry {
|
||||||
|
Some(note_ref)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_key(&self, k: &NoteKey) -> bool {
|
||||||
|
self.units.contains_key(&UnitKey::Single(*k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::{Ndb, Note, NoteKey, Transaction};
|
||||||
|
use notedeck::NoteRef;
|
||||||
|
use notedeck_ui::note::get_reposted_note;
|
||||||
|
|
||||||
|
use crate::timeline::{
|
||||||
|
note_units::{InsertManyResponse, NoteUnits},
|
||||||
|
unit::{
|
||||||
|
CompositeFragment, NoteUnit, NoteUnitFragment, Reaction, ReactionFragment, RepostFragment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct TimelineUnits {
|
||||||
|
pub units: NoteUnits,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimelineUnits {
|
||||||
|
pub fn with_capacity(cap: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
units: NoteUnits::new_with_cap(cap, false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_refs_single(refs: Vec<NoteRef>) -> Self {
|
||||||
|
let mut entries = TimelineUnits::default();
|
||||||
|
refs.into_iter().for_each(|r| entries.merge_single_note(r));
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.units.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.units.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns number of new entries merged
|
||||||
|
pub fn merge_new_notes<'a>(
|
||||||
|
&mut self,
|
||||||
|
payloads: Vec<&'a NotePayload>,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
) -> MergeResponse<'a> {
|
||||||
|
let mut unknown_pks = HashSet::with_capacity(payloads.len());
|
||||||
|
let new_fragments = payloads
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|p| to_fragment(p, ndb, txn))
|
||||||
|
.map(|f| {
|
||||||
|
if let Some(pk) = f.unknown_pk {
|
||||||
|
unknown_pks.insert(pk);
|
||||||
|
}
|
||||||
|
f.fragment
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let tl_response = if unknown_pks.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(UnknownPks { unknown_pks })
|
||||||
|
};
|
||||||
|
|
||||||
|
MergeResponse {
|
||||||
|
insertion_response: self.units.merge_fragments(new_fragments),
|
||||||
|
tl_response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest(&self) -> Option<&NoteRef> {
|
||||||
|
self.units.latest_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge_single_note(&mut self, note_ref: NoteRef) {
|
||||||
|
self.units.merge_single_unit(note_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used in the view
|
||||||
|
pub fn get(&self, index: usize) -> Option<&NoteUnit> {
|
||||||
|
self.units.kth(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MergeResponse<'a> {
|
||||||
|
pub insertion_response: InsertManyResponse,
|
||||||
|
pub tl_response: Option<UnknownPks<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UnknownPks<'a> {
|
||||||
|
pub(crate) unknown_pks: HashSet<&'a [u8; 32]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NoteUnitFragmentResponse<'a> {
|
||||||
|
pub fragment: NoteUnitFragment,
|
||||||
|
pub unknown_pk: Option<&'a [u8; 32]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NotePayload<'a> {
|
||||||
|
pub note: Note<'a>,
|
||||||
|
pub key: NoteKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NotePayload<'a> {
|
||||||
|
pub fn noteref(&self) -> NoteRef {
|
||||||
|
NoteRef {
|
||||||
|
key: self.key,
|
||||||
|
created_at: self.note.created_at(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_fragment<'a>(
|
||||||
|
payload: &'a NotePayload,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
) -> Option<NoteUnitFragmentResponse<'a>> {
|
||||||
|
match payload.note.kind() {
|
||||||
|
1 => Some(NoteUnitFragmentResponse {
|
||||||
|
fragment: NoteUnitFragment::Single(NoteRef {
|
||||||
|
key: payload.key,
|
||||||
|
created_at: payload.note.created_at(),
|
||||||
|
}),
|
||||||
|
unknown_pk: None,
|
||||||
|
}),
|
||||||
|
7 => to_reaction(payload, ndb, txn).map(|r| NoteUnitFragmentResponse {
|
||||||
|
fragment: NoteUnitFragment::Composite(CompositeFragment::Reaction(r.fragment)),
|
||||||
|
unknown_pk: Some(r.pk),
|
||||||
|
}),
|
||||||
|
6 => to_repost(payload, ndb, txn).map(RepostResponse::into),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_reaction<'a>(
|
||||||
|
payload: &'a NotePayload,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
) -> Option<ReactionResponse<'a>> {
|
||||||
|
let reaction = payload.note.content();
|
||||||
|
|
||||||
|
let mut note_reacted_to = None;
|
||||||
|
|
||||||
|
for tag in payload.note.tags() {
|
||||||
|
if tag.count() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some("e") = tag.get_str(0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(react_to_id) = tag.get_id(1) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
note_reacted_to = Some(react_to_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let reacted_to_noteid = note_reacted_to?;
|
||||||
|
|
||||||
|
let reaction_note_ref = payload.noteref();
|
||||||
|
|
||||||
|
let reacted_to_note = ndb.get_note_by_id(txn, reacted_to_noteid).ok()?;
|
||||||
|
|
||||||
|
let noteref_reacted_to = NoteRef {
|
||||||
|
key: reacted_to_note.key()?,
|
||||||
|
created_at: reacted_to_note.created_at(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ReactionResponse {
|
||||||
|
fragment: ReactionFragment {
|
||||||
|
noteref_reacted_to,
|
||||||
|
reaction_note_ref,
|
||||||
|
reaction: Reaction {
|
||||||
|
reaction: reaction.to_string(),
|
||||||
|
sender: Pubkey::new(*payload.note.pubkey()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pk: payload.note.pubkey(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReactionResponse<'a> {
|
||||||
|
fragment: ReactionFragment,
|
||||||
|
pk: &'a [u8; 32], // reaction sender
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RepostResponse<'a> {
|
||||||
|
fragment: RepostFragment,
|
||||||
|
reposter_pk: &'a [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<RepostResponse<'a>> for NoteUnitFragmentResponse<'a> {
|
||||||
|
fn from(value: RepostResponse<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
fragment: NoteUnitFragment::Composite(CompositeFragment::Repost(value.fragment)),
|
||||||
|
unknown_pk: Some(value.reposter_pk),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_repost<'a>(
|
||||||
|
payload: &'a NotePayload,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
) -> Option<RepostResponse<'a>> {
|
||||||
|
let reposted_note = match get_reposted_note(ndb, txn, &payload.note) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => {
|
||||||
|
tracing::debug!(
|
||||||
|
"Could not get reposted note for note id {}",
|
||||||
|
enostr::NoteId::new(*payload.note.id()).hex()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let reposted_key = match reposted_note.key() {
|
||||||
|
Some(r) => r,
|
||||||
|
None => {
|
||||||
|
tracing::error!(
|
||||||
|
"Could not get key of reposted note {}",
|
||||||
|
enostr::NoteId::new(*reposted_note.id()).hex()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(RepostResponse {
|
||||||
|
fragment: RepostFragment {
|
||||||
|
reposted_noteref: NoteRef {
|
||||||
|
key: reposted_key,
|
||||||
|
created_at: reposted_note.created_at(),
|
||||||
|
},
|
||||||
|
repost_noteref: payload.noteref(),
|
||||||
|
reposter: Pubkey::new(*payload.note.pubkey()),
|
||||||
|
},
|
||||||
|
reposter_pk: payload.note.pubkey(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use notedeck::NoteRef;
|
||||||
|
|
||||||
|
use crate::timeline::note_units::{CompositeKey, CompositeType, UnitKey};
|
||||||
|
|
||||||
|
/// A `NoteUnit` represents a cohesive piece of data derived from notes
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum NoteUnit {
|
||||||
|
Single(NoteRef), // A single note
|
||||||
|
Composite(CompositeUnit),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteUnit {
|
||||||
|
pub fn key(&self) -> UnitKey {
|
||||||
|
match self {
|
||||||
|
NoteUnit::Single(note_ref) => UnitKey::Single(note_ref.key),
|
||||||
|
NoteUnit::Composite(clustered_entry) => UnitKey::Composite(clustered_entry.key()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_underlying_noteref(&self) -> &NoteRef {
|
||||||
|
match self {
|
||||||
|
NoteUnit::Single(note_ref) => note_ref,
|
||||||
|
NoteUnit::Composite(clustered) => match clustered {
|
||||||
|
CompositeUnit::Reaction(reaction_entry) => &reaction_entry.note_reacted_to,
|
||||||
|
CompositeUnit::Repost(repost_unit) => &repost_unit.note_reposted,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||||
|
match self {
|
||||||
|
NoteUnit::Single(note_ref) => note_ref,
|
||||||
|
NoteUnit::Composite(composite_unit) => composite_unit.get_latest_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for NoteUnit {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.get_latest_ref().cmp(other.get_latest_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for NoteUnit {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.get_latest_ref() == other.get_latest_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for NoteUnit {}
|
||||||
|
|
||||||
|
impl PartialOrd for NoteUnit {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combines potentially many notes into one cohesive piece of data
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CompositeUnit {
|
||||||
|
Reaction(ReactionUnit),
|
||||||
|
Repost(RepostUnit),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositeUnit {
|
||||||
|
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||||
|
match self {
|
||||||
|
CompositeUnit::Reaction(reaction_unit) => reaction_unit.get_latest_ref(),
|
||||||
|
CompositeUnit::Repost(repost_unit) => repost_unit.get_latest_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for CompositeUnit {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Reaction(l0), Self::Reaction(r0)) => l0 == r0,
|
||||||
|
(Self::Repost(l0), Self::Repost(r0)) => l0 == r0,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositeUnit {
|
||||||
|
pub fn key(&self) -> CompositeKey {
|
||||||
|
match self {
|
||||||
|
CompositeUnit::Reaction(reaction_entry) => CompositeKey {
|
||||||
|
key: reaction_entry.note_reacted_to.key,
|
||||||
|
composite_type: CompositeType::Reaction,
|
||||||
|
},
|
||||||
|
CompositeUnit::Repost(repost_unit) => CompositeKey {
|
||||||
|
key: repost_unit.note_reposted.key,
|
||||||
|
composite_type: CompositeType::Repost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CompositeFragment> for CompositeUnit {
|
||||||
|
fn from(value: CompositeFragment) -> Self {
|
||||||
|
match value {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => {
|
||||||
|
CompositeUnit::Reaction(reaction_fragment.into())
|
||||||
|
}
|
||||||
|
CompositeFragment::Repost(repost_fragment) => {
|
||||||
|
CompositeUnit::Repost(repost_fragment.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents all the reactions to a specific note `ReactionUnit::note_reacted_to`
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct ReactionUnit {
|
||||||
|
pub note_reacted_to: NoteRef, // NOTE: this should not be modified after it's created
|
||||||
|
pub reactions: BTreeMap<NoteRef, Reaction>,
|
||||||
|
pub senders: HashSet<Pubkey>, // useful for making sure the same user can't add more than one reaction to a note
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReactionUnit {
|
||||||
|
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||||
|
self.reactions
|
||||||
|
.first_key_value()
|
||||||
|
.map(|(r, _)| r)
|
||||||
|
.unwrap_or(&self.note_reacted_to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReactionFragment> for ReactionUnit {
|
||||||
|
fn from(frag: ReactionFragment) -> Self {
|
||||||
|
let mut senders = HashSet::new();
|
||||||
|
senders.insert(frag.reaction.sender);
|
||||||
|
|
||||||
|
let mut reactions = BTreeMap::new();
|
||||||
|
reactions.insert(frag.reaction_note_ref, frag.reaction);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
note_reacted_to: frag.noteref_reacted_to,
|
||||||
|
reactions,
|
||||||
|
senders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct RepostUnit {
|
||||||
|
pub note_reposted: NoteRef,
|
||||||
|
pub reposts: BTreeMap<NoteRef, Pubkey>, // repost note to sender
|
||||||
|
pub senders: HashSet<Pubkey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepostUnit {
|
||||||
|
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||||
|
self.reposts
|
||||||
|
.first_key_value()
|
||||||
|
.map(|(r, _)| r)
|
||||||
|
.unwrap_or(&self.note_reposted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RepostFragment> for RepostUnit {
|
||||||
|
fn from(value: RepostFragment) -> Self {
|
||||||
|
let mut reposts = BTreeMap::new();
|
||||||
|
reposts.insert(value.repost_noteref, value.reposter);
|
||||||
|
|
||||||
|
let mut senders = HashSet::new();
|
||||||
|
senders.insert(value.reposter);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
note_reposted: value.reposted_noteref,
|
||||||
|
reposts,
|
||||||
|
senders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum NoteUnitFragment {
|
||||||
|
Single(NoteRef),
|
||||||
|
Composite(CompositeFragment),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CompositeFragment {
|
||||||
|
Reaction(ReactionFragment),
|
||||||
|
Repost(RepostFragment),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositeFragment {
|
||||||
|
pub fn fold_into(self, unit: &mut CompositeUnit) {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => {
|
||||||
|
let CompositeUnit::Reaction(reaction_unit) = unit else {
|
||||||
|
tracing::error!("Attempting to fold a reaction fragment into a unit which isn't ReactionUnit. Doing nothing, this should never occur");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
reaction_fragment.fold_into(reaction_unit);
|
||||||
|
}
|
||||||
|
CompositeFragment::Repost(repost_fragment) => {
|
||||||
|
let CompositeUnit::Repost(repost_unit) = unit else {
|
||||||
|
tracing::error!("Attempting to fold a repost fragment into a unit which isn't RepostUnit. Doing nothing, this should never occur");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
repost_fragment.fold_into(repost_unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> CompositeKey {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(reaction) => CompositeKey {
|
||||||
|
key: reaction.noteref_reacted_to.key,
|
||||||
|
composite_type: CompositeType::Reaction,
|
||||||
|
},
|
||||||
|
CompositeFragment::Repost(repost) => CompositeKey {
|
||||||
|
key: repost.reposted_noteref.key,
|
||||||
|
composite_type: CompositeType::Repost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_underlying_noteref(&self) -> &NoteRef {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => &reaction_fragment.noteref_reacted_to,
|
||||||
|
CompositeFragment::Repost(repost_fragment) => &repost_fragment.reposted_noteref,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => &reaction_fragment.reaction_note_ref,
|
||||||
|
CompositeFragment::Repost(repost_fragment) => &repost_fragment.repost_noteref,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_type(&self) -> CompositeType {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(_) => CompositeType::Reaction,
|
||||||
|
CompositeFragment::Repost(_) => CompositeType::Repost,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A singluar reaction to a note
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ReactionFragment {
|
||||||
|
pub noteref_reacted_to: NoteRef,
|
||||||
|
pub reaction_note_ref: NoteRef,
|
||||||
|
pub reaction: Reaction,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReactionFragment {
|
||||||
|
/// Add all the contents of Self into `CompositeUnit`
|
||||||
|
pub fn fold_into(self, unit: &mut ReactionUnit) {
|
||||||
|
if self.noteref_reacted_to != unit.note_reacted_to {
|
||||||
|
tracing::error!("Attempting to fold a reaction fragment into a ReactionUnit which as a different note reacted to: {:?} != {:?}. This should never occur", self.noteref_reacted_to, unit.note_reacted_to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if unit.senders.contains(&self.reaction.sender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unit.senders.insert(self.reaction.sender);
|
||||||
|
unit.reactions.insert(self.reaction_note_ref, self.reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct Reaction {
|
||||||
|
pub reaction: String, // can't use char because some emojis are 'grapheme clusters'
|
||||||
|
pub sender: Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a singular repost
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RepostFragment {
|
||||||
|
pub reposted_noteref: NoteRef,
|
||||||
|
pub repost_noteref: NoteRef,
|
||||||
|
pub reposter: Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepostFragment {
|
||||||
|
pub fn fold_into(self, unit: &mut RepostUnit) {
|
||||||
|
if self.reposted_noteref != unit.note_reposted {
|
||||||
|
tracing::error!("Attempting to fold a repost fragment into a RepostUnit which has a different note reposted: {:?} != {:?}. This should never occur", self.reposted_noteref, unit.note_reposted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if unit.senders.contains(&self.reposter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unit.senders.insert(self.reposter);
|
||||||
|
unit.reposts.insert(self.repost_noteref, self.reposter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use crate::{
|
|||||||
Damus, Route,
|
Damus, Route,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO(kernelkind): should account for mutes
|
||||||
pub fn unseen_notification(
|
pub fn unseen_notification(
|
||||||
columns: &mut Damus,
|
columns: &mut Damus,
|
||||||
ndb: &nostrdb::Ndb,
|
ndb: &nostrdb::Ndb,
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ fn login_textedit<'a>(
|
|||||||
text_edit
|
text_edit
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eye_button(ui: &mut egui::Ui, is_visible: bool) -> egui::Response {
|
pub fn eye_button(ui: &mut egui::Ui, is_visible: bool) -> egui::Response {
|
||||||
let is_dark_mode = ui.visuals().dark_mode;
|
let is_dark_mode = ui.visuals().dark_mode;
|
||||||
let icon = if is_visible && is_dark_mode {
|
let icon = if is_visible && is_dark_mode {
|
||||||
app_images::eye_dark_image()
|
app_images::eye_dark_image()
|
||||||
|
|||||||
@@ -709,6 +709,7 @@ pub fn render_add_column_routes(
|
|||||||
ctx.note_cache,
|
ctx.note_cache,
|
||||||
app.options.contains(AppOptions::SinceOptimize),
|
app.options.contains(AppOptions::SinceOptimize),
|
||||||
ctx.accounts,
|
ctx.accounts,
|
||||||
|
ctx.unknown_ids,
|
||||||
);
|
);
|
||||||
|
|
||||||
app.columns_mut(ctx.i18n, ctx.accounts)
|
app.columns_mut(ctx.i18n, ctx.accounts)
|
||||||
@@ -749,6 +750,7 @@ pub fn render_add_column_routes(
|
|||||||
ctx.note_cache,
|
ctx.note_cache,
|
||||||
app.options.contains(AppOptions::SinceOptimize),
|
app.options.contains(AppOptions::SinceOptimize),
|
||||||
ctx.accounts,
|
ctx.accounts,
|
||||||
|
ctx.unknown_ids,
|
||||||
);
|
);
|
||||||
|
|
||||||
app.columns_mut(ctx.i18n, ctx.accounts)
|
app.columns_mut(ctx.i18n, ctx.accounts)
|
||||||
|
|||||||
@@ -292,10 +292,6 @@ fn show_amount(
|
|||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// let user_changed = cur_input != Some(user_input.clone());
|
|
||||||
ui.memory_mut(|m| m.request_focus(user_input_id));
|
|
||||||
// ui.data_mut(|d| d.insert_temp(id, user_input));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SELECTION_BUTTONS: [ZapSelectionButton; 8] = [
|
const SELECTION_BUTTONS: [ZapSelectionButton; 8] = [
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
use crate::draft::{Draft, Drafts, MentionHint};
|
use crate::draft::{Draft, Drafts, MentionHint};
|
||||||
#[cfg(not(target_os = "android"))]
|
use crate::media_upload::nostrbuild_nip96_upload;
|
||||||
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
|
||||||
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
||||||
use crate::ui::mentions_picker::MentionPickerView;
|
use crate::ui::mentions_picker::MentionPickerView;
|
||||||
use crate::ui::{self, Preview, PreviewConfig};
|
use crate::ui::{self, Preview, PreviewConfig};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
|
|
||||||
use egui::{
|
use egui::{
|
||||||
text::{CCursorRange, LayoutJob},
|
text::{CCursorRange, LayoutJob},
|
||||||
text_edit::TextEditOutput,
|
text_edit::TextEditOutput,
|
||||||
@@ -16,19 +14,22 @@ use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
|||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::media::gif::ensure_latest_texture;
|
use notedeck::media::gif::ensure_latest_texture;
|
||||||
use notedeck::media::AnimationMode;
|
use notedeck::media::AnimationMode;
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
use notedeck::platform::android::try_open_file_picker;
|
||||||
|
use notedeck::platform::get_next_selected_file;
|
||||||
use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
|
use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
|
||||||
|
use notedeck::{
|
||||||
|
name::get_display_name, supported_mime_hosted_at_url, tr, Localization, NoteAction, NoteContext,
|
||||||
|
};
|
||||||
use notedeck_ui::{
|
use notedeck_ui::{
|
||||||
app_images,
|
app_images,
|
||||||
context_menu::{input_context, PasteBehavior},
|
context_menu::{input_context, PasteBehavior},
|
||||||
note::render_note_preview,
|
note::render_note_preview,
|
||||||
NoteOptions, ProfilePic,
|
NoteOptions, ProfilePic,
|
||||||
};
|
};
|
||||||
|
|
||||||
use notedeck::{
|
|
||||||
name::get_display_name, supported_mime_hosted_at_url, tr, Localization, NoteAction, NoteContext,
|
|
||||||
};
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
use {notedeck::platform::file::emit_selected_file, notedeck::platform::file::SelectedMedia};
|
||||||
|
|
||||||
pub struct PostView<'a, 'd> {
|
pub struct PostView<'a, 'd> {
|
||||||
note_context: &'a mut NoteContext<'d>,
|
note_context: &'a mut NoteContext<'d>,
|
||||||
@@ -348,6 +349,22 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui_no_scroll(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
|
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) => {
|
||||||
|
let promise = nostrbuild_nip96_upload(
|
||||||
|
self.poster.secret_key.secret_bytes(),
|
||||||
|
selected_media,
|
||||||
|
);
|
||||||
|
self.draft.uploading_media.push(promise);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{e}");
|
||||||
|
self.draft.upload_errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -521,21 +538,13 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
{
|
{
|
||||||
if let Some(files) = rfd::FileDialog::new().pick_files() {
|
if let Some(files) = rfd::FileDialog::new().pick_files() {
|
||||||
for file in files {
|
for file in files {
|
||||||
match MediaPath::new(file) {
|
emit_selected_file(SelectedMedia::from_path(file));
|
||||||
Ok(media_path) => {
|
|
||||||
let promise = nostrbuild_nip96_upload(
|
|
||||||
self.poster.secret_key.secret_bytes(),
|
|
||||||
media_path,
|
|
||||||
);
|
|
||||||
self.draft.uploading_media.push(promise);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("{e}");
|
|
||||||
self.draft.upload_errors.push(e.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
try_open_file_picker();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ use std::mem;
|
|||||||
|
|
||||||
use egui::{Layout, ScrollArea};
|
use egui::{Layout, ScrollArea};
|
||||||
use nostrdb::Ndb;
|
use nostrdb::Ndb;
|
||||||
use notedeck::{Images, JobPool, JobsCache, Localization};
|
use notedeck::{tr, Images, JobPool, JobsCache, Localization};
|
||||||
use notedeck_ui::{
|
use notedeck_ui::{
|
||||||
colors,
|
colors,
|
||||||
nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetFlags, Nip51SetWidgetResponse},
|
nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetAction, Nip51SetWidgetFlags},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{onboarding::Onboarding, ui::widgets::styled_button};
|
use crate::{onboarding::Onboarding, ui::widgets::styled_button};
|
||||||
|
|
||||||
/// Display Follow Packs for the user to choose from authors trusted by the Damus team
|
/// Display Follow Packs for the user to choose from authors trusted by the Damus team
|
||||||
pub struct FollowPackOnboardingView<'a> {
|
pub struct FollowPackOnboardingView<'a> {
|
||||||
onboarding: &'a Onboarding,
|
onboarding: &'a mut Onboarding,
|
||||||
ui_state: &'a mut Nip51SetUiCache,
|
ui_state: &'a mut Nip51SetUiCache,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
images: &'a mut Images,
|
images: &'a mut Images,
|
||||||
@@ -33,7 +33,7 @@ pub enum FollowPacksResponse {
|
|||||||
|
|
||||||
impl<'a> FollowPackOnboardingView<'a> {
|
impl<'a> FollowPackOnboardingView<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
onboarding: &'a Onboarding,
|
onboarding: &'a mut Onboarding,
|
||||||
ui_state: &'a mut Nip51SetUiCache,
|
ui_state: &'a mut Nip51SetUiCache,
|
||||||
ndb: &'a Ndb,
|
ndb: &'a Ndb,
|
||||||
images: &'a mut Images,
|
images: &'a mut Images,
|
||||||
@@ -71,7 +71,11 @@ impl<'a> FollowPackOnboardingView<'a> {
|
|||||||
.max_height(max_height)
|
.max_height(max_height)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
egui::Frame::new().inner_margin(8.0).show(ui, |ui| {
|
egui::Frame::new().inner_margin(8.0).show(ui, |ui| {
|
||||||
if let Some(resp) = Nip51SetWidget::new(
|
self.onboarding.list.borrow_mut().ui_custom_layout(
|
||||||
|
ui,
|
||||||
|
follow_pack_state.len(),
|
||||||
|
|ui, index| {
|
||||||
|
let resp = Nip51SetWidget::new(
|
||||||
follow_pack_state,
|
follow_pack_state,
|
||||||
self.ui_state,
|
self.ui_state,
|
||||||
self.ndb,
|
self.ndb,
|
||||||
@@ -81,20 +85,29 @@ impl<'a> FollowPackOnboardingView<'a> {
|
|||||||
self.jobs,
|
self.jobs,
|
||||||
)
|
)
|
||||||
.with_flags(Nip51SetWidgetFlags::TRUST_IMAGES)
|
.with_flags(Nip51SetWidgetFlags::TRUST_IMAGES)
|
||||||
.ui(ui)
|
.render_at_index(ui, index);
|
||||||
{
|
|
||||||
match resp {
|
if let Some(cur_action) = resp.action {
|
||||||
Nip51SetWidgetResponse::ViewProfile(pubkey) => {
|
match cur_action {
|
||||||
|
Nip51SetWidgetAction::ViewProfile(pubkey) => {
|
||||||
action = Some(OnboardingResponse::ViewProfile(pubkey));
|
action = Some(OnboardingResponse::ViewProfile(pubkey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.rendered {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
|
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
if ui.add(styled_button("Done", colors::PINK)).clicked() {
|
if ui.add(styled_button(tr!(self.loc, "Done", "Button to indicate that the user is done going through the onboarding process.").as_str(), colors::PINK)).clicked() {
|
||||||
action = Some(OnboardingResponse::FollowPacks(
|
action = Some(OnboardingResponse::FollowPacks(
|
||||||
FollowPacksResponse::UserSelectedPacks(mem::take(self.ui_state)),
|
FollowPacksResponse::UserSelectedPacks(mem::take(self.ui_state)),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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,13 +70,11 @@ 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
|
||||||
if let Some(offset) = ui.data(|i| i.get_temp::<f32>(offset_id)) {
|
.get_mut(&TimelineKind::Profile(*self.pubkey))?;
|
||||||
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = scroll_area.show(ui, |ui| {
|
let output = scroll_area.show(ui, |ui| {
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
@@ -82,25 +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 profile_timeline = self
|
|
||||||
.timeline_cache
|
|
||||||
.notes(
|
|
||||||
self.note_context.ndb,
|
|
||||||
self.note_context.note_cache,
|
|
||||||
&txn,
|
|
||||||
&TimelineKind::Profile(*self.pubkey),
|
|
||||||
)
|
|
||||||
.get_ptr();
|
|
||||||
|
|
||||||
profile_timeline.selected_view = tabs_ui(
|
let tabs_resp = 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
|
||||||
@@ -116,7 +113,6 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
|
|
||||||
if let Some(note_action) = TimelineTabView::new(
|
if let Some(note_action) = TimelineTabView::new(
|
||||||
profile_timeline.current_view(),
|
profile_timeline.current_view(),
|
||||||
reversed,
|
|
||||||
self.note_options,
|
self.note_options,
|
||||||
&txn,
|
&txn,
|
||||||
self.note_context,
|
self.note_context,
|
||||||
@@ -127,19 +123,25 @@ 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,
|
||||||
|
note_context: &mut NoteContext,
|
||||||
profile: Option<&ProfileRecord<'_>>,
|
profile: Option<&ProfileRecord<'_>>,
|
||||||
) -> Option<ProfileViewAction> {
|
) -> Option<ProfileViewAction> {
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
banner(
|
banner(
|
||||||
@@ -161,13 +163,16 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
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.add(copy_key_widget(&pfp_rect)).clicked() {
|
if ui
|
||||||
let to_copy = if let Some(bech) = self.pubkey.npub() {
|
.add(copy_key_widget(&pfp_rect, note_context.i18n))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
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");
|
||||||
@@ -179,12 +184,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
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()))
|
||||||
@@ -192,10 +197,7 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
|
|
||||||
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))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(ProfileViewAction::EditProfile);
|
action = Some(ProfileViewAction::EditProfile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +264,6 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
action
|
action
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProfileType {
|
enum ProfileType {
|
||||||
@@ -300,7 +301,10 @@ fn handle_lud16(ui: &mut egui::Ui, lud16: &str) {
|
|||||||
.on_hover_text(lud16);
|
.on_hover_text(lud16);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ {
|
fn copy_key_widget<'a>(
|
||||||
|
pfp_rect: &'a egui::Rect,
|
||||||
|
i18n: &'a mut Localization,
|
||||||
|
) -> impl egui::Widget + 'a {
|
||||||
|ui: &mut egui::Ui| -> egui::Response {
|
|ui: &mut egui::Ui| -> egui::Response {
|
||||||
let painter = ui.painter();
|
let painter = ui.painter();
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
@@ -314,7 +318,11 @@ fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ {
|
|||||||
ui.id().with("custom_painter"),
|
ui.id().with("custom_painter"),
|
||||||
Sense::click(),
|
Sense::click(),
|
||||||
)
|
)
|
||||||
.on_hover_text("Copy npub to clipboard");
|
.on_hover_text(tr!(
|
||||||
|
i18n,
|
||||||
|
"Copy npub to clipboard",
|
||||||
|
"Tooltip text for copying npub to clipboard"
|
||||||
|
));
|
||||||
|
|
||||||
let copy_key_rounding = CornerRadius::same(100);
|
let copy_key_rounding = CornerRadius::same(100);
|
||||||
let fill_color = if resp.hovered() {
|
let fill_color = if resp.hovered() {
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use egui::{vec2, Align, Color32, CornerRadius, RichText, Stroke, TextEdit};
|
|||||||
use enostr::{NoteId, Pubkey};
|
use enostr::{NoteId, Pubkey};
|
||||||
use state::TypingType;
|
use state::TypingType;
|
||||||
|
|
||||||
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
|
use crate::{
|
||||||
|
timeline::{TimelineTab, TimelineUnits},
|
||||||
|
ui::timeline::TimelineTabView,
|
||||||
|
};
|
||||||
use egui_winit::clipboard::Clipboard;
|
use egui_winit::clipboard::Clipboard;
|
||||||
use nostrdb::{Filter, Ndb, Transaction};
|
use nostrdb::{Filter, Ndb, Transaction};
|
||||||
use notedeck::{tr, tr_plural, JobsCache, Localization, NoteAction, NoteContext, NoteRef};
|
use notedeck::{tr, tr_plural, JobsCache, Localization, NoteAction, NoteContext, NoteRef};
|
||||||
@@ -125,7 +128,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
|||||||
"Got {count} result for '{query}'", // one
|
"Got {count} result for '{query}'", // one
|
||||||
"Got {count} results for '{query}'", // other
|
"Got {count} results for '{query}'", // other
|
||||||
"Search results count", // comment
|
"Search results count", // comment
|
||||||
self.query.notes.notes.len(), // count
|
self.query.notes.units.len(), // count
|
||||||
query = &self.query.string
|
query = &self.query.string
|
||||||
));
|
));
|
||||||
note_action = self.show_search_results(ui);
|
note_action = self.show_search_results(ui);
|
||||||
@@ -153,10 +156,8 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
|||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.id_salt(SearchView::scroll_id())
|
.id_salt(SearchView::scroll_id())
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let reversed = false;
|
|
||||||
TimelineTabView::new(
|
TimelineTabView::new(
|
||||||
&self.query.notes,
|
&self.query.notes,
|
||||||
reversed,
|
|
||||||
self.note_options,
|
self.note_options,
|
||||||
self.txn,
|
self.txn,
|
||||||
self.note_context,
|
self.note_context,
|
||||||
@@ -190,7 +191,7 @@ fn execute_search(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
tab.notes = note_refs;
|
tab.units = TimelineUnits::from_refs_single(note_refs);
|
||||||
tab.list.borrow_mut().reset();
|
tab.list.borrow_mut().reset();
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use egui::{
|
use egui::{
|
||||||
vec2, Button, Color32, ComboBox, FontId, Frame, Margin, RichText, ScrollArea, ThemePreference,
|
vec2, Button, Color32, ComboBox, CornerRadius, FontId, Frame, Layout, Margin, RichText,
|
||||||
|
ScrollArea, TextEdit, ThemePreference,
|
||||||
};
|
};
|
||||||
|
use egui_extras::{Size, StripBuilder};
|
||||||
use enostr::NoteId;
|
use enostr::NoteId;
|
||||||
use nostrdb::Transaction;
|
use nostrdb::Transaction;
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
@@ -9,9 +11,12 @@ use notedeck::{
|
|||||||
Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings,
|
Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings,
|
||||||
SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE,
|
SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE,
|
||||||
};
|
};
|
||||||
use notedeck_ui::{NoteOptions, NoteView};
|
use notedeck_ui::{
|
||||||
|
app_images::{copy_to_clipboard_dark_image, copy_to_clipboard_image},
|
||||||
|
AnimationHelper, NoteOptions, NoteView,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{nav::RouterAction, Damus, Route};
|
use crate::{nav::RouterAction, ui::account_login_view::eye_button, Damus, Route};
|
||||||
|
|
||||||
const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw";
|
const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw";
|
||||||
|
|
||||||
@@ -470,6 +475,149 @@ impl<'a> SettingsView<'a> {
|
|||||||
action
|
action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keys_section(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let title = tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
|
"Keys",
|
||||||
|
"label for keys setting section"
|
||||||
|
);
|
||||||
|
|
||||||
|
settings_group(ui, title, |ui| {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.label(
|
||||||
|
richtext_small(tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
|
"PUBLIC ACCOUNT ID",
|
||||||
|
"label describing public key"
|
||||||
|
))
|
||||||
|
.color(ui.visuals().gray_out(ui.visuals().text_color())),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let copy_img = if ui.visuals().dark_mode {
|
||||||
|
copy_to_clipboard_image()
|
||||||
|
} else {
|
||||||
|
copy_to_clipboard_dark_image()
|
||||||
|
};
|
||||||
|
let copy_max_size = vec2(16.0, 16.0);
|
||||||
|
|
||||||
|
if let Some(npub) = self.note_context.accounts.selected_account_pubkey().npub() {
|
||||||
|
item_frame(ui).show(ui, |ui| {
|
||||||
|
StripBuilder::new(ui)
|
||||||
|
.size(Size::exact(24.0))
|
||||||
|
.cell_layout(Layout::left_to_right(egui::Align::Center))
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder
|
||||||
|
.size(Size::remainder())
|
||||||
|
.size(Size::exact(16.0))
|
||||||
|
.cell_layout(Layout::left_to_right(egui::Align::Center))
|
||||||
|
.horizontal(|mut strip| {
|
||||||
|
strip.cell(|ui| {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.label(richtext_small(&npub));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
strip.cell(|ui| {
|
||||||
|
let helper = AnimationHelper::new(
|
||||||
|
ui,
|
||||||
|
"copy-to-clipboard-npub",
|
||||||
|
copy_max_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
copy_img.paint_at(ui, helper.scaled_rect());
|
||||||
|
|
||||||
|
if helper.take_animation_response().clicked() {
|
||||||
|
ui.ctx().copy_text(npub);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(filled) = self.note_context.accounts.selected_filled() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(mut nsec) = bech32::encode::<bech32::Bech32>(
|
||||||
|
bech32::Hrp::parse_unchecked("nsec"),
|
||||||
|
&filled.secret_key.secret_bytes(),
|
||||||
|
)
|
||||||
|
.ok() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.label(
|
||||||
|
richtext_small(tr!(
|
||||||
|
self.note_context.i18n,
|
||||||
|
"SECRET ACCOUNT LOGIN KEY",
|
||||||
|
"label describing secret key"
|
||||||
|
))
|
||||||
|
.color(ui.visuals().gray_out(ui.visuals().text_color())),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_password_id = ui.id().with("is-password");
|
||||||
|
let is_password = ui
|
||||||
|
.ctx()
|
||||||
|
.data_mut(|d| d.get_temp(is_password_id))
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
item_frame(ui).show(ui, |ui| {
|
||||||
|
StripBuilder::new(ui)
|
||||||
|
.size(Size::exact(24.0))
|
||||||
|
.cell_layout(Layout::left_to_right(egui::Align::Center))
|
||||||
|
.vertical(|mut strip| {
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder
|
||||||
|
.size(Size::remainder())
|
||||||
|
.size(Size::exact(48.0))
|
||||||
|
.cell_layout(Layout::left_to_right(egui::Align::Center))
|
||||||
|
.horizontal(|mut strip| {
|
||||||
|
strip.cell(|ui| {
|
||||||
|
if is_password {
|
||||||
|
ui.add(
|
||||||
|
TextEdit::singleline(&mut nsec)
|
||||||
|
.password(is_password)
|
||||||
|
.interactive(false)
|
||||||
|
.frame(false),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.label(richtext_small(&nsec));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
strip.cell(|ui| {
|
||||||
|
let helper = AnimationHelper::new(
|
||||||
|
ui,
|
||||||
|
"copy-to-clipboard-nsec",
|
||||||
|
copy_max_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
copy_img.paint_at(ui, helper.scaled_rect());
|
||||||
|
|
||||||
|
if helper.take_animation_response().clicked() {
|
||||||
|
ui.ctx().copy_text(nsec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if eye_button(ui, is_password).clicked() {
|
||||||
|
ui.ctx().data_mut(|d| {
|
||||||
|
d.insert_temp(is_password_id, !is_password)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
|
|
||||||
@@ -509,6 +657,10 @@ impl<'a> SettingsView<'a> {
|
|||||||
|
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
|
|
||||||
|
self.keys_section(ui);
|
||||||
|
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
if let Some(new_action) = self.other_options_section(ui) {
|
if let Some(new_action) = self.other_options_section(ui) {
|
||||||
action = Some(new_action);
|
action = Some(new_action);
|
||||||
}
|
}
|
||||||
@@ -542,3 +694,10 @@ pub fn format_size(size_bytes: u64) -> String {
|
|||||||
format!("{:.2} GB", size / GB)
|
format!("{:.2} GB", size / GB)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn item_frame(ui: &egui::Ui) -> egui::Frame {
|
||||||
|
Frame::new()
|
||||||
|
.inner_margin(Margin::same(8))
|
||||||
|
.corner_radius(CornerRadius::same(8))
|
||||||
|
.fill(ui.visuals().panel_fill)
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ impl<'a> DesktopSidePanel<'a> {
|
|||||||
.color(ui.visuals().noninteractive().fg_stroke.color),
|
.color(ui.visuals().noninteractive().fg_stroke.color),
|
||||||
));
|
));
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
let add_deck_resp = ui.add(add_deck_button());
|
let add_deck_resp = ui.add(add_deck_button(self.i18n));
|
||||||
|
|
||||||
let decks_inner = ScrollArea::vertical()
|
let decks_inner = ScrollArea::vertical()
|
||||||
.max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0)))
|
.max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0)))
|
||||||
@@ -383,7 +383,7 @@ pub fn search_button() -> impl Widget {
|
|||||||
|
|
||||||
// TODO: convert to responsive button when expanded side panel impl is finished
|
// TODO: convert to responsive button when expanded side panel impl is finished
|
||||||
|
|
||||||
fn add_deck_button() -> impl Widget {
|
fn add_deck_button<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
|
||||||
|ui: &mut egui::Ui| -> egui::Response {
|
|ui: &mut egui::Ui| -> egui::Response {
|
||||||
let img_size = 40.0;
|
let img_size = 40.0;
|
||||||
|
|
||||||
@@ -403,7 +403,11 @@ fn add_deck_button() -> impl Widget {
|
|||||||
helper
|
helper
|
||||||
.take_animation_response()
|
.take_animation_response()
|
||||||
.on_hover_cursor(CursorIcon::PointingHand)
|
.on_hover_cursor(CursorIcon::PointingHand)
|
||||||
.on_hover_text("Add new deck")
|
.on_hover_text(tr!(
|
||||||
|
i18n,
|
||||||
|
"Add new deck",
|
||||||
|
"Tooltip text for adding a new deck button"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ impl<'a, 'd> ThreadView<'a, 'd> {
|
|||||||
parent_state = ParentState::Unknown;
|
parent_state = ParentState::Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
for note_ref in &cur_node.replies {
|
for note_ref in cur_node.replies.values() {
|
||||||
if let Ok(note) = self.note_context.ndb.get_note_by_key(txn, note_ref.key) {
|
if let Ok(note) = self.note_context.ndb.get_note_by_key(txn, note_ref.key) {
|
||||||
note_builder.add_reply(note);
|
note_builder.add_reply(note);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
use egui::containers::scroll_area::ScrollBarVisibility;
|
use egui::containers::scroll_area::ScrollBarVisibility;
|
||||||
use egui::{vec2, Direction, Layout, Pos2, Stroke};
|
use egui::{vec2, Color32, Direction, Layout, Margin, Pos2, RichText, ScrollArea, Sense, Stroke};
|
||||||
use egui_tabs::TabColor;
|
use egui_tabs::TabColor;
|
||||||
use nostrdb::Transaction;
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::{Note, ProfileRecord, Transaction};
|
||||||
|
use notedeck::fonts::get_font_size;
|
||||||
|
use notedeck::name::get_display_name;
|
||||||
use notedeck::ui::is_narrow;
|
use notedeck::ui::is_narrow;
|
||||||
use notedeck::JobsCache;
|
use notedeck::{tr_plural, JobsCache, Muted, NotedeckTextStyle};
|
||||||
|
use notedeck_ui::app_images::{like_image, repost_image};
|
||||||
|
use notedeck_ui::{ProfilePic, ProfilePreview};
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
use crate::timeline::{TimelineCache, TimelineKind, TimelineTab, ViewFilter};
|
use crate::timeline::{
|
||||||
|
CompositeType, CompositeUnit, NoteUnit, ReactionUnit, RepostUnit, TimelineCache, TimelineKind,
|
||||||
|
TimelineTab, ViewFilter,
|
||||||
|
};
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
note::root_note_id_from_selected_id, tr, Localization, NoteAction, NoteContext, ScrollInfo,
|
note::root_note_id_from_selected_id, tr, Localization, NoteAction, NoteContext, ScrollInfo,
|
||||||
};
|
};
|
||||||
@@ -20,7 +28,6 @@ pub struct TimelineView<'a, 'd> {
|
|||||||
timeline_id: &'a TimelineKind,
|
timeline_id: &'a TimelineKind,
|
||||||
timeline_cache: &'a mut TimelineCache,
|
timeline_cache: &'a mut TimelineCache,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
reverse: bool,
|
|
||||||
note_context: &'a mut NoteContext<'d>,
|
note_context: &'a mut NoteContext<'d>,
|
||||||
jobs: &'a mut JobsCache,
|
jobs: &'a mut JobsCache,
|
||||||
col: usize,
|
col: usize,
|
||||||
@@ -37,13 +44,11 @@ impl<'a, 'd> TimelineView<'a, 'd> {
|
|||||||
jobs: &'a mut JobsCache,
|
jobs: &'a mut JobsCache,
|
||||||
col: usize,
|
col: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let reverse = false;
|
|
||||||
let scroll_to_top = false;
|
let scroll_to_top = false;
|
||||||
TimelineView {
|
TimelineView {
|
||||||
timeline_id,
|
timeline_id,
|
||||||
timeline_cache,
|
timeline_cache,
|
||||||
note_options,
|
note_options,
|
||||||
reverse,
|
|
||||||
note_context,
|
note_context,
|
||||||
jobs,
|
jobs,
|
||||||
col,
|
col,
|
||||||
@@ -56,7 +61,6 @@ impl<'a, 'd> TimelineView<'a, 'd> {
|
|||||||
ui,
|
ui,
|
||||||
self.timeline_id,
|
self.timeline_id,
|
||||||
self.timeline_cache,
|
self.timeline_cache,
|
||||||
self.reverse,
|
|
||||||
self.note_options,
|
self.note_options,
|
||||||
self.note_context,
|
self.note_context,
|
||||||
self.jobs,
|
self.jobs,
|
||||||
@@ -70,11 +74,6 @@ impl<'a, 'd> TimelineView<'a, 'd> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reversed(mut self) -> Self {
|
|
||||||
self.reverse = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scroll_id(
|
pub fn scroll_id(
|
||||||
timeline_cache: &TimelineCache,
|
timeline_cache: &TimelineCache,
|
||||||
timeline_id: &TimelineKind,
|
timeline_id: &TimelineKind,
|
||||||
@@ -90,8 +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,
|
||||||
reversed: bool,
|
mut note_options: NoteOptions,
|
||||||
note_options: NoteOptions,
|
|
||||||
note_context: &mut NoteContext,
|
note_context: &mut NoteContext,
|
||||||
jobs: &mut JobsCache,
|
jobs: &mut JobsCache,
|
||||||
col: usize,
|
col: usize,
|
||||||
@@ -121,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);
|
||||||
@@ -154,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);
|
||||||
}
|
}
|
||||||
@@ -184,9 +177,12 @@ 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(),
|
||||||
reversed,
|
|
||||||
note_options,
|
note_options,
|
||||||
&txn,
|
&txn,
|
||||||
note_context,
|
note_context,
|
||||||
@@ -195,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));
|
||||||
|
|
||||||
@@ -284,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)
|
||||||
@@ -332,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");
|
||||||
@@ -359,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 {
|
||||||
@@ -380,7 +376,6 @@ fn shrink_range_to_width(range: egui::Rangef, width: f32) -> egui::Rangef {
|
|||||||
|
|
||||||
pub struct TimelineTabView<'a, 'd> {
|
pub struct TimelineTabView<'a, 'd> {
|
||||||
tab: &'a TimelineTab,
|
tab: &'a TimelineTab,
|
||||||
reversed: bool,
|
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
note_context: &'a mut NoteContext<'d>,
|
note_context: &'a mut NoteContext<'d>,
|
||||||
@@ -391,7 +386,6 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
tab: &'a TimelineTab,
|
tab: &'a TimelineTab,
|
||||||
reversed: bool,
|
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
note_context: &'a mut NoteContext<'d>,
|
note_context: &'a mut NoteContext<'d>,
|
||||||
@@ -399,7 +393,6 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tab,
|
tab,
|
||||||
reversed,
|
|
||||||
note_options,
|
note_options,
|
||||||
txn,
|
txn,
|
||||||
note_context,
|
note_context,
|
||||||
@@ -409,57 +402,30 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
|||||||
|
|
||||||
pub fn show(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
|
pub fn show(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
|
||||||
let mut action: Option<NoteAction> = None;
|
let mut action: Option<NoteAction> = None;
|
||||||
let len = self.tab.notes.len();
|
let len = self.tab.units.len();
|
||||||
|
|
||||||
let is_muted = self.note_context.accounts.mutefun();
|
let mute = self.note_context.accounts.mute();
|
||||||
|
|
||||||
self.tab
|
self.tab
|
||||||
.list
|
.list
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.ui_custom_layout(ui, len, |ui, start_index| {
|
.ui_custom_layout(ui, len, |ui, index| {
|
||||||
|
// tracing::info!("rendering index: {index}");
|
||||||
ui.spacing_mut().item_spacing.y = 0.0;
|
ui.spacing_mut().item_spacing.y = 0.0;
|
||||||
ui.spacing_mut().item_spacing.x = 4.0;
|
ui.spacing_mut().item_spacing.x = 4.0;
|
||||||
|
|
||||||
let ind = if self.reversed {
|
let Some(entry) = self.tab.units.get(index) else {
|
||||||
len - start_index - 1
|
|
||||||
} else {
|
|
||||||
start_index
|
|
||||||
};
|
|
||||||
|
|
||||||
let note_key = self.tab.notes[ind].key;
|
|
||||||
|
|
||||||
let note =
|
|
||||||
if let Ok(note) = self.note_context.ndb.get_note_by_key(self.txn, note_key) {
|
|
||||||
note
|
|
||||||
} else {
|
|
||||||
warn!("failed to query note {:?}", note_key);
|
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// should we mute the thread? we might not have it!
|
match self.render_entry(ui, entry, &mute) {
|
||||||
let muted = if let Ok(root_id) = root_note_id_from_selected_id(
|
RenderEntryResponse::Unsuccessful => return 0,
|
||||||
self.note_context.ndb,
|
|
||||||
self.note_context.note_cache,
|
|
||||||
self.txn,
|
|
||||||
note.id(),
|
|
||||||
) {
|
|
||||||
is_muted(¬e, root_id.bytes())
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if !muted {
|
RenderEntryResponse::Success(note_action) => {
|
||||||
notedeck_ui::padding(8.0, ui, |ui| {
|
if let Some(cur_action) = note_action {
|
||||||
let resp =
|
action = Some(cur_action);
|
||||||
NoteView::new(self.note_context, ¬e, self.note_options, self.jobs)
|
}
|
||||||
.show(ui);
|
|
||||||
|
|
||||||
if let Some(note_action) = resp.action {
|
|
||||||
action = Some(note_action)
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
notedeck_ui::hline(ui);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
1
|
1
|
||||||
@@ -467,4 +433,549 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
|||||||
|
|
||||||
action
|
action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_entry(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
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(_) => render_note(
|
||||||
|
ui,
|
||||||
|
self.note_context,
|
||||||
|
self.note_options,
|
||||||
|
self.jobs,
|
||||||
|
&underlying_note,
|
||||||
|
),
|
||||||
|
NoteUnit::Composite(composite) => match composite {
|
||||||
|
CompositeUnit::Reaction(reaction_unit) => render_reaction_cluster(
|
||||||
|
ui,
|
||||||
|
self.note_context,
|
||||||
|
self.note_options,
|
||||||
|
self.jobs,
|
||||||
|
mute,
|
||||||
|
self.txn,
|
||||||
|
&underlying_note,
|
||||||
|
reaction_unit,
|
||||||
|
),
|
||||||
|
CompositeUnit::Repost(repost_unit) => render_repost_cluster(
|
||||||
|
ui,
|
||||||
|
self.note_context,
|
||||||
|
self.note_options,
|
||||||
|
self.jobs,
|
||||||
|
mute,
|
||||||
|
self.txn,
|
||||||
|
&underlying_note,
|
||||||
|
repost_unit,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReferencedNoteType {
|
||||||
|
Tagged,
|
||||||
|
Yours,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositeType {
|
||||||
|
fn image(&self, darkmode: bool) -> egui::Image<'static> {
|
||||||
|
match self {
|
||||||
|
CompositeType::Reaction => like_image(),
|
||||||
|
CompositeType::Repost => {
|
||||||
|
repost_image(darkmode).tint(Color32::from_rgb(0x68, 0xC3, 0x51))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(
|
||||||
|
&self,
|
||||||
|
loc: &mut Localization,
|
||||||
|
first_name: &str,
|
||||||
|
total_count: usize,
|
||||||
|
referenced_type: ReferencedNoteType,
|
||||||
|
notification: bool,
|
||||||
|
) -> String {
|
||||||
|
let count = total_count - 1;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
CompositeType::Reaction => {
|
||||||
|
reaction_description(loc, first_name, count, referenced_type)
|
||||||
|
}
|
||||||
|
CompositeType::Repost => repost_description(
|
||||||
|
loc,
|
||||||
|
first_name,
|
||||||
|
count,
|
||||||
|
if notification {
|
||||||
|
DescriptionType::Notification(referenced_type)
|
||||||
|
} else {
|
||||||
|
DescriptionType::Other
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reaction_description(
|
||||||
|
loc: &mut Localization,
|
||||||
|
first_name: &str,
|
||||||
|
count: usize,
|
||||||
|
referenced_type: ReferencedNoteType,
|
||||||
|
) -> String {
|
||||||
|
match referenced_type {
|
||||||
|
ReferencedNoteType::Tagged => {
|
||||||
|
if count == 0 {
|
||||||
|
tr!(
|
||||||
|
loc,
|
||||||
|
"{name} reacted to a note you were tagged in",
|
||||||
|
"reaction from user to a note you were tagged in",
|
||||||
|
name = first_name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tr_plural!(
|
||||||
|
loc,
|
||||||
|
"{name} and {count} other reacted to a note you were tagged in",
|
||||||
|
"{name} and {count} others reacted to a note you were tagged in",
|
||||||
|
"amount of reactions a note you were tagged in received",
|
||||||
|
count,
|
||||||
|
name = first_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReferencedNoteType::Yours => {
|
||||||
|
if count == 0 {
|
||||||
|
tr!(
|
||||||
|
loc,
|
||||||
|
"{name} reacted to your note",
|
||||||
|
"reaction from user to your note",
|
||||||
|
name = first_name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tr_plural!(
|
||||||
|
loc,
|
||||||
|
"{name} and {count} other reacted to your note",
|
||||||
|
"{name} and {count} others reacted to your note",
|
||||||
|
"describing the amount of reactions your note received",
|
||||||
|
count,
|
||||||
|
name = first_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DescriptionType {
|
||||||
|
Notification(ReferencedNoteType),
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repost_description(
|
||||||
|
loc: &mut Localization,
|
||||||
|
first_name: &str,
|
||||||
|
count: usize,
|
||||||
|
description_type: DescriptionType,
|
||||||
|
) -> String {
|
||||||
|
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 => {
|
||||||
|
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",
|
||||||
|
"repost from user",
|
||||||
|
name = first_name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tr_plural!(
|
||||||
|
loc,
|
||||||
|
"{name} and {count} other reposted",
|
||||||
|
"{name} and {count} others reposted",
|
||||||
|
"describing the amount of reposts a note has",
|
||||||
|
count,
|
||||||
|
name = first_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_note(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
note_context: &mut NoteContext,
|
||||||
|
note_options: NoteOptions,
|
||||||
|
jobs: &mut JobsCache,
|
||||||
|
note: &Note,
|
||||||
|
) -> RenderEntryResponse {
|
||||||
|
let mut action = None;
|
||||||
|
notedeck_ui::padding(8.0, ui, |ui| {
|
||||||
|
let resp = NoteView::new(note_context, note, note_options, jobs).show(ui);
|
||||||
|
|
||||||
|
if let Some(note_action) = resp.action {
|
||||||
|
action = Some(note_action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
notedeck_ui::hline(ui);
|
||||||
|
|
||||||
|
RenderEntryResponse::Success(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn render_reaction_cluster(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
note_context: &mut NoteContext,
|
||||||
|
note_options: NoteOptions,
|
||||||
|
jobs: &mut JobsCache,
|
||||||
|
mute: &std::sync::Arc<Muted>,
|
||||||
|
txn: &Transaction,
|
||||||
|
underlying_note: &Note,
|
||||||
|
reaction: &ReactionUnit,
|
||||||
|
) -> RenderEntryResponse {
|
||||||
|
let profiles_to_show: Vec<ProfileEntry> = reaction
|
||||||
|
.reactions
|
||||||
|
.values()
|
||||||
|
.filter(|r| !mute.is_pk_muted(r.sender.bytes()))
|
||||||
|
.map(|r| &r.sender)
|
||||||
|
.map(|p| ProfileEntry {
|
||||||
|
record: note_context.ndb.get_profile_by_pubkey(txn, p.bytes()).ok(),
|
||||||
|
pk: p,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
render_composite_entry(
|
||||||
|
ui,
|
||||||
|
note_context,
|
||||||
|
note_options | NoteOptions::Notification,
|
||||||
|
jobs,
|
||||||
|
underlying_note,
|
||||||
|
profiles_to_show,
|
||||||
|
CompositeType::Reaction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn render_composite_entry(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
note_context: &mut NoteContext,
|
||||||
|
mut note_options: NoteOptions,
|
||||||
|
jobs: &mut JobsCache,
|
||||||
|
underlying_note: &nostrdb::Note<'_>,
|
||||||
|
profiles_to_show: Vec<ProfileEntry>,
|
||||||
|
composite_type: CompositeType,
|
||||||
|
) -> RenderEntryResponse {
|
||||||
|
let first_name = get_display_name(profiles_to_show.iter().find_map(|opt| opt.record.as_ref()))
|
||||||
|
.name()
|
||||||
|
.to_string();
|
||||||
|
let num_profiles = 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| {
|
||||||
|
let show_label_newline = ui
|
||||||
|
.horizontal_wrapped(|ui| {
|
||||||
|
let pfps_resp = ui
|
||||||
|
.allocate_ui_with_layout(
|
||||||
|
vec2(ui.available_width(), 32.0),
|
||||||
|
Layout::left_to_right(egui::Align::Center),
|
||||||
|
|ui| {
|
||||||
|
render_profiles(
|
||||||
|
ui,
|
||||||
|
profiles_to_show,
|
||||||
|
&composite_type,
|
||||||
|
note_context.img_cache,
|
||||||
|
note_options.contains(NoteOptions::Notification),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.inner;
|
||||||
|
|
||||||
|
if let Some(cur_action) = pfps_resp.action {
|
||||||
|
action = Some(cur_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
let description = composite_type.description(
|
||||||
|
note_context.i18n,
|
||||||
|
&first_name,
|
||||||
|
num_profiles,
|
||||||
|
referenced_type,
|
||||||
|
note_options.contains(NoteOptions::Notification),
|
||||||
|
);
|
||||||
|
let galley = ui.painter().layout_no_wrap(
|
||||||
|
description.clone(),
|
||||||
|
NotedeckTextStyle::Small.get_font_id(ui.ctx()),
|
||||||
|
ui.visuals().text_color(),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
let galley_pos = {
|
||||||
|
let mut galley_pos = ui.next_widget_position();
|
||||||
|
galley_pos.y = pfps_resp.resp.rect.right_center().y;
|
||||||
|
galley_pos.y -= galley.rect.height() / 2.0;
|
||||||
|
galley_pos
|
||||||
|
};
|
||||||
|
|
||||||
|
let fits_no_wrap = {
|
||||||
|
let mut rightmost_pos = galley_pos;
|
||||||
|
rightmost_pos.x += galley.rect.width();
|
||||||
|
|
||||||
|
ui.available_rect_before_wrap().contains(rightmost_pos)
|
||||||
|
};
|
||||||
|
|
||||||
|
if fits_no_wrap {
|
||||||
|
ui.painter()
|
||||||
|
.galley(galley_pos, galley, ui.visuals().text_color());
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(description)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.inner;
|
||||||
|
|
||||||
|
if let Some(desc) = show_label_newline {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(48.0);
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.add(egui::Label::new(
|
||||||
|
RichText::new(desc)
|
||||||
|
.size(get_font_size(ui.ctx(), &NotedeckTextStyle::Small)),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(16.0);
|
||||||
|
|
||||||
|
let resp = ui
|
||||||
|
.horizontal(|ui| {
|
||||||
|
if note_options.contains(NoteOptions::Notification) {
|
||||||
|
note_options = note_options
|
||||||
|
.difference(NoteOptions::ActionBar | NoteOptions::OptionsButton)
|
||||||
|
.union(NoteOptions::NotificationPreview);
|
||||||
|
|
||||||
|
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,
|
||||||
|
note_options: NoteOptions,
|
||||||
|
jobs: &mut JobsCache,
|
||||||
|
mute: &std::sync::Arc<Muted>,
|
||||||
|
txn: &Transaction,
|
||||||
|
underlying_note: &Note,
|
||||||
|
repost: &RepostUnit,
|
||||||
|
) -> RenderEntryResponse {
|
||||||
|
let profiles_to_show: Vec<ProfileEntry> = repost
|
||||||
|
.reposts
|
||||||
|
.values()
|
||||||
|
.filter(|r| !mute.is_pk_muted(r.bytes()))
|
||||||
|
.map(|p| ProfileEntry {
|
||||||
|
record: note_context.ndb.get_profile_by_pubkey(txn, p.bytes()).ok(),
|
||||||
|
pk: p,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
render_composite_entry(
|
||||||
|
ui,
|
||||||
|
note_context,
|
||||||
|
note_options,
|
||||||
|
jobs,
|
||||||
|
underlying_note,
|
||||||
|
profiles_to_show,
|
||||||
|
CompositeType::Repost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RenderEntryResponse {
|
||||||
|
Unsuccessful,
|
||||||
|
Success(Option<NoteAction>),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProfileEntry<'a> {
|
||||||
|
record: Option<ProfileRecord<'a>>,
|
||||||
|
pk: &'a Pubkey,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use egui::{Pos2, Rect, Response, Sense};
|
use egui::{vec2, Pos2, Rect, Response, Sense};
|
||||||
|
|
||||||
pub fn hover_expand(
|
pub fn hover_expand(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
@@ -116,6 +116,16 @@ impl AnimationHelper {
|
|||||||
self.rect
|
self.rect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scaled_rect(&self) -> egui::Rect {
|
||||||
|
let min_height = self.rect.height() * (1.0 / self.expansion_multiple);
|
||||||
|
let min_width = self.rect.width() * (1.0 / self.expansion_multiple);
|
||||||
|
|
||||||
|
egui::Rect::from_center_size(
|
||||||
|
self.center,
|
||||||
|
vec2(self.scale_1d_pos(min_width), self.scale_1d_pos(min_height)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn center(&self) -> Pos2 {
|
pub fn center(&self) -> Pos2 {
|
||||||
self.rect.center()
|
self.rect.center()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,14 @@ pub fn repost_light_image() -> Image<'static> {
|
|||||||
Image::new(include_image!("../../../assets/icons/repost_light_4x.png"))
|
Image::new(include_image!("../../../assets/icons/repost_light_4x.png"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn repost_image(dark_mode: bool) -> Image<'static> {
|
||||||
|
if dark_mode {
|
||||||
|
repost_dark_image()
|
||||||
|
} else {
|
||||||
|
repost_light_image()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reply_dark_image() -> Image<'static> {
|
pub fn reply_dark_image() -> Image<'static> {
|
||||||
Image::new(include_image!("../../../assets/icons/reply.png"))
|
Image::new(include_image!("../../../assets/icons/reply.png"))
|
||||||
}
|
}
|
||||||
@@ -240,3 +248,17 @@ pub fn zap_dark_image() -> Image<'static> {
|
|||||||
pub fn zap_light_image() -> Image<'static> {
|
pub fn zap_light_image() -> Image<'static> {
|
||||||
zap_dark_image().tint(Color32::BLACK)
|
zap_dark_image().tint(Color32::BLACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn like_image() -> Image<'static> {
|
||||||
|
Image::new(include_image!("../../../assets/icons/like_icon_4x.png"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_to_clipboard_image() -> Image<'static> {
|
||||||
|
Image::new(include_image!(
|
||||||
|
"../../../assets/icons/copy-to-clipboard.svg"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_to_clipboard_dark_image() -> Image<'static> {
|
||||||
|
copy_to_clipboard_image().tint(Color32::BLACK)
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ impl Default for Nip51SetWidgetFlags {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Nip51SetWidgetResponse {
|
pub enum Nip51SetWidgetAction {
|
||||||
ViewProfile(Pubkey),
|
ViewProfile(Pubkey),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,21 +73,22 @@ impl<'a> Nip51SetWidget<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<Nip51SetWidgetResponse> {
|
fn render_set(&mut self, ui: &mut egui::Ui, set: &Nip51Set) -> Nip51SetWidgetResponse {
|
||||||
let mut resp = None;
|
if should_skip(set, &self.flags) {
|
||||||
for pack in self.state.iter() {
|
return Nip51SetWidgetResponse {
|
||||||
if should_skip(pack, &self.flags) {
|
action: None,
|
||||||
continue;
|
rendered: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
egui::Frame::new()
|
let action = egui::Frame::new()
|
||||||
.corner_radius(CornerRadius::same(8))
|
.corner_radius(CornerRadius::same(8))
|
||||||
.fill(ui.visuals().extreme_bg_color)
|
//.fill(ui.visuals().extreme_bg_color)
|
||||||
.inner_margin(Margin::same(8))
|
.inner_margin(Margin::same(8))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
if let Some(cur_resp) = render_pack(
|
render_pack(
|
||||||
ui,
|
ui,
|
||||||
pack,
|
set,
|
||||||
self.ui_state,
|
self.ui_state,
|
||||||
self.ndb,
|
self.ndb,
|
||||||
self.images,
|
self.images,
|
||||||
@@ -95,10 +96,39 @@ impl<'a> Nip51SetWidget<'a> {
|
|||||||
self.jobs,
|
self.jobs,
|
||||||
self.loc,
|
self.loc,
|
||||||
self.flags.contains(Nip51SetWidgetFlags::TRUST_IMAGES),
|
self.flags.contains(Nip51SetWidgetFlags::TRUST_IMAGES),
|
||||||
) {
|
)
|
||||||
resp = Some(cur_resp);
|
})
|
||||||
|
.inner;
|
||||||
|
|
||||||
|
Nip51SetWidgetResponse {
|
||||||
|
action,
|
||||||
|
rendered: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_at_index(&mut self, ui: &mut egui::Ui, index: usize) -> Nip51SetWidgetResponse {
|
||||||
|
let Some(set) = self.state.at_index(index) else {
|
||||||
|
return Nip51SetWidgetResponse {
|
||||||
|
action: None,
|
||||||
|
rendered: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
self.render_set(ui, set)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<Nip51SetWidgetAction> {
|
||||||
|
let mut resp = None;
|
||||||
|
for pack in self.state.iter() {
|
||||||
|
let res = self.render_set(ui, pack);
|
||||||
|
|
||||||
|
if let Some(action) = res.action {
|
||||||
|
resp = Some(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !res.rendered {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
}
|
}
|
||||||
@@ -107,6 +137,11 @@ impl<'a> Nip51SetWidget<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Nip51SetWidgetResponse {
|
||||||
|
pub action: Option<Nip51SetWidgetAction>,
|
||||||
|
pub rendered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn should_skip(set: &Nip51Set, required: &Nip51SetWidgetFlags) -> bool {
|
fn should_skip(set: &Nip51Set, required: &Nip51SetWidgetFlags) -> bool {
|
||||||
(required.contains(Nip51SetWidgetFlags::REQUIRES_TITLE) && set.title.is_none())
|
(required.contains(Nip51SetWidgetFlags::REQUIRES_TITLE) && set.title.is_none())
|
||||||
|| (required.contains(Nip51SetWidgetFlags::REQUIRES_IMAGE) && set.image.is_none())
|
|| (required.contains(Nip51SetWidgetFlags::REQUIRES_IMAGE) && set.image.is_none())
|
||||||
@@ -126,7 +161,7 @@ fn render_pack(
|
|||||||
jobs: &mut JobsCache,
|
jobs: &mut JobsCache,
|
||||||
loc: &mut Localization,
|
loc: &mut Localization,
|
||||||
image_trusted: bool,
|
image_trusted: bool,
|
||||||
) -> Option<Nip51SetWidgetResponse> {
|
) -> Option<Nip51SetWidgetAction> {
|
||||||
let max_img_size = vec2(ui.available_width(), 200.0);
|
let max_img_size = vec2(ui.available_width(), 200.0);
|
||||||
|
|
||||||
ui.allocate_new_ui(UiBuilder::new(), |ui| 's: {
|
ui.allocate_new_ui(UiBuilder::new(), |ui| 's: {
|
||||||
@@ -170,9 +205,14 @@ fn render_pack(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if let Some(desc) = &pack.description {
|
if let Some(desc) = &pack.description {
|
||||||
ui.add(egui::Label::new(egui::RichText::new(desc).size(
|
ui.add(egui::Label::new(
|
||||||
get_font_size(ui.ctx(), ¬edeck::NotedeckTextStyle::Heading3),
|
egui::RichText::new(desc)
|
||||||
)));
|
.size(get_font_size(
|
||||||
|
ui.ctx(),
|
||||||
|
¬edeck::NotedeckTextStyle::Heading3,
|
||||||
|
))
|
||||||
|
.color(ui.visuals().weak_text_color()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let checked = ui.checkbox(
|
let checked = ui.checkbox(
|
||||||
ui_state.get_select_all_state(&pack.identifier),
|
ui_state.get_select_all_state(&pack.identifier),
|
||||||
@@ -199,8 +239,9 @@ fn render_pack(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut resp = None;
|
let mut resp = None;
|
||||||
for pk in &pack.pks {
|
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
|
|
||||||
|
for pk in &pack.pks {
|
||||||
let m_profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok();
|
let m_profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok();
|
||||||
|
|
||||||
let cur_state = ui_state.get_pk_selected_state(&pack.identifier, pk);
|
let cur_state = ui_state.get_pk_selected_state(&pack.identifier, pk);
|
||||||
@@ -210,13 +251,15 @@ fn render_pack(
|
|||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if render_profile_item(ui, images, m_profile.as_ref(), cur_state) {
|
if render_profile_item(ui, images, m_profile.as_ref(), cur_state) {
|
||||||
resp = Some(Nip51SetWidgetResponse::ViewProfile(*pk));
|
resp = Some(Nip51SetWidgetAction::ViewProfile(*pk));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PFP_SIZE: f32 = 32.0;
|
||||||
|
|
||||||
fn render_profile_item(
|
fn render_profile_item(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
images: &mut Images,
|
images: &mut Images,
|
||||||
@@ -224,7 +267,7 @@ fn render_profile_item(
|
|||||||
checked: &mut bool,
|
checked: &mut bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let (card_rect, card_resp) =
|
let (card_rect, card_resp) =
|
||||||
ui.allocate_exact_size(vec2(ui.available_width(), 48.0), egui::Sense::click());
|
ui.allocate_exact_size(vec2(ui.available_width(), PFP_SIZE), egui::Sense::click());
|
||||||
|
|
||||||
let mut clicked_response = card_resp;
|
let mut clicked_response = card_resp;
|
||||||
|
|
||||||
@@ -246,13 +289,14 @@ fn render_profile_item(
|
|||||||
|
|
||||||
clicked_response = clicked_response.union(resp.response);
|
clicked_response = clicked_response.union(resp.response);
|
||||||
|
|
||||||
let (pfp_rect, body_rect) = remaining_rect.split_left_right_at_x(remaining_rect.left() + 48.0);
|
let (pfp_rect, body_rect) =
|
||||||
|
remaining_rect.split_left_right_at_x(remaining_rect.left() + PFP_SIZE);
|
||||||
|
|
||||||
let _ = ui.allocate_new_ui(UiBuilder::new().max_rect(pfp_rect), |ui| {
|
let _ = ui.allocate_new_ui(UiBuilder::new().max_rect(pfp_rect), |ui| {
|
||||||
let pfp_resp = ui.add(
|
let pfp_resp = ui.add(
|
||||||
&mut ProfilePic::new(images, get_profile_url(profile))
|
&mut ProfilePic::new(images, get_profile_url(profile))
|
||||||
.sense(Sense::click())
|
.sense(Sense::click())
|
||||||
.size(48.0),
|
.size(PFP_SIZE),
|
||||||
);
|
);
|
||||||
|
|
||||||
clicked_response = clicked_response.union(pfp_resp);
|
clicked_response = clicked_response.union(pfp_resp);
|
||||||
@@ -273,7 +317,7 @@ fn render_profile_item(
|
|||||||
if let Some(disp) = name.display_name {
|
if let Some(disp) = name.display_name {
|
||||||
let galley = painter.layout_no_wrap(
|
let galley = painter.layout_no_wrap(
|
||||||
disp.to_owned(),
|
disp.to_owned(),
|
||||||
NotedeckTextStyle::Heading3.get_font_id(ui.ctx()),
|
NotedeckTextStyle::Body.get_font_id(ui.ctx()),
|
||||||
ui.visuals().text_color(),
|
ui.visuals().text_color(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -334,14 +332,14 @@ fn render_undecorated_note_contents<'a>(
|
|||||||
.selectable(selectable),
|
.selectable(selectable),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ui.add(
|
let mut richtext = RichText::new(block_str)
|
||||||
Label::new(
|
.text_style(NotedeckTextStyle::NoteBody.text_style());
|
||||||
RichText::new(block_str)
|
|
||||||
.text_style(NotedeckTextStyle::NoteBody.text_style()),
|
if options.contains(NoteOptions::NotificationPreview) {
|
||||||
)
|
richtext = richtext.color(egui::Color32::from_rgb(0x87, 0x87, 0x8D));
|
||||||
.wrap()
|
}
|
||||||
.selectable(selectable),
|
|
||||||
);
|
ui.add(Label::new(richtext).wrap().selectable(selectable));
|
||||||
}
|
}
|
||||||
// don't render any more blocks
|
// don't render any more blocks
|
||||||
if truncate {
|
if truncate {
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
+109
-120
@@ -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, ¬e_to_repost, self.flags, self.jobs).show(ui)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show_impl(&mut self, ui: &mut egui::Ui) -> NoteResponse {
|
|
||||||
let txn = self.note.txn().expect("txn");
|
|
||||||
if let Some(note_to_repost) = get_reposted_note(self.note_context.ndb, txn, self.note) {
|
|
||||||
self.show_repost(ui, txn, note_to_repost)
|
|
||||||
} else {
|
|
||||||
self.show_standard(ui)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,16 +380,19 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
) -> egui::InnerResponse<NoteUiResponse> {
|
) -> egui::InnerResponse<NoteUiResponse> {
|
||||||
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
||||||
let mut note_action: Option<NoteAction> = None;
|
let mut note_action: Option<NoteAction> = None;
|
||||||
let pfp_rect = ui
|
let mut pfp_rect = None;
|
||||||
.horizontal(|ui| {
|
|
||||||
|
if !self.flags.contains(NoteOptions::NotificationPreview) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
let pfp_resp = self.pfp(note_key, profile, ui);
|
let pfp_resp = self.pfp(note_key, profile, ui);
|
||||||
let pfp_rect = pfp_resp.bounding_rect;
|
pfp_rect = Some(pfp_resp.bounding_rect);
|
||||||
note_action = pfp_resp
|
note_action = pfp_resp
|
||||||
.into_action(self.note.pubkey())
|
.into_action(self.note.pubkey())
|
||||||
.or(note_action.take());
|
.or(note_action.take());
|
||||||
|
|
||||||
let size = ui.available_size();
|
let size = ui.available_size();
|
||||||
ui.vertical(|ui| 's: {
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
[size.x, self.options().pfp_size() as f32],
|
[size.x, self.options().pfp_size() as f32],
|
||||||
|ui: &mut egui::Ui| {
|
|ui: &mut egui::Ui| {
|
||||||
@@ -460,7 +417,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
.borrow(self.note.tags());
|
.borrow(self.note.tags());
|
||||||
|
|
||||||
if note_reply.reply().is_none() {
|
if note_reply.reply().is_none() {
|
||||||
break 's;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
@@ -477,10 +434,8 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
.or(note_action.take());
|
.or(note_action.take());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
pfp_rect
|
}
|
||||||
})
|
|
||||||
.inner;
|
|
||||||
|
|
||||||
let mut contents =
|
let mut contents =
|
||||||
NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs);
|
NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs);
|
||||||
@@ -530,14 +485,27 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
) -> egui::InnerResponse<NoteUiResponse> {
|
) -> egui::InnerResponse<NoteUiResponse> {
|
||||||
// main design
|
// main design
|
||||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
|
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
|
||||||
|
let (mut note_action, pfp_rect) =
|
||||||
|
if self.flags.contains(NoteOptions::NotificationPreview) {
|
||||||
|
// do not render pfp
|
||||||
|
(None, None)
|
||||||
|
} else {
|
||||||
let pfp_resp = self.pfp(note_key, profile, ui);
|
let pfp_resp = self.pfp(note_key, profile, ui);
|
||||||
let pfp_rect = pfp_resp.bounding_rect;
|
let pfp_rect = pfp_resp.bounding_rect;
|
||||||
let mut note_action: Option<NoteAction> = pfp_resp.into_action(self.note.pubkey());
|
(pfp_resp.into_action(self.note.pubkey()), Some(pfp_rect))
|
||||||
|
};
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
||||||
NoteView::note_header(ui, self.note_context.i18n, self.note, profile, self.flags);
|
if !self.flags.contains(NoteOptions::NotificationPreview) {
|
||||||
|
NoteView::note_header(
|
||||||
|
ui,
|
||||||
|
self.note_context.i18n,
|
||||||
|
self.note,
|
||||||
|
profile,
|
||||||
|
self.flags,
|
||||||
|
);
|
||||||
|
|
||||||
ui.horizontal_wrapped(|ui| 's: {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.spacing_mut().item_spacing.x = 1.0;
|
ui.spacing_mut().item_spacing.x = 1.0;
|
||||||
|
|
||||||
let note_reply = self
|
let note_reply = self
|
||||||
@@ -548,7 +516,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
.borrow(self.note.tags());
|
.borrow(self.note.tags());
|
||||||
|
|
||||||
if note_reply.reply().is_none() {
|
if note_reply.reply().is_none() {
|
||||||
break 's;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
note_action = reply_desc(
|
note_action = reply_desc(
|
||||||
@@ -561,6 +529,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
)
|
)
|
||||||
.or(note_action.take());
|
.or(note_action.take());
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let mut contents =
|
let mut contents =
|
||||||
NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs);
|
NoteContents::new(self.note_context, txn, self.note, self.flags, self.jobs);
|
||||||
@@ -639,9 +608,12 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
|||||||
.then_some(NoteAction::note(NoteId::new(*self.note.id())))
|
.then_some(NoteAction::note(NoteId::new(*self.note.id())))
|
||||||
.or(note_action);
|
.or(note_action);
|
||||||
|
|
||||||
NoteResponse::new(response.response)
|
let mut resp = NoteResponse::new(response.response).with_action(note_action);
|
||||||
.with_action(note_action)
|
if let Some(pfp_rect) = note_ui_resp.pfp_rect {
|
||||||
.with_pfp(note_ui_resp.pfp_rect)
|
resp = resp.with_pfp(pfp_rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,7 +631,7 @@ fn get_zapper<'a>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option<Note<'a>> {
|
pub fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option<Note<'a>> {
|
||||||
if note.kind() != 6 {
|
if note.kind() != 6 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -687,7 +659,7 @@ fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option
|
|||||||
|
|
||||||
struct NoteUiResponse {
|
struct NoteUiResponse {
|
||||||
action: Option<NoteAction>,
|
action: Option<NoteAction>,
|
||||||
pfp_rect: egui::Rect,
|
pfp_rect: Option<egui::Rect>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PfpResponse {
|
struct PfpResponse {
|
||||||
@@ -771,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.
|
||||||
@@ -814,33 +786,14 @@ struct Zapper<'a> {
|
|||||||
cur_acc: KeypairUnowned<'a>,
|
cur_acc: KeypairUnowned<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[profiling::function]
|
fn zap_actionbar_button(
|
||||||
fn render_note_actionbar(
|
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
zapper: Option<Zapper<'_>>,
|
|
||||||
note_id: &[u8; 32],
|
note_id: &[u8; 32],
|
||||||
note_pubkey: &[u8; 32],
|
note_pubkey: &[u8; 32],
|
||||||
note_key: NoteKey,
|
zapper: Option<Zapper<'_>>,
|
||||||
i18n: &mut Localization,
|
i18n: &mut Localization,
|
||||||
) -> Option<NoteAction> {
|
) -> Option<NoteAction> {
|
||||||
ui.set_min_height(26.0);
|
let mut action: Option<NoteAction> = None;
|
||||||
ui.spacing_mut().item_spacing.x = 24.0;
|
|
||||||
|
|
||||||
let reply_resp =
|
|
||||||
reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
|
||||||
|
|
||||||
let quote_resp =
|
|
||||||
quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
|
||||||
|
|
||||||
let to_noteid = |id: &[u8; 32]| NoteId::new(*id);
|
|
||||||
if reply_resp.clicked() {
|
|
||||||
return Some(NoteAction::Reply(to_noteid(note_id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if quote_resp.clicked() {
|
|
||||||
return Some(NoteAction::Quote(to_noteid(note_id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let Zapper { zaps, cur_acc } = zapper?;
|
let Zapper { zaps, cur_acc } = zapper?;
|
||||||
|
|
||||||
let zap_target = ZapTarget::Note(NoteZapTarget {
|
let zap_target = ZapTarget::Note(NoteZapTarget {
|
||||||
@@ -851,39 +804,75 @@ fn render_note_actionbar(
|
|||||||
let zap_state = zaps.any_zap_state_for(cur_acc.pubkey.bytes(), zap_target);
|
let zap_state = zaps.any_zap_state_for(cur_acc.pubkey.bytes(), zap_target);
|
||||||
|
|
||||||
let target = NoteZapTargetOwned {
|
let target = NoteZapTargetOwned {
|
||||||
note_id: to_noteid(note_id),
|
note_id: NoteId::new(*note_id),
|
||||||
zap_recipient: Pubkey::new(*note_pubkey),
|
zap_recipient: Pubkey::new(*note_pubkey),
|
||||||
};
|
};
|
||||||
|
|
||||||
if zap_state.is_err() {
|
|
||||||
return Some(NoteAction::Zap(ZapAction::ClearError(target)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let zap_resp = {
|
|
||||||
cur_acc.secret_key.as_ref()?;
|
cur_acc.secret_key.as_ref()?;
|
||||||
|
|
||||||
match zap_state {
|
match zap_state {
|
||||||
Ok(any_zap_state) => ui.add(zap_button(i18n, any_zap_state, note_id)),
|
Ok(any_zap_state) => {
|
||||||
|
let zap_resp = ui.add(zap_button(i18n, any_zap_state, note_id));
|
||||||
|
|
||||||
|
if zap_resp.secondary_clicked() {
|
||||||
|
action = Some(NoteAction::Zap(ZapAction::CustomizeAmount(target.clone())));
|
||||||
|
}
|
||||||
|
|
||||||
|
if zap_resp.clicked() {
|
||||||
|
action = Some(NoteAction::Zap(ZapAction::Send(ZapTargetAmount {
|
||||||
|
target,
|
||||||
|
specified_msats: None,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
zap_resp
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let (rect, _) = ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
|
let (rect, _) = ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
|
||||||
ui.add(x_button(rect)).on_hover_text(err.to_string())
|
let x_button = ui.add(x_button(rect)).on_hover_text(err.to_string());
|
||||||
|
|
||||||
|
if x_button.clicked() {
|
||||||
|
action = Some(NoteAction::Zap(ZapAction::ClearError(target.clone())));
|
||||||
}
|
}
|
||||||
|
x_button
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||||
|
|
||||||
if zap_resp.secondary_clicked() {
|
action
|
||||||
return Some(NoteAction::Zap(ZapAction::CustomizeAmount(target)));
|
}
|
||||||
|
|
||||||
|
#[profiling::function]
|
||||||
|
fn render_note_actionbar(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
zapper: Option<Zapper<'_>>,
|
||||||
|
note_id: &[u8; 32],
|
||||||
|
note_pubkey: &[u8; 32],
|
||||||
|
note_key: NoteKey,
|
||||||
|
i18n: &mut Localization,
|
||||||
|
) -> Option<NoteAction> {
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
ui.set_min_height(26.0);
|
||||||
|
ui.spacing_mut().item_spacing.x = 24.0;
|
||||||
|
|
||||||
|
let reply_resp =
|
||||||
|
reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||||
|
|
||||||
|
let quote_resp =
|
||||||
|
quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||||
|
|
||||||
|
if reply_resp.clicked() {
|
||||||
|
action = Some(NoteAction::Reply(NoteId::new(*note_id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !zap_resp.clicked() {
|
if quote_resp.clicked() {
|
||||||
return None;
|
action = Some(NoteAction::Quote(NoteId::new(*note_id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(NoteAction::Zap(ZapAction::Send(ZapTargetAmount {
|
action = zap_actionbar_button(ui, note_id, note_pubkey, zapper, i18n).or(action);
|
||||||
target,
|
|
||||||
specified_msats: None,
|
action
|
||||||
})))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[profiling::function]
|
#[profiling::function]
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ bitflags! {
|
|||||||
|
|
||||||
/// no animation override (accessibility)
|
/// no animation override (accessibility)
|
||||||
const NoAnimations = 1 << 17;
|
const NoAnimations = 1 << 17;
|
||||||
|
|
||||||
|
/// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ fi
|
|||||||
|
|
||||||
# Build the .app bundle
|
# Build the .app bundle
|
||||||
echo "Building .app bundle..."
|
echo "Building .app bundle..."
|
||||||
cargo bundle --release --target $TARGET
|
cargo bundle -k notedeck_chrome --release --target $TARGET
|
||||||
|
|
||||||
# Sign the app
|
# Sign the app
|
||||||
echo "Codesigning the app..."
|
echo "Codesigning the app..."
|
||||||
|
|||||||
Reference in New Issue
Block a user