Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
+31
-26
@@ -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",
|
||||||
@@ -193,7 +193,7 @@ dependencies = [
|
|||||||
"objc2-foundation 0.3.1",
|
"objc2-foundation 0.3.1",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -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",
|
||||||
@@ -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",
|
||||||
@@ -3558,7 +3561,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 +3593,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 +3612,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 +3632,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 +3668,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",
|
||||||
@@ -3688,7 +3693,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 +3702,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",
|
||||||
@@ -7447,10 +7452,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",
|
||||||
|
|||||||
+8
-8
@@ -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",
|
||||||
@@ -88,7 +88,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 +106,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" }
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -352,6 +352,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
|
||||||
|
|
||||||
@@ -466,6 +469,9 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -352,6 +352,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{"]"}
|
||||||
|
|
||||||
@@ -466,6 +469,9 @@ See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàç
|
|||||||
# 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{"]"}
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,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
|
||||||
@@ -313,6 +315,8 @@ Searching_for___query_5d18 = Recherche par '{ $query }'
|
|||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = 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
|
||||||
@@ -313,6 +315,8 @@ Searching_for___query_5d18 = Procurando por '{ $query }'
|
|||||||
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
|
||||||
|
|||||||
@@ -46,7 +46,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 +90,11 @@ 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 ของโน้ต
|
||||||
# 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
|
||||||
@@ -164,7 +164,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
|
||||||
@@ -238,7 +238,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,7 +256,7 @@ 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
|
||||||
@@ -292,7 +294,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
|
||||||
@@ -315,6 +317,8 @@ Searching_for___query_5d18 = กำลังค้นหา '{ $query }'
|
|||||||
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 +332,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 +358,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 +380,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 +392,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
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ md5 = { workspace = true }
|
|||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
indexmap = {workspace = true}
|
||||||
|
crossbeam-channel = "0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
@@ -58,6 +60,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
|
||||||
|
|||||||
@@ -183,21 +183,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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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",),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+184
-72
@@ -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
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
// Shrink view so it does not get covered by insets.
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setupInsets();
|
public void openFilePicker() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
//setupFullscreen()
|
intent.setType("*/*");
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||||
//keyboardHelper = new KeyboardHeightHelper(this);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
||||||
@@ -92,35 +68,171 @@ public class MainActivity extends GameActivity {
|
|||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
keyboardHelper.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
keyboardHelper.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
keyboardHelper.close();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Override
|
private void processSelectedFile(Uri uri) {
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
try {
|
||||||
// Offset the location so it fits the view with margins caused by insets.
|
nativeOnFilePickedWithContent(this.getUriInfo(uri), readUriContent(uri));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("MainActivity", "Error processing file: " + uri.toString(), e);
|
||||||
|
|
||||||
int[] location = new int[2];
|
nativeOnFilePickedFailed(uri.toString(), e.toString());
|
||||||
findViewById(android.R.id.content).getLocationOnScreen(location);
|
}
|
||||||
event.offsetLocation(-location[0], -location[1]);
|
|
||||||
|
|
||||||
return super.onTouchEvent(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
// Offset the location so it fits the view with margins caused by insets.
|
||||||
|
|
||||||
|
int[] location = new int[2];
|
||||||
|
findViewById(android.R.id.content).getLocationOnScreen(location);
|
||||||
|
event.offsetLocation(-location[0], -location[1]);
|
||||||
|
|
||||||
|
return super.onTouchEvent(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
|
if let Some(new_notes) = &self.new_notes {
|
||||||
TimelineOpenResult::NewNotes(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());
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,19 +115,22 @@ 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) {
|
||||||
@@ -149,9 +154,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,11 +174,12 @@ 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 = find_new_notes(
|
||||||
timeline.all_or_any_notes(),
|
timeline.all_or_any_entries().latest(),
|
||||||
timeline.subscription.get_filter()?.local(),
|
timeline.subscription.get_filter()?.local(),
|
||||||
txn,
|
txn,
|
||||||
ndb,
|
ndb,
|
||||||
@@ -207,6 +216,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 +247,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) {
|
||||||
|
|||||||
@@ -625,7 +625,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])
|
||||||
.limit(default_limit())
|
.limit(default_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::{InsertionResponse, NoteUnits};
|
||||||
//#[derive(Debug, Hash, Clone, Eq, PartialEq)]
|
pub use timeline_units::{TimelineUnits, UnknownPks};
|
||||||
//pub type TimelineId = TimelineKind;
|
pub use unit::{CompositeUnit, NoteUnit, ReactionUnit};
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -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,54 @@ 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,
|
||||||
|
) -> 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;
|
|
||||||
|
|
||||||
// TODO: technically items could have been added inbetween
|
let resp = self.units.merge_new_notes(payloads, ndb, txn);
|
||||||
if new_items > 0 {
|
|
||||||
let mut list = self.list.borrow_mut();
|
|
||||||
|
|
||||||
match merge_kind {
|
let InsertManyResponse::Some {
|
||||||
// TODO: update egui_virtual_list to support spliced inserts
|
entries_merged,
|
||||||
MergeKind::Spliced => {
|
merge_kind,
|
||||||
debug!(
|
} = resp.insertion_response
|
||||||
"spliced when inserting {} new notes, resetting virtual list",
|
else {
|
||||||
new_refs.len()
|
return resp.tl_response;
|
||||||
);
|
};
|
||||||
list.reset();
|
|
||||||
}
|
let mut list = self.list.borrow_mut();
|
||||||
MergeKind::FrontInsert => {
|
|
||||||
// only run this logic if we're reverse-chronological
|
match merge_kind {
|
||||||
// reversed in this case means chronological, since the
|
// TODO: update egui_virtual_list to support spliced inserts
|
||||||
// default is reverse-chronological. yeah it's confusing.
|
MergeKind::Spliced => {
|
||||||
if !reversed {
|
debug!("spliced when inserting {num_refs} new notes, resetting virtual list",);
|
||||||
debug!("inserting {} new notes at start", new_refs.len());
|
list.reset();
|
||||||
list.items_inserted_at_start(new_items);
|
}
|
||||||
}
|
MergeKind::FrontInsert => {
|
||||||
|
// only run this logic if we're reverse-chronological
|
||||||
|
// reversed in this case means chronological, since the
|
||||||
|
// default is reverse-chronological. yeah it's confusing.
|
||||||
|
if !reversed {
|
||||||
|
debug!("inserting {num_refs} new notes at start");
|
||||||
|
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 +191,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 {
|
||||||
@@ -293,15 +290,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(|| {
|
||||||
.expect("should have at least notes")
|
self.entries(ViewFilter::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 +322,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 +330,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,11 +338,32 @@ 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
|
||||||
@@ -354,7 +378,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 +395,32 @@ 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(res, ndb, txn, reversed) {
|
||||||
view.insert(&refs, reversed);
|
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) {
|
||||||
|
res.process(unknown_ids, ndb, txn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,6 +457,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 +525,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 +598,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 +606,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 +663,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
|
||||||
@@ -654,7 +689,9 @@ fn setup_initial_timeline(
|
|||||||
.map(NoteRef::from_query_result)
|
.map(NoteRef::from_query_result)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
timeline.insert_new(txn, ndb, note_cache, ¬es);
|
if let Some(pks) = timeline.insert_new(txn, ndb, note_cache, ¬es) {
|
||||||
|
pks.process(ndb, txn, unknown_ids);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -663,10 +700,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 +717,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 +725,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 +740,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 +814,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,559 @@
|
|||||||
|
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<NoteKey, StorageIndex>, // `NoteKey` 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: &NoteKey) -> 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 = inserted_new > 0
|
||||||
|
&& if self.order.is_empty() || new_order.is_empty() {
|
||||||
|
true
|
||||||
|
} else if !self.reversed {
|
||||||
|
let first_new = *new_order.first().unwrap();
|
||||||
|
let last_old = *self.order.last().unwrap();
|
||||||
|
self.storage[first_new] >= self.storage[last_old]
|
||||||
|
} else {
|
||||||
|
let last_new = *new_order.last().unwrap();
|
||||||
|
let first_old = *self.order.first().unwrap();
|
||||||
|
self.storage[last_new] <= self.storage[first_old]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut merged = Vec::with_capacity(self.order.len() + new_order.len());
|
||||||
|
let (mut i, mut j) = (0, 0);
|
||||||
|
while i < self.order.len() && j < new_order.len() {
|
||||||
|
let index_left = self.order[i];
|
||||||
|
let index_right = new_order[j];
|
||||||
|
let left_item = &self.storage[index_left];
|
||||||
|
let right_item = &self.storage[index_right];
|
||||||
|
if left_item <= right_item {
|
||||||
|
// left_item is newer than right_item
|
||||||
|
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<NoteKey, 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(&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;
|
||||||
|
|
||||||
|
if let Some(&storage_idx) = self.lookup.get(&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(key) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
NoteUnits,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[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_fragment(&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 fragment(&mut self, reacted_to: NoteRef) -> String {
|
||||||
|
let frag = self.build_fragment(reacted_to);
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
self.frags.insert(id.clone(), frag.clone());
|
||||||
|
|
||||||
|
self.units.merge_fragments(vec![frag]);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fragments_pair(&mut self, reacted_to: NoteRef) -> (String, String) {
|
||||||
|
let frag1 = self.build_fragment(reacted_to);
|
||||||
|
let frag2 = self.build_fragment(reacted_to);
|
||||||
|
|
||||||
|
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 generate_reaction_note(&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_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),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Expect<'a> {
|
||||||
|
Single(&'a String),
|
||||||
|
Reaction(Vec<&'a String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
let mut builder = UnitBuilder::default();
|
||||||
|
let reaction_note = builder.generate_reaction_note();
|
||||||
|
|
||||||
|
let single0 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac1 = builder.fragment(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.fragment(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.fragment(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 test2() {
|
||||||
|
let mut builder = UnitBuilder::default();
|
||||||
|
let reaction_note1 = builder.generate_reaction_note();
|
||||||
|
let reaction_note2 = builder.generate_reaction_note();
|
||||||
|
|
||||||
|
let single0 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac1_1 = builder.fragment(reaction_note1);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1_1]));
|
||||||
|
builder.aeq(1, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac2_1 = builder.fragment(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.fragment(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.fragment(reaction_note1);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac1_2, &reac1_1, &reac1_3]));
|
||||||
|
builder.aeq(1, Expect::Single(&single2));
|
||||||
|
builder.aeq(2, Expect::Single(&single1));
|
||||||
|
builder.aeq(3, Expect::Reaction(vec![&reac2_1]));
|
||||||
|
builder.aeq(4, Expect::Single(&single0));
|
||||||
|
|
||||||
|
let reac2_2 = builder.fragment(reaction_note2);
|
||||||
|
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 test3() {
|
||||||
|
let mut builder = UnitBuilder::default();
|
||||||
|
let reaction_note1 = builder.generate_reaction_note();
|
||||||
|
|
||||||
|
let single1 = builder.insert_note();
|
||||||
|
builder.aeq(0, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let reac0 = builder.fragment(reaction_note1);
|
||||||
|
builder.aeq(0, Expect::Reaction(vec![&reac0]));
|
||||||
|
builder.aeq(1, Expect::Single(&single1));
|
||||||
|
|
||||||
|
let (reac1, reac2) = builder.fragments_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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,13 @@ 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, 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 +28,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 +389,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(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::{Ndb, Note, NoteKey, Transaction};
|
||||||
|
use notedeck::NoteRef;
|
||||||
|
|
||||||
|
use crate::timeline::{
|
||||||
|
note_units::{InsertManyResponse, NoteUnits},
|
||||||
|
unit::{CompositeFragment, NoteUnit, NoteUnitFragment, Reaction, ReactionFragment},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
_ => 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 = NoteRef {
|
||||||
|
key: payload.key,
|
||||||
|
created_at: payload.note.created_at(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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],
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::NoteKey;
|
||||||
|
use notedeck::NoteRef;
|
||||||
|
|
||||||
|
/// 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) -> NoteKey {
|
||||||
|
match self {
|
||||||
|
NoteUnit::Single(note_ref) => note_ref.key,
|
||||||
|
NoteUnit::Composite(clustered_entry) => 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositeUnit {
|
||||||
|
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||||
|
match self {
|
||||||
|
CompositeUnit::Reaction(reaction_unit) => reaction_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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositeUnit {
|
||||||
|
pub fn key(&self) -> NoteKey {
|
||||||
|
match self {
|
||||||
|
CompositeUnit::Reaction(reaction_entry) => reaction_entry.note_reacted_to.key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CompositeFragment> for CompositeUnit {
|
||||||
|
fn from(value: CompositeFragment) -> Self {
|
||||||
|
match value {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => {
|
||||||
|
CompositeUnit::Reaction(reaction_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(Clone)]
|
||||||
|
pub enum NoteUnitFragment {
|
||||||
|
Single(NoteRef),
|
||||||
|
Composite(CompositeFragment),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CompositeFragment {
|
||||||
|
Reaction(ReactionFragment),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositeFragment {
|
||||||
|
pub fn fold_into(self, unit: &mut CompositeUnit) {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => reaction_fragment.fold_into(unit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> NoteKey {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => {
|
||||||
|
reaction_fragment.reaction_note_ref.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_underlying_noteref(&self) -> &NoteRef {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => &reaction_fragment.noteref_reacted_to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||||
|
match self {
|
||||||
|
CompositeFragment::Reaction(reaction_fragment) => &reaction_fragment.reaction_note_ref,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 CompositeUnit) {
|
||||||
|
match unit {
|
||||||
|
CompositeUnit::Reaction(reaction_unit) => {
|
||||||
|
if self.noteref_reacted_to != reaction_unit.note_reacted_to {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if reaction_unit.senders.contains(&self.reaction.sender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reaction_unit.senders.insert(self.reaction.sender);
|
||||||
|
reaction_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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>,
|
||||||
@@ -341,6 +342,22 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
|
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt(PostView::scroll_id())
|
.id_salt(PostView::scroll_id())
|
||||||
.show(ui, |ui| self.ui_no_scroll(txn, ui))
|
.show(ui, |ui| self.ui_no_scroll(txn, ui))
|
||||||
@@ -521,22 +538,14 @@ 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ use nostrdb::Ndb;
|
|||||||
use notedeck::{Images, JobPool, JobsCache, Localization};
|
use notedeck::{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,24 +71,37 @@ 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(
|
||||||
follow_pack_state,
|
ui,
|
||||||
self.ui_state,
|
follow_pack_state.len(),
|
||||||
self.ndb,
|
|ui, index| {
|
||||||
self.loc,
|
let resp = Nip51SetWidget::new(
|
||||||
self.images,
|
follow_pack_state,
|
||||||
self.job_pool,
|
self.ui_state,
|
||||||
self.jobs,
|
self.ndb,
|
||||||
)
|
self.loc,
|
||||||
.with_flags(Nip51SetWidgetFlags::TRUST_IMAGES)
|
self.images,
|
||||||
.ui(ui)
|
self.job_pool,
|
||||||
{
|
self.jobs,
|
||||||
match resp {
|
)
|
||||||
Nip51SetWidgetResponse::ViewProfile(pubkey) => {
|
.with_flags(Nip51SetWidgetFlags::TRUST_IMAGES)
|
||||||
action = Some(OnboardingResponse::ViewProfile(pubkey));
|
.render_at_index(ui, index);
|
||||||
|
|
||||||
|
if let Some(cur_action) = resp.action {
|
||||||
|
match cur_action {
|
||||||
|
Nip51SetWidgetAction::ViewProfile(pubkey) => {
|
||||||
|
action = Some(OnboardingResponse::ViewProfile(pubkey));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
if resp.rendered {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
scroll_area = scroll_area.vertical_scroll_offset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = scroll_area.show(ui, |ui| {
|
let output = scroll_area.show(ui, |ui| 's: {
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
let txn = Transaction::new(self.note_context.ndb).expect("txn");
|
let txn = Transaction::new(self.note_context.ndb).expect("txn");
|
||||||
let profile = self
|
let profile = self
|
||||||
@@ -85,15 +85,13 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
|||||||
if let Some(profile_view_action) = self.profile_body(ui, profile.as_ref()) {
|
if let Some(profile_view_action) = self.profile_body(ui, profile.as_ref()) {
|
||||||
action = Some(profile_view_action);
|
action = Some(profile_view_action);
|
||||||
}
|
}
|
||||||
let profile_timeline = self
|
|
||||||
|
let Some(profile_timeline) = self
|
||||||
.timeline_cache
|
.timeline_cache
|
||||||
.notes(
|
.get_mut(&TimelineKind::Profile(*self.pubkey))
|
||||||
self.note_context.ndb,
|
else {
|
||||||
self.note_context.note_cache,
|
break 's action;
|
||||||
&txn,
|
};
|
||||||
&TimelineKind::Profile(*self.pubkey),
|
|
||||||
)
|
|
||||||
.get_ptr();
|
|
||||||
|
|
||||||
profile_timeline.selected_view = tabs_ui(
|
profile_timeline.selected_view = tabs_ui(
|
||||||
ui,
|
ui,
|
||||||
@@ -116,7 +114,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,
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,19 @@
|
|||||||
use egui::containers::scroll_area::ScrollBarVisibility;
|
use egui::containers::scroll_area::ScrollBarVisibility;
|
||||||
use egui::{vec2, Direction, Layout, Pos2, Stroke};
|
use egui::{vec2, Direction, Layout, Margin, Pos2, ScrollArea, Sense, Stroke};
|
||||||
use egui_tabs::TabColor;
|
use egui_tabs::TabColor;
|
||||||
use nostrdb::Transaction;
|
use enostr::Pubkey;
|
||||||
|
use nostrdb::{ProfileRecord, Transaction};
|
||||||
|
use notedeck::name::get_display_name;
|
||||||
use notedeck::ui::is_narrow;
|
use notedeck::ui::is_narrow;
|
||||||
use notedeck::JobsCache;
|
use notedeck::{JobsCache, Muted, NoteRef};
|
||||||
|
use notedeck_ui::app_images::like_image;
|
||||||
|
use notedeck_ui::ProfilePic;
|
||||||
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::{
|
||||||
|
CompositeUnit, NoteUnit, ReactionUnit, 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 +26,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 +42,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 +59,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 +72,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,7 +87,6 @@ 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,
|
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
note_context: &mut NoteContext,
|
note_context: &mut NoteContext,
|
||||||
jobs: &mut JobsCache,
|
jobs: &mut JobsCache,
|
||||||
@@ -186,7 +182,6 @@ fn timeline_ui(
|
|||||||
|
|
||||||
TimelineTabView::new(
|
TimelineTabView::new(
|
||||||
timeline.current_view(),
|
timeline.current_view(),
|
||||||
reversed,
|
|
||||||
note_options,
|
note_options,
|
||||||
&txn,
|
&txn,
|
||||||
note_context,
|
note_context,
|
||||||
@@ -380,7 +375,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 +385,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 +392,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 +401,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
|
return 0;
|
||||||
} else {
|
|
||||||
start_index
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let note_key = self.tab.notes[ind].key;
|
match self.render_entry(ui, entry, &mute) {
|
||||||
|
RenderEntryResponse::Unsuccessful => return 0,
|
||||||
|
|
||||||
let note =
|
RenderEntryResponse::Success(note_action) => {
|
||||||
if let Ok(note) = self.note_context.ndb.get_note_by_key(self.txn, note_key) {
|
if let Some(cur_action) = note_action {
|
||||||
note
|
action = Some(cur_action);
|
||||||
} else {
|
|
||||||
warn!("failed to query note {:?}", note_key);
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// should we mute the thread? we might not have it!
|
|
||||||
let muted = if let Ok(root_id) = root_note_id_from_selected_id(
|
|
||||||
self.note_context.ndb,
|
|
||||||
self.note_context.note_cache,
|
|
||||||
self.txn,
|
|
||||||
note.id(),
|
|
||||||
) {
|
|
||||||
is_muted(¬e, root_id.bytes())
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if !muted {
|
|
||||||
notedeck_ui::padding(8.0, ui, |ui| {
|
|
||||||
let resp =
|
|
||||||
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 +432,204 @@ 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 {
|
||||||
|
match entry {
|
||||||
|
NoteUnit::Single(note_ref) => render_note(
|
||||||
|
ui,
|
||||||
|
self.note_context,
|
||||||
|
self.note_options,
|
||||||
|
self.jobs,
|
||||||
|
mute,
|
||||||
|
self.txn,
|
||||||
|
note_ref,
|
||||||
|
),
|
||||||
|
NoteUnit::Composite(composite) => match composite {
|
||||||
|
CompositeUnit::Reaction(reaction_unit) => render_reaction_cluster(
|
||||||
|
ui,
|
||||||
|
self.note_context,
|
||||||
|
self.note_options,
|
||||||
|
self.jobs,
|
||||||
|
mute,
|
||||||
|
self.txn,
|
||||||
|
reaction_unit,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_note(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
note_context: &mut NoteContext,
|
||||||
|
note_options: NoteOptions,
|
||||||
|
jobs: &mut JobsCache,
|
||||||
|
mute: &std::sync::Arc<Muted>,
|
||||||
|
txn: &Transaction,
|
||||||
|
note_ref: &NoteRef,
|
||||||
|
) -> RenderEntryResponse {
|
||||||
|
let note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, note_ref.key) {
|
||||||
|
note
|
||||||
|
} else {
|
||||||
|
warn!("failed to query note {:?}", note_ref.key);
|
||||||
|
return RenderEntryResponse::Unsuccessful;
|
||||||
|
};
|
||||||
|
|
||||||
|
let muted = if let Ok(root_id) =
|
||||||
|
root_note_id_from_selected_id(note_context.ndb, note_context.note_cache, txn, note.id())
|
||||||
|
{
|
||||||
|
mute.is_muted(¬e, root_id.bytes())
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if muted {
|
||||||
|
return RenderEntryResponse::Success(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut action = None;
|
||||||
|
notedeck_ui::padding(8.0, ui, |ui| {
|
||||||
|
let resp = NoteView::new(note_context, ¬e, note_options, jobs).show(ui);
|
||||||
|
|
||||||
|
if let Some(note_action) = resp.action {
|
||||||
|
action = Some(note_action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
notedeck_ui::hline(ui);
|
||||||
|
|
||||||
|
RenderEntryResponse::Success(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
reaction: &ReactionUnit,
|
||||||
|
) -> RenderEntryResponse {
|
||||||
|
let reacted_to_key = reaction.note_reacted_to.key;
|
||||||
|
let reacted_to_note = if let Ok(note) = note_context.ndb.get_note_by_key(txn, reacted_to_key) {
|
||||||
|
note
|
||||||
|
} else {
|
||||||
|
warn!("failed to query note {:?}", reacted_to_key);
|
||||||
|
return RenderEntryResponse::Unsuccessful;
|
||||||
|
};
|
||||||
|
|
||||||
|
let profiles_to_show: Vec<ProfileEntry> = reaction
|
||||||
|
.reactions
|
||||||
|
.values()
|
||||||
|
.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();
|
||||||
|
|
||||||
|
let first_name = get_display_name(profiles_to_show.iter().find_map(|opt| opt.record.as_ref()))
|
||||||
|
.name()
|
||||||
|
.to_string();
|
||||||
|
let num_profiles_other = profiles_to_show.len() - 1;
|
||||||
|
|
||||||
|
let mut action = None;
|
||||||
|
egui::Frame::new()
|
||||||
|
.inner_margin(Margin::symmetric(8, 4))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
vec2(ui.available_width(), 32.0),
|
||||||
|
Layout::left_to_right(egui::Align::Center),
|
||||||
|
|ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ui.add_sized(vec2(28.0, 28.0), like_image());
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(16.0);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ScrollArea::horizontal()
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for entry in profiles_to_show {
|
||||||
|
let resp = ui.add(
|
||||||
|
&mut ProfilePic::from_profile_or_default(
|
||||||
|
note_context.img_cache,
|
||||||
|
entry.record.as_ref(),
|
||||||
|
)
|
||||||
|
.size(24.0)
|
||||||
|
.sense(Sense::click()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if resp.clicked() {
|
||||||
|
action = Some(NoteAction::Profile(*entry.pk))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let note_type_desc = if note_context
|
||||||
|
.accounts
|
||||||
|
.get_selected_account()
|
||||||
|
.key
|
||||||
|
.pubkey
|
||||||
|
.bytes()
|
||||||
|
!= reacted_to_note.pubkey()
|
||||||
|
{
|
||||||
|
"note you were tagged in"
|
||||||
|
} else {
|
||||||
|
"your note"
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add_space(2.0);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(52.0);
|
||||||
|
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
if num_profiles_other > 0 {
|
||||||
|
ui.label(format!(
|
||||||
|
"{first_name} and {num_profiles_other} others reacted to {note_type_desc}",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ui.label(format!("{first_name} reacted to {note_type_desc}"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(16.0);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(48.0);
|
||||||
|
let options = note_options
|
||||||
|
.difference(NoteOptions::ActionBar | NoteOptions::OptionsButton)
|
||||||
|
.union(NoteOptions::NotificationPreview);
|
||||||
|
let resp = NoteView::new(note_context, &reacted_to_note, options, jobs).show(ui);
|
||||||
|
|
||||||
|
if let Some(note_action) = resp.action {
|
||||||
|
action = Some(note_action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
notedeck_ui::hline(ui);
|
||||||
|
RenderEntryResponse::Success(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RenderEntryResponse {
|
||||||
|
Unsuccessful,
|
||||||
|
Success(Option<NoteAction>),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProfileEntry<'a> {
|
||||||
|
record: Option<ProfileRecord<'a>>,
|
||||||
|
pk: &'a Pubkey,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,3 +240,7 @@ 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"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ impl Default for Nip51SetWidgetFlags {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Nip51SetWidgetResponse {
|
pub enum Nip51SetWidgetAction {
|
||||||
ViewProfile(Pubkey),
|
ViewProfile(Pubkey),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,32 +73,62 @@ 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 {
|
||||||
|
if should_skip(set, &self.flags) {
|
||||||
|
return Nip51SetWidgetResponse {
|
||||||
|
action: None,
|
||||||
|
rendered: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = egui::Frame::new()
|
||||||
|
.corner_radius(CornerRadius::same(8))
|
||||||
|
//.fill(ui.visuals().extreme_bg_color)
|
||||||
|
.inner_margin(Margin::same(8))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
render_pack(
|
||||||
|
ui,
|
||||||
|
set,
|
||||||
|
self.ui_state,
|
||||||
|
self.ndb,
|
||||||
|
self.images,
|
||||||
|
self.job_pool,
|
||||||
|
self.jobs,
|
||||||
|
self.loc,
|
||||||
|
self.flags.contains(Nip51SetWidgetFlags::TRUST_IMAGES),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.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;
|
let mut resp = None;
|
||||||
for pack in self.state.iter() {
|
for pack in self.state.iter() {
|
||||||
if should_skip(pack, &self.flags) {
|
let res = self.render_set(ui, pack);
|
||||||
continue;
|
|
||||||
|
if let Some(action) = res.action {
|
||||||
|
resp = Some(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
egui::Frame::new()
|
if !res.rendered {
|
||||||
.corner_radius(CornerRadius::same(8))
|
continue;
|
||||||
.fill(ui.visuals().extreme_bg_color)
|
}
|
||||||
.inner_margin(Margin::same(8))
|
|
||||||
.show(ui, |ui| {
|
|
||||||
if let Some(cur_resp) = render_pack(
|
|
||||||
ui,
|
|
||||||
pack,
|
|
||||||
self.ui_state,
|
|
||||||
self.ndb,
|
|
||||||
self.images,
|
|
||||||
self.job_pool,
|
|
||||||
self.jobs,
|
|
||||||
self.loc,
|
|
||||||
self.flags.contains(Nip51SetWidgetFlags::TRUST_IMAGES),
|
|
||||||
) {
|
|
||||||
resp = Some(cur_resp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
||||||
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
|
|
||||||
for pk in &pack.pks {
|
for pk in &pack.pks {
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
|
||||||
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(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -334,14 +334,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 {
|
||||||
|
|||||||
@@ -426,16 +426,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 +463,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 +480,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,37 +531,51 @@ 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 pfp_resp = self.pfp(note_key, profile, ui);
|
let (mut note_action, pfp_rect) =
|
||||||
let pfp_rect = pfp_resp.bounding_rect;
|
if self.flags.contains(NoteOptions::NotificationPreview) {
|
||||||
let mut note_action: Option<NoteAction> = pfp_resp.into_action(self.note.pubkey());
|
// do not render pfp
|
||||||
|
(None, None)
|
||||||
|
} else {
|
||||||
|
let pfp_resp = self.pfp(note_key, profile, ui);
|
||||||
|
let pfp_rect = pfp_resp.bounding_rect;
|
||||||
|
(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.horizontal_wrapped(|ui| 's: {
|
|
||||||
ui.spacing_mut().item_spacing.x = 1.0;
|
|
||||||
|
|
||||||
let note_reply = self
|
|
||||||
.note_context
|
|
||||||
.note_cache
|
|
||||||
.cached_note_or_insert_mut(note_key, self.note)
|
|
||||||
.reply
|
|
||||||
.borrow(self.note.tags());
|
|
||||||
|
|
||||||
if note_reply.reply().is_none() {
|
|
||||||
break 's;
|
|
||||||
}
|
|
||||||
|
|
||||||
note_action = reply_desc(
|
|
||||||
ui,
|
ui,
|
||||||
txn,
|
self.note_context.i18n,
|
||||||
¬e_reply,
|
self.note,
|
||||||
self.note_context,
|
profile,
|
||||||
self.flags,
|
self.flags,
|
||||||
self.jobs,
|
);
|
||||||
)
|
|
||||||
.or(note_action.take());
|
ui.horizontal_wrapped(|ui| {
|
||||||
});
|
ui.spacing_mut().item_spacing.x = 1.0;
|
||||||
|
|
||||||
|
let note_reply = self
|
||||||
|
.note_context
|
||||||
|
.note_cache
|
||||||
|
.cached_note_or_insert_mut(note_key, self.note)
|
||||||
|
.reply
|
||||||
|
.borrow(self.note.tags());
|
||||||
|
|
||||||
|
if note_reply.reply().is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
note_action = reply_desc(
|
||||||
|
ui,
|
||||||
|
txn,
|
||||||
|
¬e_reply,
|
||||||
|
self.note_context,
|
||||||
|
self.flags,
|
||||||
|
self.jobs,
|
||||||
|
)
|
||||||
|
.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 +654,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,7 +705,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 {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ bitflags! {
|
|||||||
|
|
||||||
/// no animation override (accessibility)
|
/// no animation override (accessibility)
|
||||||
const NoAnimations = 1 << 17;
|
const NoAnimations = 1 << 17;
|
||||||
|
|
||||||
|
/// Styled for a notification preview
|
||||||
|
const NotificationPreview = 1 << 18;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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