Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c1d3be4c07
|
@@ -18,7 +18,4 @@ export OLLAMA_HOST=http://ollama.jb55.com
|
|||||||
|
|
||||||
# simple todo reminders
|
# simple todo reminders
|
||||||
export TODO_FILE=TODO
|
export TODO_FILE=TODO
|
||||||
|
|
||||||
export RUST_LOG="egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug,lnsocket=trace,notedeck_clndash=debug"
|
|
||||||
|
|
||||||
2>/dev/null todo.sh ls || :
|
2>/dev/null todo.sh ls || :
|
||||||
|
|||||||
Generated
+37
-83
@@ -105,8 +105,7 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "android-activity"
|
name = "android-activity"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5#a8948332c7c551303d32eb26a59d0abd676e47a5"
|
||||||
checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-properties",
|
"android-properties",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
@@ -126,7 +125,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=4ee16f1585e4a75031dc10785163d4b920f95805#4ee16f1585e4a75031dc10785163d4b920f95805"
|
source = "git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9#c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-properties",
|
"android-properties",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
@@ -193,7 +192,7 @@ dependencies = [
|
|||||||
"objc2-foundation 0.3.1",
|
"objc2-foundation 0.3.1",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1403,7 +1402,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=a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d#a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d"
|
source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
@@ -1420,17 +1419,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||||
"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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1466,13 +1465,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
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=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||||
"epaint",
|
"epaint",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
@@ -1484,7 +1483,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1503,7 +1502,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"arboard",
|
"arboard",
|
||||||
@@ -1521,7 +1520,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -1538,7 +1537,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -1555,7 +1554,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_nav"
|
name = "egui_nav"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/damus-io/egui-nav?rev=de6e2d51892478fdd516df166f866e64dedbae07#de6e2d51892478fdd516df166f866e64dedbae07"
|
source = "git+https://github.com/damus-io/egui-nav?rev=3c67eb6298edbff36d46546897cfac33df4f04db#3c67eb6298edbff36d46546897cfac33df4f04db"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
@@ -1617,7 +1616,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1715,13 +1714,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"ecolor",
|
"ecolor",
|
||||||
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=c9073832236dadec21f263ef1f1ffa4d7a159f72)",
|
"emath 0.31.1 (git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd)",
|
||||||
"epaint_default_fonts",
|
"epaint_default_fonts",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
@@ -1733,7 +1732,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=c9073832236dadec21f263ef1f1ffa4d7a159f72#c9073832236dadec21f263ef1f1ffa4d7a159f72"
|
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equator"
|
name = "equator"
|
||||||
@@ -2337,12 +2336,6 @@ version = "0.12.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.13.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.4"
|
version = "0.15.4"
|
||||||
@@ -2371,9 +2364,6 @@ name = "hex"
|
|||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex-conservative"
|
name = "hex-conservative"
|
||||||
@@ -3039,7 +3029,6 @@ dependencies = [
|
|||||||
"bech32",
|
"bech32",
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"lightning-types",
|
"lightning-types",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3075,22 +3064,6 @@ version = "0.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lnsocket"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "724c7fba2188a49ab31316e52dd410d4d3168b8e6482aa2ac3889dd840d28712"
|
|
||||||
dependencies = [
|
|
||||||
"bitcoin",
|
|
||||||
"hashbrown 0.13.2",
|
|
||||||
"hex",
|
|
||||||
"lightning-types",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -3505,16 +3478,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck"
|
name = "notedeck"
|
||||||
version = "0.7.1"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"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",
|
|
||||||
"crossbeam-channel",
|
|
||||||
"dirs",
|
"dirs",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -3528,12 +3498,10 @@ 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",
|
||||||
@@ -3549,6 +3517,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
|
"sys-locale",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokenator",
|
"tokenator",
|
||||||
@@ -3561,9 +3530,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_chrome"
|
name = "notedeck_chrome"
|
||||||
version = "0.7.1"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-winit",
|
"egui-winit",
|
||||||
@@ -3571,7 +3539,6 @@ dependencies = [
|
|||||||
"egui_tabs",
|
"egui_tabs",
|
||||||
"nostrdb",
|
"nostrdb",
|
||||||
"notedeck",
|
"notedeck",
|
||||||
"notedeck_clndash",
|
|
||||||
"notedeck_columns",
|
"notedeck_columns",
|
||||||
"notedeck_dave",
|
"notedeck_dave",
|
||||||
"notedeck_notebook",
|
"notedeck_notebook",
|
||||||
@@ -3591,28 +3558,9 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "notedeck_clndash"
|
|
||||||
version = "0.7.1"
|
|
||||||
dependencies = [
|
|
||||||
"eframe",
|
|
||||||
"egui",
|
|
||||||
"egui_extras",
|
|
||||||
"hex",
|
|
||||||
"lightning-invoice",
|
|
||||||
"lnsocket",
|
|
||||||
"nostrdb",
|
|
||||||
"notedeck",
|
|
||||||
"notedeck_ui",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_columns"
|
name = "notedeck_columns"
|
||||||
version = "0.7.1"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bech32",
|
"bech32",
|
||||||
@@ -3632,8 +3580,6 @@ 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",
|
||||||
@@ -3668,7 +3614,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_dave"
|
name = "notedeck_dave"
|
||||||
version = "0.7.1"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-openai",
|
"async-openai",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -3676,7 +3622,6 @@ dependencies = [
|
|||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
"egui_extras",
|
|
||||||
"enostr",
|
"enostr",
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -3693,7 +3638,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_notebook"
|
name = "notedeck_notebook"
|
||||||
version = "0.7.1"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"jsoncanvas",
|
"jsoncanvas",
|
||||||
@@ -3702,7 +3647,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notedeck_ui"
|
name = "notedeck_ui"
|
||||||
version = "0.7.1"
|
version = "0.5.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"eframe",
|
"eframe",
|
||||||
@@ -5777,6 +5722,15 @@ dependencies = [
|
|||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sys-locale"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sysinfo"
|
name = "sysinfo"
|
||||||
version = "0.30.13"
|
version = "0.30.13"
|
||||||
@@ -7452,10 +7406,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=a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d#a07ea4c4d76cdc5306da8e8aabb05cab1a7d8e4d"
|
source = "git+https://github.com/damus-io/winit?rev=eaff639ab0a14fccf595241f687be883154b267c#eaff639ab0a14fccf595241f687be883154b267c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=4ee16f1585e4a75031dc10785163d4b920f95805)",
|
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=c3c0decc83c4d6c94d2c448391fc8dd51b13f3d9)",
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
@@ -7507,7 +7461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
|
checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"android-activity 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"android-activity 0.6.0 (git+https://github.com/damus-io/android-activity?rev=a8948332c7c551303d32eb26a59d0abd676e47a5)",
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"block2 0.5.1",
|
"block2 0.5.1",
|
||||||
|
|||||||
+15
-17
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
package.version = "0.7.1"
|
package.version = "0.5.9"
|
||||||
members = [
|
members = [
|
||||||
"crates/notedeck",
|
"crates/notedeck",
|
||||||
"crates/notedeck_chrome",
|
"crates/notedeck_chrome",
|
||||||
@@ -8,14 +8,12 @@ members = [
|
|||||||
"crates/notedeck_dave",
|
"crates/notedeck_dave",
|
||||||
"crates/notedeck_notebook",
|
"crates/notedeck_notebook",
|
||||||
"crates/notedeck_ui",
|
"crates/notedeck_ui",
|
||||||
"crates/notedeck_clndash",
|
|
||||||
|
|
||||||
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
|
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
opener = "0.8.2"
|
opener = "0.8.2"
|
||||||
chrono = "0.4.40"
|
|
||||||
base32 = "0.4.0"
|
base32 = "0.4.0"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
rmpv = "1.3.0"
|
rmpv = "1.3.0"
|
||||||
@@ -27,7 +25,7 @@ egui = { version = "0.31.1", features = ["serde"] }
|
|||||||
egui-wgpu = "0.31.1"
|
egui-wgpu = "0.31.1"
|
||||||
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
egui_extras = { version = "0.31.1", features = ["all_loaders"] }
|
||||||
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
|
||||||
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "de6e2d51892478fdd516df166f866e64dedbae07" }
|
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "3c67eb6298edbff36d46546897cfac33df4f04db" }
|
||||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" }
|
||||||
#egui_virtual_list = "0.6.0"
|
#egui_virtual_list = "0.6.0"
|
||||||
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
|
||||||
@@ -37,7 +35,7 @@ ewebsock = { version = "0.2.0", features = ["tls"] }
|
|||||||
fluent = "0.17.0"
|
fluent = "0.17.0"
|
||||||
fluent-resmgr = "0.0.8"
|
fluent-resmgr = "0.0.8"
|
||||||
fluent-langneg = "0.13"
|
fluent-langneg = "0.13"
|
||||||
hex = { version = "0.4.3", features = ["serde"] }
|
hex = "0.4.3"
|
||||||
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
|
||||||
indexmap = "2.6.0"
|
indexmap = "2.6.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
@@ -49,7 +47,6 @@ nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b
|
|||||||
#nostrdb = "0.6.1"
|
#nostrdb = "0.6.1"
|
||||||
notedeck = { path = "crates/notedeck" }
|
notedeck = { path = "crates/notedeck" }
|
||||||
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
notedeck_chrome = { path = "crates/notedeck_chrome" }
|
||||||
notedeck_clndash = { path = "crates/notedeck_clndash" }
|
|
||||||
notedeck_columns = { path = "crates/notedeck_columns" }
|
notedeck_columns = { path = "crates/notedeck_columns" }
|
||||||
notedeck_dave = { path = "crates/notedeck_dave" }
|
notedeck_dave = { path = "crates/notedeck_dave" }
|
||||||
notedeck_notebook = { path = "crates/notedeck_notebook" }
|
notedeck_notebook = { path = "crates/notedeck_notebook" }
|
||||||
@@ -72,6 +69,7 @@ tracing-appender = "0.2.3"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tempfile = "3.13.0"
|
tempfile = "3.13.0"
|
||||||
unic-langid = { version = "0.9.6", features = ["macros"] }
|
unic-langid = { version = "0.9.6", features = ["macros"] }
|
||||||
|
sys-locale = "0.3"
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = { version = "1.10.0", features = ["v4"] }
|
uuid = { version = "1.10.0", features = ["v4"] }
|
||||||
@@ -81,14 +79,14 @@ mime_guess = "2.0.5"
|
|||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
jni = "0.21.1"
|
jni = "0.21.1"
|
||||||
profiling = "1.0"
|
profiling = "1.0"
|
||||||
lightning-invoice = { version = "0.33.1", features = ["serde"] }
|
lightning-invoice = "0.33.1"
|
||||||
secp256k1 = "0.30.0"
|
secp256k1 = "0.30.0"
|
||||||
hashbrown = "0.15.2"
|
hashbrown = "0.15.2"
|
||||||
openai-api-rs = "6.0.3"
|
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 = "4ee16f1585e4a75031dc10785163d4b920f95805", features = [ "game-activity" ] }
|
|
||||||
|
|
||||||
[profile.small]
|
[profile.small]
|
||||||
inherits = 'release'
|
inherits = 'release'
|
||||||
@@ -106,15 +104,15 @@ 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 = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
egui = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
eframe = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
eframe = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
egui-winit = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
egui-winit = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
egui-wgpu = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
egui_extras = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
egui_extras = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
epaint = { git = "https://github.com/damus-io/egui", rev = "c9073832236dadec21f263ef1f1ffa4d7a159f72" }
|
epaint = { git = "https://github.com/damus-io/egui", rev = "a67ab901e197ce13948ff7d00aa6e07e31a68ccd" }
|
||||||
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 = "14d61a74bee0c9863abe7ef28efae2c4d8bd3743" }
|
||||||
#winit = { path = "/home/jb55/dev/github/rust-windowing/winit" }
|
#winit = { path = "/home/jb55/dev/github/rust-windowing/winit" }
|
||||||
#android-activity = { git = "https://github.com/damus-io/android-activity", rev = "f56c974aa5182d5fbd361879f5899eb8f11a37ec" }
|
android-activity = { git = "https://github.com/damus-io/android-activity", rev = "a8948332c7c551303d32eb26a59d0abd676e47a5" }
|
||||||
#android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" }
|
#android-activity = { path = "/home/jb55/dev/github/rust-mobile/android-activity/android-activity" }
|
||||||
|
|||||||
@@ -27,4 +27,4 @@ push-android-config:
|
|||||||
android: jni
|
android: jni
|
||||||
cd $(ANDROID_DIR) && ./gradlew installDebug
|
cd $(ANDROID_DIR) && ./gradlew installDebug
|
||||||
adb shell am start -n com.damus.notedeck/.MainActivity
|
adb shell am start -n com.damus.notedeck/.MainActivity
|
||||||
adb logcat -v color -s GameActivity -s RustStdoutStderr -s threaded_app | tee logcat.txt
|
adb logcat -v color -s RustStdoutStderr -s threaded_app | tee logcat.txt
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="256mm"
|
|
||||||
height="256mm"
|
|
||||||
viewBox="0 0 256 256"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
sodipodi:docname="clnlogo.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:zoom="1.078823"
|
|
||||||
inkscape:cx="396.72867"
|
|
||||||
inkscape:cy="561.25984"
|
|
||||||
inkscape:window-width="2020"
|
|
||||||
inkscape:window-height="1420"
|
|
||||||
inkscape:window-x="270"
|
|
||||||
inkscape:window-y="20"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1" />
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="matrix(1.0800571,0,0,1.0347966,-2.6149197,-3.0116377)"
|
|
||||||
style="display:inline">
|
|
||||||
<g
|
|
||||||
id="g4"
|
|
||||||
transform="matrix(0.43515072,0,0,0.43515072,68.289343,9.0200629)">
|
|
||||||
<path
|
|
||||||
class="st1"
|
|
||||||
d="M 214.6,0 2.2,285.8 246.4,222.3 100.1,222.4 Z"
|
|
||||||
id="path3"
|
|
||||||
style="fill:#f0d003" />
|
|
||||||
<path
|
|
||||||
fill="#fffae6"
|
|
||||||
d="M 31.8,550.7 244.1,264.9 0,328.4 146.3,328.3 Z"
|
|
||||||
id="path4" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -45,8 +45,6 @@ Algo_2452 = Algorithmus
|
|||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmische Feeds zur Hilfe bei der Entdeckung von Notizen
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = Menge
|
Amount_70f0 = Menge
|
||||||
# Label for appearance settings section
|
|
||||||
Appearance_4c7f = Darstellung
|
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = Fragen
|
Ask_b7f4 = Fragen
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -61,18 +59,10 @@ Broadcast_fe43 = Senden
|
|||||||
Broadcast_Local_7e50 = Lokal senden
|
Broadcast_Local_7e50 = Lokal senden
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = Abbrechen
|
Cancel_ed3b = Abbrechen
|
||||||
# Label for cancel clear cache, Storage settings section
|
|
||||||
Cancel_fd8b = Abbrechen
|
|
||||||
# Label for clear cache button, Storage settings section
|
|
||||||
Clear_cache_dccb = Zwischenspeicher leeren
|
|
||||||
# Hover text for editable zap amount
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = Zum Bearbeiten anklicken
|
Click_to_edit_0414 = Zum Bearbeiten anklicken
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = Notiz erstellen
|
Compose_Note_c094 = Notiz erstellen
|
||||||
# Label for configure relays, settings section
|
|
||||||
Configure_relays_d156 = Relays konfigurieren
|
|
||||||
# Label for confirm clear cache, Storage settings section
|
|
||||||
Confirm_9d9d = Bestätigen
|
|
||||||
# Button label to confirm an action
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = Bestätigen
|
Confirm_f8a6 = Bestätigen
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -98,19 +88,19 @@ Copy_Pubkey_9cc4 = Pubkey kopieren
|
|||||||
# Copy the text content of the note to clipboard
|
# Copy the text content of the note to clipboard
|
||||||
Copy_Text_f81c = Text kopieren
|
Copy_Text_f81c = Text kopieren
|
||||||
# Relative time in days
|
# Relative time in days
|
||||||
count_d_b9be = { $count }T
|
count_d_b9be = { $count }Tg.
|
||||||
# Relative time in hours
|
# Relative time in hours
|
||||||
count_h_3ecb = { $count }h
|
count_h_3ecb = { $count }Std.
|
||||||
# Relative time in minutes
|
# Relative time in minutes
|
||||||
count_m_b41e = { $count }min
|
count_m_b41e = { $count }Min.
|
||||||
# Relative time in months
|
# Relative time in months
|
||||||
count_mo_7aba = { $count }M
|
count_mo_7aba = { $count }Mon.
|
||||||
# Relative time in seconds
|
# Relative time in seconds
|
||||||
count_s_aa26 = { $count }s
|
count_s_aa26 = { $count }Sek.
|
||||||
# Relative time in weeks
|
# Relative time in weeks
|
||||||
count_w_7468 = { $count }W
|
count_w_7468 = { $count }Wo.
|
||||||
# Relative time in years
|
# Relative time in years
|
||||||
count_y_9408 = { $count }J
|
count_y_9408 = { $count }J.
|
||||||
# Button to create a new account
|
# Button to create a new account
|
||||||
Create_Account_6994 = Konto erstellen
|
Create_Account_6994 = Konto erstellen
|
||||||
# Button label to create a new deck
|
# Button label to create a new deck
|
||||||
@@ -121,8 +111,6 @@ Custom_a69e = Benutzerdefiniert
|
|||||||
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
|
Customize_Zap_Amount_cfc4 = Zap-Betrag anpassen
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = Damus Support
|
Damus_Support_27c0 = Damus Support
|
||||||
# Label for Theme Dark, Appearance settings section
|
|
||||||
Dark_85fe = Dunkel
|
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = Deck-Name
|
Deck_name_cd32 = Deck-Name
|
||||||
# Label for decks section in side panel
|
# Label for decks section in side panel
|
||||||
@@ -163,16 +151,12 @@ Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__ns
|
|||||||
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
|
Für das Veröffentlichen von Beiträgen und andere Aktionen ist dein privater Schlüssel erforderlich.
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = Profil finden
|
Find_User_bd12 = Profil finden
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = Schriftgröße:
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Startseite
|
Home_8c19 = Startseite
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = Symbol
|
Icon_b0ab = Symbol
|
||||||
# Label for Image cache size, Storage settings section
|
|
||||||
Image_cache_size_3004 = Bildcache Größe:
|
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = Individuell
|
Individual_b776 = Individuell
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
@@ -193,12 +177,8 @@ k_50K_c2dc = 50K
|
|||||||
k_5K_f7e6 = 5K
|
k_5K_f7e6 = 5K
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
|
Keep_track_of_your_notes___replies_a334 = Behalte den Überblick über deine Notizen & Antworten
|
||||||
# Label for language, Appearance settings section
|
|
||||||
Language_e264 = Sprache:
|
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = Letzte Notiz pro Profil
|
Last_Note_per_User_17ad = Letzte Notiz pro Profil
|
||||||
# Label for Theme Light, Appearance settings section
|
|
||||||
Light_7475 = Hell
|
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
|
Lightning_network_address__lud16_ea51 = Lightning-Netzwerkadresse (lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -236,17 +216,11 @@ Notifications_d673 = Benachrichtigungen
|
|||||||
# Title for notifications column
|
# Title for notifications column
|
||||||
Notifications_ef56 = Benachrichtigungen
|
Notifications_ef56 = Benachrichtigungen
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = Gerade eben
|
now_2181 = Jetzt
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
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
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Öffne deinen Standard-E-Mail-Client, um Hilfe vom Damus-Team zu erhalten
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Öffne deinen Standard-E-Mail-Client, um Hilfe vom Damus-Team zu erhalten
|
||||||
# Label for others settings section
|
|
||||||
Others_7267 = Andere
|
|
||||||
# Placeholder text for NWC URI input
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
|
Paste_your_NWC_URI_here_b471 = Füge hier deine NWC-URI ein...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
@@ -293,10 +267,6 @@ replying_to_a_note_e0bc = Antwort auf eine Notiz
|
|||||||
Repost_this_note_8e56 = Diese Notiz teilen
|
Repost_this_note_8e56 = Diese Notiz teilen
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Teilen
|
Reposted_61c8 = Teilen
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = Zurücksetzen
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
|
||||||
Reset_62d4 = Zurücksetzen
|
|
||||||
# Heading for support section
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
|
Running_into_a_bug_1796 = Ein Fehler aufgetreten?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||||
@@ -317,12 +287,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
|
|
||||||
Settings_7a4f = Einstellungen
|
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Zeige die letzte Notiz für jedes Profil aus einer Liste
|
Show_the_last_note_for_each_user_from_a_list_50e7 = Zeige die letzte Notiz für jedes Profil aus einer Liste
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -331,8 +297,6 @@ Sign_out_337b = Abmelden
|
|||||||
Someone_else_s_Notes_7e5f = Notizen anderer Profile
|
Someone_else_s_Notes_7e5f = Notizen anderer Profile
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
|
Someone_else_s_Notifications_82e6 = Mitteilungen anderer Profile
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = Neueste Antworten zuerst sortieren:
|
|
||||||
# Description for contact list column
|
# Description for contact list column
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Die letzte Notiz für jedes Profil aus deiner Kontaktliste anzeigen
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Die letzte Notiz für jedes Profil aus deiner Kontaktliste anzeigen
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -351,14 +315,10 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Bleib bei deinen Ben
|
|||||||
Step_1_8656 = Schritt 1
|
Step_1_8656 = Schritt 1
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = Schritt 2
|
Step_2_d08d = Schritt 2
|
||||||
# Label for storage settings section
|
|
||||||
Storage_ed65 = Speicher
|
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
|
Subscribe_to_someone_else_s_notes_d1e9 = Abonniere die Notizen eines anderen
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
|
Subscribe_to_someone_s_notes_b3c8 = Abonniere die Notizen von jemandem
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = E-Mail Support:
|
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
|
Switch_to_dark_mode_4dec = Zum Dunkelmodus wechseln
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -367,8 +327,6 @@ Switch_to_light_mode_72ce = Zum Hellmodus wechseln
|
|||||||
Tap_to_Load_4b05 = Zum Laden antippen
|
Tap_to_Load_4b05 = Zum Laden antippen
|
||||||
# Message shown when Dave trial period has ended
|
# Message shown when Dave trial period has ended
|
||||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Die Testphase des Dave Nostr KI-Assistenten ist beendet :(. Vielen Dank fürs Ausprobieren! Zap-fähiger Dave kommt bald!
|
||||||
# Label for theme, Appearance settings section
|
|
||||||
Theme_4aac = Design:
|
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = Unterhaltung
|
Thread_0f20 = Unterhaltung
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
@@ -383,8 +341,6 @@ Use_this_wallet_for_the_current_account_only_61dc = Diese Wallet nur für das ak
|
|||||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
|
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" bei "{ $domain }" wird für die Identifikation verwendet werden
|
||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Benutzername
|
Username_daa7 = Benutzername
|
||||||
# Label for view folder button, Storage settings section
|
|
||||||
View_folder_9742 = Ordner anzeigen
|
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Wallet
|
Wallet_5e50 = Wallet
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -403,8 +359,6 @@ Your_Notifications_080d = Deine Benachrichtigungen
|
|||||||
Zap_16b4 = Zap
|
Zap_16b4 = Zap
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Zappe diese Notiz
|
Zap_this_note_42b2 = Zappe diese Notiz
|
||||||
# Label for zoom level, Appearance settings section
|
|
||||||
Zoom_Level_29a8 = Zoomstufe:
|
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ Banner_52ef = Banner
|
|||||||
# Beta version label
|
# Beta version label
|
||||||
BETA_8e5d = BETA
|
BETA_8e5d = BETA
|
||||||
|
|
||||||
|
# Option in settings section to show the source client label at the bottom of the note
|
||||||
|
Bottom_33c8 = Bottom
|
||||||
|
|
||||||
# Broadcast the note to all connected relays
|
# Broadcast the note to all connected relays
|
||||||
Broadcast_fe43 = Broadcast
|
Broadcast_fe43 = Broadcast
|
||||||
|
|
||||||
@@ -238,12 +241,12 @@ 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
|
Find_User_bd12 = Find User
|
||||||
|
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = Font size:
|
|
||||||
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
|
|
||||||
|
# Option in settings section to hide the source client label in note display
|
||||||
|
Hide_281d = Hide
|
||||||
|
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Home
|
Home_8c19 = Home
|
||||||
|
|
||||||
@@ -349,12 +352,6 @@ Notifications_ef56 = Notifications
|
|||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = now
|
now_2181 = now
|
||||||
|
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
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
|
||||||
|
|
||||||
@@ -433,9 +430,6 @@ Repost_this_note_8e56 = Repost this note
|
|||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Reposted
|
Reposted_61c8 = Reposted
|
||||||
|
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = Reset
|
|
||||||
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
# Label for reset zoom level, Appearance settings section
|
||||||
Reset_62d4 = Reset
|
Reset_62d4 = Reset
|
||||||
|
|
||||||
@@ -469,15 +463,15 @@ 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
|
||||||
|
|
||||||
# Column title for app settings
|
# Column title for app settings
|
||||||
Settings_7a4f = Settings
|
Settings_7a4f = Settings
|
||||||
|
|
||||||
|
# Label for Show source client, others settings section
|
||||||
|
Show_source_client_9e31 = Show source client
|
||||||
|
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
|
Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list
|
||||||
|
|
||||||
@@ -490,9 +484,6 @@ Someone_else_s_Notes_7e5f = Someone else's Notes
|
|||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Someone else's Notifications
|
Someone_else_s_Notifications_82e6 = Someone else's Notifications
|
||||||
|
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = Sort replies newest first:
|
|
||||||
|
|
||||||
# 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
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source the last note for each user in your contact list
|
||||||
|
|
||||||
@@ -529,9 +520,6 @@ Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes
|
|||||||
# 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
|
Subscribe_to_someone_s_notes_b3c8 = Subscribe to someone's notes
|
||||||
|
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = Support email:
|
|
||||||
|
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Switch to dark mode
|
Switch_to_dark_mode_4dec = Switch to dark mode
|
||||||
|
|
||||||
@@ -553,6 +541,9 @@ Thread_0f20 = Thread
|
|||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
thread_ad1f = thread
|
thread_ad1f = thread
|
||||||
|
|
||||||
|
# Option in settings section to show the source client label at the top of the note
|
||||||
|
Top_6aeb = Top
|
||||||
|
|
||||||
# Title for universe column
|
# Title for universe column
|
||||||
Universe_e01e = Universe
|
Universe_e01e = Universe
|
||||||
|
|
||||||
@@ -569,7 +560,7 @@ username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at
|
|||||||
Username_daa7 = Username
|
Username_daa7 = Username
|
||||||
|
|
||||||
# Label for view folder button, Storage settings section
|
# Label for view folder button, Storage settings section
|
||||||
View_folder_9742 = View folder
|
View_folder_9742 = View folder:
|
||||||
|
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Wallet
|
Wallet_5e50 = Wallet
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ Banner_52ef = {"["}Bàññér{"]"}
|
|||||||
# Beta version label
|
# Beta version label
|
||||||
BETA_8e5d = {"["}BÉTÀ{"]"}
|
BETA_8e5d = {"["}BÉTÀ{"]"}
|
||||||
|
|
||||||
|
# Option in settings section to show the source client label at the bottom of the note
|
||||||
|
Bottom_33c8 = {"["}Bóttóm{"]"}
|
||||||
|
|
||||||
# Broadcast the note to all connected relays
|
# Broadcast the note to all connected relays
|
||||||
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
|
Broadcast_fe43 = {"["}Bróàdçàst{"]"}
|
||||||
|
|
||||||
@@ -238,12 +241,12 @@ 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 = {"["}Fíñd Úsér{"]"}
|
Find_User_bd12 = {"["}Fíñd Úsér{"]"}
|
||||||
|
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = {"["}Fóñt sízé:{"]"}
|
|
||||||
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
|
Hashtags_f8e0 = {"["}Hàshtàgs{"]"}
|
||||||
|
|
||||||
|
# Option in settings section to hide the source client label in note display
|
||||||
|
Hide_281d = {"["}Hídé{"]"}
|
||||||
|
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = {"["}Hómé{"]"}
|
Home_8c19 = {"["}Hómé{"]"}
|
||||||
|
|
||||||
@@ -349,12 +352,6 @@ Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"}
|
|||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = {"["}ñów{"]"}
|
now_2181 = {"["}ñów{"]"}
|
||||||
|
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
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{"]"}
|
||||||
|
|
||||||
@@ -433,9 +430,6 @@ Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"}
|
|||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = {"["}Répóstéd{"]"}
|
Reposted_61c8 = {"["}Répóstéd{"]"}
|
||||||
|
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = {"["}Rését{"]"}
|
|
||||||
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
# Label for reset zoom level, Appearance settings section
|
||||||
Reset_62d4 = {"["}Rését{"]"}
|
Reset_62d4 = {"["}Rését{"]"}
|
||||||
|
|
||||||
@@ -469,15 +463,15 @@ 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{"]"}
|
||||||
|
|
||||||
# Column title for app settings
|
# Column title for app settings
|
||||||
Settings_7a4f = {"["}Séttíñgs{"]"}
|
Settings_7a4f = {"["}Séttíñgs{"]"}
|
||||||
|
|
||||||
|
# Label for Show source client, others settings section
|
||||||
|
Show_source_client_9e31 = {"["}Shów sóúrçé çlíéñt{"]"}
|
||||||
|
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"}
|
Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"}
|
||||||
|
|
||||||
@@ -490,9 +484,6 @@ Someone_else_s_Notes_7e5f = {"["}Sóméóñé élsé's Ñótés{"]"}
|
|||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtíóñs{"]"}
|
Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtíóñs{"]"}
|
||||||
|
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = {"["}Sórt réplíés ñéwést fírst:{"]"}
|
|
||||||
|
|
||||||
# Description for contact list column
|
# Description for contact list column
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = {"["}Sóúrçé thé làst ñóté fór éàçh úsér íñ yóúr çóñtàçt líst{"]"}
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = {"["}Sóúrçé thé làst ñóté fór éàçh úsér íñ yóúr çóñtàçt líst{"]"}
|
||||||
|
|
||||||
@@ -529,9 +520,6 @@ Subscribe_to_someone_else_s_notes_d1e9 = {"["}Súbsçríbé tó sóméóñé él
|
|||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = {"["}Súbsçríbé tó sóméóñé's ñótés{"]"}
|
Subscribe_to_someone_s_notes_b3c8 = {"["}Súbsçríbé tó sóméóñé's ñótés{"]"}
|
||||||
|
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = {"["}Súppórt émàíl:{"]"}
|
|
||||||
|
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
|
Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"}
|
||||||
|
|
||||||
@@ -553,6 +541,9 @@ Thread_0f20 = {"["}Thréàd{"]"}
|
|||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
thread_ad1f = {"["}thréàd{"]"}
|
thread_ad1f = {"["}thréàd{"]"}
|
||||||
|
|
||||||
|
# Option in settings section to show the source client label at the top of the note
|
||||||
|
Top_6aeb = {"["}Tóp{"]"}
|
||||||
|
|
||||||
# Title for universe column
|
# Title for universe column
|
||||||
Universe_e01e = {"["}Úñívérsé{"]"}
|
Universe_e01e = {"["}Úñívérsé{"]"}
|
||||||
|
|
||||||
@@ -569,7 +560,7 @@ username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username
|
|||||||
Username_daa7 = {"["}Úsérñàmé{"]"}
|
Username_daa7 = {"["}Úsérñàmé{"]"}
|
||||||
|
|
||||||
# Label for view folder button, Storage settings section
|
# Label for view folder button, Storage settings section
|
||||||
View_folder_9742 = {"["}Víéw fóldér{"]"}
|
View_folder_9742 = {"["}Víéw fóldér:{"]"}
|
||||||
|
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = {"["}Wàllét{"]"}
|
Wallet_5e50 = {"["}Wàllét{"]"}
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ Algo_2452 = Algo
|
|||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = Cantidad
|
Amount_70f0 = Cantidad
|
||||||
# Label for appearance settings section
|
|
||||||
Appearance_4c7f = Aspecto
|
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = Preguntar
|
Ask_b7f4 = Preguntar
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -61,18 +59,10 @@ Broadcast_fe43 = Transmitir
|
|||||||
Broadcast_Local_7e50 = Transmitir localmente
|
Broadcast_Local_7e50 = Transmitir localmente
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = Cancelar
|
Cancel_ed3b = Cancelar
|
||||||
# Label for cancel clear cache, Storage settings section
|
|
||||||
Cancel_fd8b = Cancelar
|
|
||||||
# Label for clear cache button, Storage settings section
|
|
||||||
Clear_cache_dccb = Limpiar caché
|
|
||||||
# Hover text for editable zap amount
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = Haz clic para editar
|
Click_to_edit_0414 = Haz clic para editar
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = Redactar nota
|
Compose_Note_c094 = Redactar nota
|
||||||
# Label for configure relays, settings section
|
|
||||||
Configure_relays_d156 = Configurar relés
|
|
||||||
# Label for confirm clear cache, Storage settings section
|
|
||||||
Confirm_9d9d = Confirmar
|
|
||||||
# Button label to confirm an action
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = Confirmar
|
Confirm_f8a6 = Confirmar
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -121,8 +111,6 @@ Custom_a69e = Personalizado
|
|||||||
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = Ayuda de Damus
|
Damus_Support_27c0 = Ayuda de Damus
|
||||||
# Label for Theme Dark, Appearance settings section
|
|
||||||
Dark_85fe = Oscuro
|
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = Nombre del deck
|
Deck_name_cd32 = Nombre del deck
|
||||||
# Label for decks section in side panel
|
# Label for decks section in side panel
|
||||||
@@ -161,16 +149,12 @@ Enter_your_key_0fca = Ingresa tu clave
|
|||||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
|
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = Buscar usuario
|
Find_User_bd12 = Buscar usuario
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = Font size:
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Inicio
|
Home_8c19 = Inicio
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = Ícono
|
Icon_b0ab = Ícono
|
||||||
# Label for Image cache size, Storage settings section
|
|
||||||
Image_cache_size_3004 = Tamaño de caché de imágenes:
|
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = Individual
|
Individual_b776 = Individual
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
@@ -191,12 +175,8 @@ k_50K_c2dc = 50.000
|
|||||||
k_5K_f7e6 = 5.000
|
k_5K_f7e6 = 5.000
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
||||||
# Label for language, Appearance settings section
|
|
||||||
Language_e264 = Idioma:
|
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = Última nota por usuario
|
Last_Note_per_User_17ad = Última nota por usuario
|
||||||
# Label for Theme Light, Appearance settings section
|
|
||||||
Light_7475 = Claro
|
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -235,14 +215,10 @@ Notifications_d673 = Notificaciones
|
|||||||
Notifications_ef56 = Notificaciones
|
Notifications_ef56 = Notificaciones
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = ahora
|
now_2181 = ahora
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
On_f412 = On
|
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Abrir correo electrónico
|
Open_Email_25e9 = Abrir correo electrónico
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
|
||||||
# Label for others settings section
|
|
||||||
Others_7267 = Otros
|
|
||||||
# Placeholder text for NWC URI input
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
@@ -289,10 +265,6 @@ replying_to_a_note_e0bc = respondiendo a una nota
|
|||||||
Repost_this_note_8e56 = Volver a publicar esta nota
|
Repost_this_note_8e56 = Volver a publicar esta nota
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Publicadas de nuevo
|
Reposted_61c8 = Publicadas de nuevo
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = Reset
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
|
||||||
Reset_62d4 = Restablecer
|
|
||||||
# Heading for support section
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = ¿Encontraste un error?
|
Running_into_a_bug_1796 = ¿Encontraste un error?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||||
@@ -315,8 +287,6 @@ See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
|
|||||||
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = Enviar
|
Send_1ea4 = Enviar
|
||||||
# Column title for app settings
|
|
||||||
Settings_7a4f = Configuración
|
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
|
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -325,8 +295,6 @@ Sign_out_337b = Cerrar sesión
|
|||||||
Someone_else_s_Notes_7e5f = Notas de otra persona
|
Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = Sort replies newest first:
|
|
||||||
# Description for contact list column
|
# Description for contact list column
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -345,14 +313,10 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con
|
|||||||
Step_1_8656 = Paso 1
|
Step_1_8656 = Paso 1
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = Paso 2
|
Step_2_d08d = Paso 2
|
||||||
# Label for storage settings section
|
|
||||||
Storage_ed65 = Almacenamiento
|
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = Support email:
|
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -361,8 +325,6 @@ Switch_to_light_mode_72ce = Cambiar a modo claro
|
|||||||
Tap_to_Load_4b05 = Toca para cargar
|
Tap_to_Load_4b05 = Toca para cargar
|
||||||
# Message shown when Dave trial period has ended
|
# Message shown when Dave trial period has ended
|
||||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr finalizó :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr finalizó :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
|
||||||
# Label for theme, Appearance settings section
|
|
||||||
Theme_4aac = Tema:
|
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = Conversación
|
Thread_0f20 = Conversación
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
@@ -377,8 +339,6 @@ Use_this_wallet_for_the_current_account_only_61dc = Usar esta billetera solo par
|
|||||||
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Nombre de usuario
|
Username_daa7 = Nombre de usuario
|
||||||
# Label for view folder button, Storage settings section
|
|
||||||
View_folder_9742 = Ver carpeta
|
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Billetera
|
Wallet_5e50 = Billetera
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -397,8 +357,6 @@ Your_Notifications_080d = Tus notificaciones
|
|||||||
Zap_16b4 = Zap
|
Zap_16b4 = Zap
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Enviar un zap a esta nota
|
Zap_this_note_42b2 = Enviar un zap a esta nota
|
||||||
# Label for zoom level, Appearance settings section
|
|
||||||
Zoom_Level_29a8 = Nivel de zoom:
|
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ Algo_2452 = Algo
|
|||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Feeds algorítmicos para ayudar en el descubrimiento de notas
|
||||||
# Label for zap amount input field
|
# Label for zap amount input field
|
||||||
Amount_70f0 = Cantidad
|
Amount_70f0 = Cantidad
|
||||||
# Label for appearance settings section
|
|
||||||
Appearance_4c7f = Aspecto
|
|
||||||
# Button to send message to Dave AI assistant
|
# Button to send message to Dave AI assistant
|
||||||
Ask_b7f4 = Preguntar
|
Ask_b7f4 = Preguntar
|
||||||
# Placeholder text for Dave AI input field
|
# Placeholder text for Dave AI input field
|
||||||
@@ -61,18 +59,10 @@ Broadcast_fe43 = Transmitir
|
|||||||
Broadcast_Local_7e50 = Transmitir localmente
|
Broadcast_Local_7e50 = Transmitir localmente
|
||||||
# Button label to cancel an action
|
# Button label to cancel an action
|
||||||
Cancel_ed3b = Cancelar
|
Cancel_ed3b = Cancelar
|
||||||
# Label for cancel clear cache, Storage settings section
|
|
||||||
Cancel_fd8b = Cancelar
|
|
||||||
# Label for clear cache button, Storage settings section
|
|
||||||
Clear_cache_dccb = Limpiar caché
|
|
||||||
# Hover text for editable zap amount
|
# Hover text for editable zap amount
|
||||||
Click_to_edit_0414 = Haz clic para editar
|
Click_to_edit_0414 = Haz clic para editar
|
||||||
# Column title for note composition
|
# Column title for note composition
|
||||||
Compose_Note_c094 = Redactar nota
|
Compose_Note_c094 = Redactar nota
|
||||||
# Label for configure relays, settings section
|
|
||||||
Configure_relays_d156 = Configurar relés
|
|
||||||
# Label for confirm clear cache, Storage settings section
|
|
||||||
Confirm_9d9d = Confirmar
|
|
||||||
# Button label to confirm an action
|
# Button label to confirm an action
|
||||||
Confirm_f8a6 = Confirmar
|
Confirm_f8a6 = Confirmar
|
||||||
# Status label for connected relay
|
# Status label for connected relay
|
||||||
@@ -121,8 +111,6 @@ Custom_a69e = Personalizado
|
|||||||
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
Customize_Zap_Amount_cfc4 = Personalizar cantidad de zap
|
||||||
# Column title for support page
|
# Column title for support page
|
||||||
Damus_Support_27c0 = Ayuda de Damus
|
Damus_Support_27c0 = Ayuda de Damus
|
||||||
# Label for Theme Dark, Appearance settings section
|
|
||||||
Dark_85fe = Oscuro
|
|
||||||
# Label for deck name input field
|
# Label for deck name input field
|
||||||
Deck_name_cd32 = Nombre del deck
|
Deck_name_cd32 = Nombre del deck
|
||||||
# Label for decks section in side panel
|
# Label for decks section in side panel
|
||||||
@@ -161,16 +149,12 @@ Enter_your_key_0fca = Ingresa tu clave
|
|||||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
|
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Ingresa tu clave pública (npub), dirección de Nostr (por ejemplo, { $address }) o clave privada (nsec). Debes ingresar tu clave privada para poder publicar, responder, etc.
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = Buscar usuario
|
Find_User_bd12 = Buscar usuario
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = Font size:
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Inicio
|
Home_8c19 = Inicio
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
Icon_b0ab = Icono
|
Icon_b0ab = Icono
|
||||||
# Label for Image cache size, Storage settings section
|
|
||||||
Image_cache_size_3004 = Tamaño de caché de imágenes:
|
|
||||||
# Title for individual user column
|
# Title for individual user column
|
||||||
Individual_b776 = Individual
|
Individual_b776 = Individual
|
||||||
# Error message for invalid zap amount
|
# Error message for invalid zap amount
|
||||||
@@ -191,12 +175,8 @@ k_50K_c2dc = 50.000
|
|||||||
k_5K_f7e6 = 5.000
|
k_5K_f7e6 = 5.000
|
||||||
# Description for your notes column
|
# Description for your notes column
|
||||||
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
Keep_track_of_your_notes___replies_a334 = Haz seguimiento de tus notas y respuestas
|
||||||
# Label for language, Appearance settings section
|
|
||||||
Language_e264 = Idioma:
|
|
||||||
# Title for last note per user column
|
# Title for last note per user column
|
||||||
Last_Note_per_User_17ad = Última nota por usuario
|
Last_Note_per_User_17ad = Última nota por usuario
|
||||||
# Label for Theme Light, Appearance settings section
|
|
||||||
Light_7475 = Claro
|
|
||||||
# Bitcoin Lightning network address field label
|
# Bitcoin Lightning network address field label
|
||||||
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
Lightning_network_address__lud16_ea51 = Dirección de la red Lightning (lud16)
|
||||||
# Login page title
|
# Login page title
|
||||||
@@ -235,14 +215,10 @@ Notifications_d673 = Notificaciones
|
|||||||
Notifications_ef56 = Notificaciones
|
Notifications_ef56 = Notificaciones
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = ahora
|
now_2181 = ahora
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
On_f412 = On
|
|
||||||
# Button label to open email client
|
# Button label to open email client
|
||||||
Open_Email_25e9 = Abrir correo electrónico
|
Open_Email_25e9 = Abrir correo electrónico
|
||||||
# Instruction to open email client
|
# Instruction to open email client
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
|
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre tu cliente de correo predeterminado para recibir ayuda del equipo de Damus
|
||||||
# Label for others settings section
|
|
||||||
Others_7267 = Otros
|
|
||||||
# Placeholder text for NWC URI input
|
# Placeholder text for NWC URI input
|
||||||
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
Paste_your_NWC_URI_here_b471 = Pega tu NWC URI aquí...
|
||||||
# Error message for missing deck name
|
# Error message for missing deck name
|
||||||
@@ -289,10 +265,6 @@ replying_to_a_note_e0bc = respondiendo a una nota
|
|||||||
Repost_this_note_8e56 = Volver a publicar esta nota
|
Repost_this_note_8e56 = Volver a publicar esta nota
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Publicadas de nuevo
|
Reposted_61c8 = Publicadas de nuevo
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = Reset
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
|
||||||
Reset_62d4 = Restablecer
|
|
||||||
# Heading for support section
|
# Heading for support section
|
||||||
Running_into_a_bug_1796 = ¿Has encontrado un error?
|
Running_into_a_bug_1796 = ¿Has encontrado un error?
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
||||||
@@ -315,8 +287,6 @@ See_notes_from_your_contacts_ac16 = Ver notas de tus contactos
|
|||||||
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
See_the_whole_nostr_universe_7694 = Ver todo el universo de nostr
|
||||||
# Button label to send a zap
|
# Button label to send a zap
|
||||||
Send_1ea4 = Enviar
|
Send_1ea4 = Enviar
|
||||||
# Column title for app settings
|
|
||||||
Settings_7a4f = Configuración
|
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
|
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar la última nota para cada usuario de una lista
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -325,8 +295,6 @@ Sign_out_337b = Cerrar sesión
|
|||||||
Someone_else_s_Notes_7e5f = Notas de otra persona
|
Someone_else_s_Notes_7e5f = Notas de otra persona
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
Someone_else_s_Notifications_82e6 = Notificaciones de otra persona
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = Sort replies newest first:
|
|
||||||
# Description for contact list column
|
# Description for contact list column
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Busca la última nota de cada usuario en tu lista de contactos
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -345,14 +313,10 @@ Stay_up_to_date_with_your_notifications_and_mentions_e73e = Mantente al día con
|
|||||||
Step_1_8656 = Paso 1
|
Step_1_8656 = Paso 1
|
||||||
# Step 2 label in support instructions
|
# Step 2 label in support instructions
|
||||||
Step_2_d08d = Paso 2
|
Step_2_d08d = Paso 2
|
||||||
# Label for storage settings section
|
|
||||||
Storage_ed65 = Almacenamiento
|
|
||||||
# Column title for subscribing to external user
|
# Column title for subscribing to external user
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
Subscribe_to_someone_else_s_notes_d1e9 = Suscribirse a las notas de otra persona
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
Subscribe_to_someone_s_notes_b3c8 = Suscribirse a las notas de alguien
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = Support email:
|
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
Switch_to_dark_mode_4dec = Cambiar a modo oscuro
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -361,8 +325,6 @@ Switch_to_light_mode_72ce = Cambiar a modo claro
|
|||||||
Tap_to_Load_4b05 = Toca para cargar
|
Tap_to_Load_4b05 = Toca para cargar
|
||||||
# Message shown when Dave trial period has ended
|
# Message shown when Dave trial period has ended
|
||||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr ha finalizado :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
|
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = La prueba del asistente de IA Dave de Nostr ha finalizado :(. ¡Gracias por probarlo! ¡Dave con zaps estará disponible muy pronto!
|
||||||
# Label for theme, Appearance settings section
|
|
||||||
Theme_4aac = Tema:
|
|
||||||
# Column title for note thread view
|
# Column title for note thread view
|
||||||
Thread_0f20 = Conversación
|
Thread_0f20 = Conversación
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
@@ -377,8 +339,6 @@ Use_this_wallet_for_the_current_account_only_61dc = Usar este monedero solo para
|
|||||||
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
username___at___domain___will_be_used_for_identification_a4fd = Se utilizará "{ $username }" en "{ $domain }" para la identificación
|
||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Nombre de usuario
|
Username_daa7 = Nombre de usuario
|
||||||
# Label for view folder button, Storage settings section
|
|
||||||
View_folder_9742 = Ver carpeta
|
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Monedero
|
Wallet_5e50 = Monedero
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -397,8 +357,6 @@ Your_Notifications_080d = Tus notificaciones
|
|||||||
Zap_16b4 = Zap
|
Zap_16b4 = Zap
|
||||||
# Hover text for zap button
|
# Hover text for zap button
|
||||||
Zap_this_note_42b2 = Enviar un zap a esta nota
|
Zap_this_note_42b2 = Enviar un zap a esta nota
|
||||||
# Label for zoom level, Appearance settings section
|
|
||||||
Zoom_Level_29a8 = Nivel de zoom:
|
|
||||||
|
|
||||||
# Pluralized strings
|
# Pluralized strings
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ Ask_dave_anything_33d1 = Demandez à Dave n'importe quoi...
|
|||||||
Banner_52ef = Bannière
|
Banner_52ef = Bannière
|
||||||
# Beta version label
|
# Beta version label
|
||||||
BETA_8e5d = BETA
|
BETA_8e5d = BETA
|
||||||
|
# Option in settings section to show the source client label at the bottom of the note
|
||||||
|
Bottom_33c8 = En bas
|
||||||
# Broadcast the note to all connected relays
|
# Broadcast the note to all connected relays
|
||||||
Broadcast_fe43 = Diffusion
|
Broadcast_fe43 = Diffusion
|
||||||
# Broadcast the note only to local network relays
|
# Broadcast the note only to local network relays
|
||||||
@@ -161,10 +163,10 @@ Enter_your_key_0fca = Entrez votre clé
|
|||||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
|
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Entrez votre clé publique (npub), votre adresse nostr (par exemple { $address }), ou votre clé privée (nsec). Vous devez entrer votre clé privée pour pouvoir poster, répondre, etc.
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = Trouver un utilisateur
|
Find_User_bd12 = Trouver un utilisateur
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = Taille du texte :
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = Hashtags
|
Hashtags_f8e0 = Hashtags
|
||||||
|
# Option in settings section to hide the source client label in note display
|
||||||
|
Hide_281d = Masquer
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Accueil
|
Home_8c19 = Accueil
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
@@ -235,10 +237,6 @@ Notifications_d673 = Notifications
|
|||||||
Notifications_ef56 = Notifications
|
Notifications_ef56 = Notifications
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = maintenant
|
now_2181 = maintenant
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
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
|
||||||
@@ -291,8 +289,6 @@ replying_to_a_note_e0bc = répondre à une note
|
|||||||
Repost_this_note_8e56 = Republier cette note
|
Repost_this_note_8e56 = Republier cette note
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Republier
|
Reposted_61c8 = Republier
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = Réinitialiser
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
# Label for reset zoom level, Appearance settings section
|
||||||
Reset_62d4 = Réinitialiser
|
Reset_62d4 = Réinitialiser
|
||||||
# Heading for support section
|
# Heading for support section
|
||||||
@@ -315,12 +311,12 @@ 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
|
||||||
Settings_7a4f = Paramètres
|
Settings_7a4f = Paramètres
|
||||||
|
# Label for Show source client, others settings section
|
||||||
|
Show_source_client_9e31 = Afficher le client source
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
|
Show_the_last_note_for_each_user_from_a_list_50e7 = Afficher la dernière note de chaque utilisateur à partir d'une liste
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -329,8 +325,6 @@ Sign_out_337b = Se déconnecter
|
|||||||
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
|
Someone_else_s_Notes_7e5f = Notes de quelqu'un d'autre
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Notifications de quelqu'un d'autre
|
Someone_else_s_Notifications_82e6 = Notifications de quelqu'un d'autre
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = Trier les réponses les plus récentes en premier :
|
|
||||||
# Description for contact list column
|
# Description for contact list column
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source de la dernière note pour chaque utilisateur de votre liste de contacts
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source de la dernière note pour chaque utilisateur de votre liste de contacts
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -355,8 +349,6 @@ Storage_ed65 = Stockage
|
|||||||
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
|
Subscribe_to_someone_else_s_notes_d1e9 = S'abonner aux notes de quelqu'un d'autre
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
|
Subscribe_to_someone_s_notes_b3c8 = S'abonner aux notes de quelqu'un
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = Adresse email de l'assistance :
|
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Passer en mode sombre
|
Switch_to_dark_mode_4dec = Passer en mode sombre
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -371,6 +363,8 @@ Theme_4aac = Thème :
|
|||||||
Thread_0f20 = Fil
|
Thread_0f20 = Fil
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
thread_ad1f = fil
|
thread_ad1f = fil
|
||||||
|
# Option in settings section to show the source client label at the top of the note
|
||||||
|
Top_6aeb = En haut
|
||||||
# Title for universe column
|
# Title for universe column
|
||||||
Universe_e01e = Universel
|
Universe_e01e = Universel
|
||||||
# Column title for universe feed
|
# Column title for universe feed
|
||||||
@@ -382,7 +376,7 @@ username___at___domain___will_be_used_for_identification_a4fd = "{ $username }"
|
|||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Nom d'utilisateur
|
Username_daa7 = Nom d'utilisateur
|
||||||
# Label for view folder button, Storage settings section
|
# Label for view folder button, Storage settings section
|
||||||
View_folder_9742 = Voir le dossier
|
View_folder_9742 = Voir le dossier :
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Portefeuille
|
Wallet_5e50 = Portefeuille
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
|
|||||||
@@ -1,410 +0,0 @@
|
|||||||
# Main translation file for Notedeck
|
|
||||||
# This file contains common UI strings used throughout the application
|
|
||||||
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
|
|
||||||
|
|
||||||
|
|
||||||
# Regular strings
|
|
||||||
|
|
||||||
# Profile about/bio field label
|
|
||||||
About_00c0 = 概要
|
|
||||||
# Column title for account management
|
|
||||||
Accounts_f018 = アカウント
|
|
||||||
# Button label to add a relay
|
|
||||||
Add_269d = 追加
|
|
||||||
# Label for add column button
|
|
||||||
Add_47df = 追加
|
|
||||||
# Button label to add a different wallet
|
|
||||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = このアカウントでのみ使用される別のウォレットを追加
|
|
||||||
# Error message for missing wallet
|
|
||||||
Add_a_wallet_to_continue_d170 = 続行するにはウォレットを追加してください
|
|
||||||
# Button label to add a new account
|
|
||||||
Add_account_1cfc = アカウントを追加
|
|
||||||
# Column title for adding new account
|
|
||||||
Add_Account_d06c = アカウントの追加
|
|
||||||
# Column title for adding algorithm column
|
|
||||||
Add_Algo_Column_0d75 = アルゴカラムの追加
|
|
||||||
# Column title for adding new column
|
|
||||||
Add_Column_c764 = カラムの追加
|
|
||||||
# Column title for adding new deck
|
|
||||||
Add_Deck_fabf = デッキの追加
|
|
||||||
# Column title for adding external notifications column
|
|
||||||
Add_External_Notifications_Column_41ae = 外部通知カラムの追加
|
|
||||||
# Column title for adding hashtag column
|
|
||||||
Add_Hashtag_Column_ebf4 = ハッシュタグカラムの追加
|
|
||||||
# Column title for adding last notes column
|
|
||||||
Add_Last_Notes_Column_bbad = 最後の投稿カラムの追加
|
|
||||||
# Column title for adding notifications column
|
|
||||||
Add_Notifications_Column_79f8 = 外部通知カラムの追加
|
|
||||||
# Button label to add a relay
|
|
||||||
Add_relay_269d = リレーを追加
|
|
||||||
# Button label to add a wallet
|
|
||||||
Add_Wallet_d1be = ウォレットを追加
|
|
||||||
# Title for algorithmic feeds column
|
|
||||||
Algo_2452 = アルゴ
|
|
||||||
# Description for algorithmic feeds column
|
|
||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = 投稿の発見に役立つアルゴリズムフィードです
|
|
||||||
# Label for zap amount input field
|
|
||||||
Amount_70f0 = 金額
|
|
||||||
# Label for appearance settings section
|
|
||||||
Appearance_4c7f = 外観
|
|
||||||
# Button to send message to Dave AI assistant
|
|
||||||
Ask_b7f4 = 質問
|
|
||||||
# Placeholder text for Dave AI input field
|
|
||||||
Ask_dave_anything_33d1 = Dave に何でも質問してみましょう…
|
|
||||||
# Profile banner URL field label
|
|
||||||
Banner_52ef = バナー
|
|
||||||
# Beta version label
|
|
||||||
BETA_8e5d = ベータ
|
|
||||||
# Broadcast the note to all connected relays
|
|
||||||
Broadcast_fe43 = ブロードキャスト
|
|
||||||
# Broadcast the note only to local network relays
|
|
||||||
Broadcast_Local_7e50 = ローカルにブロードキャスト
|
|
||||||
# Button label to cancel an action
|
|
||||||
Cancel_ed3b = キャンセル
|
|
||||||
# Label for cancel clear cache, Storage settings section
|
|
||||||
Cancel_fd8b = キャンセル
|
|
||||||
# Label for clear cache button, Storage settings section
|
|
||||||
Clear_cache_dccb = キャッシュを消去
|
|
||||||
# Hover text for editable zap amount
|
|
||||||
Click_to_edit_0414 = クリックして編集
|
|
||||||
# Column title for note composition
|
|
||||||
Compose_Note_c094 = メモの作成
|
|
||||||
# Label for configure relays, settings section
|
|
||||||
Configure_relays_d156 = リレーを設定
|
|
||||||
# Label for confirm clear cache, Storage settings section
|
|
||||||
Confirm_9d9d = 決定
|
|
||||||
# Button label to confirm an action
|
|
||||||
Confirm_f8a6 = 決定
|
|
||||||
# Status label for connected relay
|
|
||||||
Connected_f8cc = 接続済
|
|
||||||
# Status label for connecting relay
|
|
||||||
Connecting_6b7e = 接続中…
|
|
||||||
# Title for contact list column
|
|
||||||
Contact_List_f85a = フォロイーリスト
|
|
||||||
# Column title for contact lists
|
|
||||||
Contacts_7533 = フォロー
|
|
||||||
# Column title for last notes per contact
|
|
||||||
Contacts__last_notes_3f84 = フォロー (最後の投稿)
|
|
||||||
# Button label to copy logs
|
|
||||||
Copy_a688 = コピー
|
|
||||||
# Button to copy media link to clipboard
|
|
||||||
Copy_Link_dc7c = リンクをコピー
|
|
||||||
# Copy the unique note identifier to clipboard
|
|
||||||
Copy_Note_ID_6b45 = 投稿 ID をコピー
|
|
||||||
# Copy the raw note data in JSON format to clipboard
|
|
||||||
Copy_Note_JSON_9e4e = 投稿の JSON をコピー
|
|
||||||
# Copy the author's public key to clipboard
|
|
||||||
Copy_Pubkey_9cc4 = 公開鍵をコピー
|
|
||||||
# Copy the text content of the note to clipboard
|
|
||||||
Copy_Text_f81c = テキストをコピー
|
|
||||||
# Relative time in days
|
|
||||||
count_d_b9be = { $count }日
|
|
||||||
# Relative time in hours
|
|
||||||
count_h_3ecb = { $count }時間
|
|
||||||
# Relative time in minutes
|
|
||||||
count_m_b41e = { $count }分
|
|
||||||
# Relative time in months
|
|
||||||
count_mo_7aba = { $count }ヶ月
|
|
||||||
# Relative time in seconds
|
|
||||||
count_s_aa26 = { $count }秒
|
|
||||||
# Relative time in weeks
|
|
||||||
count_w_7468 = { $count }週間
|
|
||||||
# Relative time in years
|
|
||||||
count_y_9408 = { $count }年
|
|
||||||
# Button to create a new account
|
|
||||||
Create_Account_6994 = アカウントを作成
|
|
||||||
# Button label to create a new deck
|
|
||||||
Create_Deck_16b7 = デッキを作成
|
|
||||||
# Column title for custom timelines
|
|
||||||
Custom_a69e = カスタマイズ
|
|
||||||
# Column title for zap amount customization
|
|
||||||
Customize_Zap_Amount_cfc4 = Zap 金額をカスタマイズ
|
|
||||||
# Column title for support page
|
|
||||||
Damus_Support_27c0 = Damus サポート
|
|
||||||
# Label for Theme Dark, Appearance settings section
|
|
||||||
Dark_85fe = ダーク
|
|
||||||
# Label for deck name input field
|
|
||||||
Deck_name_cd32 = デッキ名
|
|
||||||
# Label for decks section in side panel
|
|
||||||
DECKS_1fad = デッキ
|
|
||||||
# Label for default zap amount input
|
|
||||||
Default_amount_per_zap_399d = Zap ごとのデフォルトの金額:
|
|
||||||
# Name of the default deck feed
|
|
||||||
Default_Deck_fcca = 既定のデッキ
|
|
||||||
# Button label to delete a deck
|
|
||||||
Delete_Deck_bb29 = デッキを削除
|
|
||||||
# Tooltip for deleting a column
|
|
||||||
Delete_this_column_8d5a = このカラムを削除します
|
|
||||||
# Button label to delete a wallet
|
|
||||||
Delete_Wallet_d1d4 = ウォレットを削除
|
|
||||||
# Profile display name field label
|
|
||||||
Display_name_f9d9 = 表示名
|
|
||||||
# Domain identification message
|
|
||||||
domain___will_be_used_for_identification_b67e = "{ $domain }" が識別に使用されます
|
|
||||||
# Column title for editing deck
|
|
||||||
Edit_Deck_4018 = デッキの編集
|
|
||||||
# Button label to edit a deck
|
|
||||||
Edit_Deck_fd93 = デッキを編集
|
|
||||||
# Button label to edit user profile
|
|
||||||
Edit_Profile_49e6 = プロファイルを編集
|
|
||||||
# Column title for profile editing
|
|
||||||
Edit_Profile_8ad4 = プロファイルの編集
|
|
||||||
# Placeholder for hashtag input field
|
|
||||||
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = 必要なハッシュタグをここに入力してください (複数スペースで区切る場合)
|
|
||||||
# Placeholder for relay input field
|
|
||||||
Enter_the_relay_here_1c8b = ここにリレーを入力してください
|
|
||||||
# Hint text to prompt entering the user's public key.
|
|
||||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = ユーザーの鍵 (npub, hex, nip05) を入力してください...
|
|
||||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
|
||||||
Enter_your_key_0fca = 鍵を入力してください
|
|
||||||
# Instructions for entering Nostr credentials
|
|
||||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 公開鍵 (npub)、nostr アドレス (例: { $address })、秘密鍵 (nsec) を入力してください。 投稿、返信などを行うには秘密鍵を入力する必要があります。
|
|
||||||
# Label for find user button
|
|
||||||
Find_User_bd12 = ユーザーを探す
|
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = フォントサイズ:
|
|
||||||
# Title for hashtags column
|
|
||||||
Hashtags_f8e0 = ハッシュタグ
|
|
||||||
# Title for Home column
|
|
||||||
Home_8c19 = ホーム
|
|
||||||
# Label for deck icon selection
|
|
||||||
Icon_b0ab = アイコン
|
|
||||||
# Label for Image cache size, Storage settings section
|
|
||||||
Image_cache_size_3004 = 画像キャッシュのサイズ:
|
|
||||||
# Title for individual user column
|
|
||||||
Individual_b776 = 個人用
|
|
||||||
# Error message for invalid zap amount
|
|
||||||
Invalid_amount_6630 = 無効な金額です
|
|
||||||
# Error message for invalid key input
|
|
||||||
Invalid_key_4726 = 無効な鍵です。
|
|
||||||
# Error message for invalid Nostr Wallet Connect URI
|
|
||||||
Invalid_NWC_URI_031b = 無効な NWC URI です
|
|
||||||
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_100K_686c = 100K
|
|
||||||
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_10K_f7e6 = 10K
|
|
||||||
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_20K_4977 = 20K
|
|
||||||
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_50K_c2dc = 50K
|
|
||||||
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_5K_f7e6 = 5K
|
|
||||||
# Description for your notes column
|
|
||||||
Keep_track_of_your_notes___replies_a334 = 投稿と返信を記録します
|
|
||||||
# Label for language, Appearance settings section
|
|
||||||
Language_e264 = 言語:
|
|
||||||
# Title for last note per user column
|
|
||||||
Last_Note_per_User_17ad = ユーザーごとの最後の投稿
|
|
||||||
# Label for Theme Light, Appearance settings section
|
|
||||||
Light_7475 = ライト
|
|
||||||
# Bitcoin Lightning network address field label
|
|
||||||
Lightning_network_address__lud16_ea51 = ライトニングネットワークアドレス (lud16)
|
|
||||||
# Login page title
|
|
||||||
Login_9eef = ログイン
|
|
||||||
# Login button text
|
|
||||||
Login_now___let_s_do_this_5630 = 今すぐログイン — レッツゴー!
|
|
||||||
# Text shown on blurred media from unfollowed users
|
|
||||||
Media_from_someone_you_don_t_follow_5611 = フォローしていない人のメディアです
|
|
||||||
# Tooltip for moving a column
|
|
||||||
Moves_this_column_to_another_position_0d4b = このカラムを別の位置に移動します
|
|
||||||
# Title for the user's deck
|
|
||||||
My_Deck_4ac5 = あなたのデッキ
|
|
||||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
|
||||||
New_to_Nostr_a2fd = Nostr は初めてですか?
|
|
||||||
# NIP-05 identity field label
|
|
||||||
Nostr_address__NIP-05_identity_74a2 = Nostr アドレス (NIP-05)
|
|
||||||
# Default username when profile is not available
|
|
||||||
nostrich_df29 = ノス民
|
|
||||||
# Status label for disconnected relay
|
|
||||||
Not_Connected_6292 = 未接続
|
|
||||||
# Link text for note references
|
|
||||||
note_cad6 = 投稿
|
|
||||||
# Beta product warning message
|
|
||||||
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck はベータ製品です。問題が発生した場合はサポートに問い合わせてください。
|
|
||||||
# Filter label for notes only view
|
|
||||||
Notes_03fb = 投稿
|
|
||||||
# Label for notes-only filter
|
|
||||||
Notes_60d2 = 投稿
|
|
||||||
# Filter label for notes and replies view
|
|
||||||
Notes___Replies_1ec2 = 投稿 & 返信
|
|
||||||
# Label for notes and replies filter
|
|
||||||
Notes___Replies_6e3b = 投稿 & 返信
|
|
||||||
# Column title for notifications
|
|
||||||
Notifications_d673 = 通知
|
|
||||||
# Title for notifications column
|
|
||||||
Notifications_ef56 = 通知
|
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
|
||||||
now_2181 = たった今
|
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
On_f412 = 有効
|
|
||||||
# Button label to open email client
|
|
||||||
Open_Email_25e9 = メールを開く
|
|
||||||
# Instruction to open email client
|
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = デフォルトのメールクライアントを開いて、Damus チームのヘルプを表示しましょう。
|
|
||||||
# Label for others settings section
|
|
||||||
Others_7267 = その他
|
|
||||||
# Placeholder text for NWC URI input
|
|
||||||
Paste_your_NWC_URI_here_b471 = ここに NWC の URI を貼り付けてください...
|
|
||||||
# Error message for missing deck name
|
|
||||||
Please_create_a_name_for_the_deck_38e7 = デッキの名前を作成してください。
|
|
||||||
# Error message for missing deck name and icon
|
|
||||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = デッキの名前を作成してアイコンを選択してください。
|
|
||||||
# Error message for missing deck icon
|
|
||||||
Please_select_an_icon_655b = アイコンを選択してください。
|
|
||||||
# Button label to post a note
|
|
||||||
Post_now_8a49 = すぐに投稿
|
|
||||||
# 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 = 下のボタンを押して、最新のログをシステムのクリップボードにコピーします。その後、メールに貼り付けてください。
|
|
||||||
# Profile picture URL field label
|
|
||||||
Profile_picture_81ff = プロフィール写真
|
|
||||||
# Column title for quote composition
|
|
||||||
Quote_475c = 引用
|
|
||||||
# Error message when quote note cannot be found
|
|
||||||
Quote_of_unknown_note_e4f0 = 不明な投稿の引用です
|
|
||||||
# Label for read-only profile mode
|
|
||||||
Read_only_82ff = 読み取り専用
|
|
||||||
# Column title for relay management
|
|
||||||
Relays_9d89 = リレー
|
|
||||||
# Label for relay list section
|
|
||||||
Relays_ad5e = リレー
|
|
||||||
# Column title for reply composition
|
|
||||||
Reply_3bf1 = 返信
|
|
||||||
# Hover text for reply button
|
|
||||||
Reply_to_this_note_f5de = この投稿に返信
|
|
||||||
# Error message when reply note cannot be found
|
|
||||||
Reply_to_unknown_note_4401 = 不明な投稿に返信しています
|
|
||||||
# Fallback template for replying to user
|
|
||||||
replying_to__user_15ab = { $user } に返信
|
|
||||||
# Template for replying to user in unknown thread
|
|
||||||
replying_to__user__in_someone_s_thread_e148 = 誰かのスレッドで { $user } に返信
|
|
||||||
# Template for replying to note in different user's thread
|
|
||||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = { $user }の { $note } の { $thread_user }の { $thread } に返信
|
|
||||||
# Template for replying to user's note
|
|
||||||
replying_to__user__s__note_ccba = { $user }の { $note } に返信
|
|
||||||
# Template for replying to root thread
|
|
||||||
replying_to__user__s__thread_444d = { $user }の { $thread } に返信
|
|
||||||
# Fallback text when reply note is not found
|
|
||||||
replying_to_a_note_e0bc = 投稿に返信
|
|
||||||
# Hover text for repost button
|
|
||||||
Repost_this_note_8e56 = このメモを再投稿
|
|
||||||
# Label for reposted notes
|
|
||||||
Reposted_61c8 = 再投稿
|
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = リセット
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
|
||||||
Reset_62d4 = リセット
|
|
||||||
# Heading for support section
|
|
||||||
Running_into_a_bug_1796 = バグに遭遇しましたか?
|
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
|
||||||
SATS_45d7 = SATS
|
|
||||||
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
|
|
||||||
sats_e5ec = sats
|
|
||||||
# Button to save default zap amount
|
|
||||||
Save_6f7c = 保存
|
|
||||||
# Button label to save profile changes
|
|
||||||
Save_changes_00db = 変更を保存
|
|
||||||
# Column title for search page
|
|
||||||
Search_c573 = 検索
|
|
||||||
# Placeholder for search notes input field
|
|
||||||
Search_notes_42a6 = 投稿を検索しましょう...
|
|
||||||
# Search in progress message
|
|
||||||
Searching_for___query_5d18 = 「{ $query }」を検索中
|
|
||||||
# Description for Home column
|
|
||||||
See_notes_from_your_contacts_ac16 = フォローしている人の投稿を表示
|
|
||||||
# Description for universe column
|
|
||||||
See_the_whole_nostr_universe_7694 = 全ユニバースを表示します
|
|
||||||
# Button label to send a zap
|
|
||||||
Send_1ea4 = 送信
|
|
||||||
# Column title for app settings
|
|
||||||
Settings_7a4f = 設定
|
|
||||||
# Description for last note per user column
|
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = 一覧から各ユーザーの最後の投稿を表示する
|
|
||||||
# Button label to sign out of account
|
|
||||||
Sign_out_337b = サインアウト
|
|
||||||
# Title for someone else's notes column
|
|
||||||
Someone_else_s_Notes_7e5f = 他の人の投稿
|
|
||||||
# Title for someone else's notifications column
|
|
||||||
Someone_else_s_Notifications_82e6 = 他の人の通知
|
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = 最新の返信を最初に並べ替え:
|
|
||||||
# Description for contact list column
|
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = フォローリストにある各ユーザーの最後の投稿を取得します
|
|
||||||
# Description for hashtags column
|
|
||||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = 特定のハッシュタグで最新の情報を受け取ります
|
|
||||||
# Description for notifications column
|
|
||||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = 通知とメンションの最新の情報を受け取ります
|
|
||||||
# Description for someone else's notes column
|
|
||||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = 他のユーザーの投稿と返信の最新の情報を受け取ります
|
|
||||||
# Description for someone else's notifications column
|
|
||||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = 他のユーザーの投稿と返信の最新の情報を受け取ります
|
|
||||||
# Description for individual user column
|
|
||||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = 投稿と返信の最新の情報を受け取ります
|
|
||||||
# Description for your notifications column
|
|
||||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = あなたの通知とメンションの最新の情報を受け取ります
|
|
||||||
# Step 1 label in support instructions
|
|
||||||
Step_1_8656 = ステップ 1
|
|
||||||
# Step 2 label in support instructions
|
|
||||||
Step_2_d08d = ステップ 2
|
|
||||||
# Label for storage settings section
|
|
||||||
Storage_ed65 = ストレージ
|
|
||||||
# Column title for subscribing to external user
|
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = 他のユーザー投稿の購読
|
|
||||||
# Column title for subscribing to individual user
|
|
||||||
Subscribe_to_someone_s_notes_b3c8 = 投稿の購読
|
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = サポートメール:
|
|
||||||
# Hover text for dark mode toggle button
|
|
||||||
Switch_to_dark_mode_4dec = ダークモードに切り替える
|
|
||||||
# Hover text for light mode toggle button
|
|
||||||
Switch_to_light_mode_72ce = ライトモードに切り替える
|
|
||||||
# Button text to load blurred media
|
|
||||||
Tap_to_Load_4b05 = タップして読み込む
|
|
||||||
# Message shown when Dave trial period has ended
|
|
||||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = Dave Nostr AI アシスタントトライアルが終了しました: (テストしていただきありがとうございます! Zap 対応デイブは近日公開予定です!
|
|
||||||
# Label for theme, Appearance settings section
|
|
||||||
Theme_4aac = テーマ:
|
|
||||||
# Column title for note thread view
|
|
||||||
Thread_0f20 = スレッド
|
|
||||||
# Link text for thread references
|
|
||||||
thread_ad1f = スレッド
|
|
||||||
# Title for universe column
|
|
||||||
Universe_e01e = ユニバース
|
|
||||||
# Column title for universe feed
|
|
||||||
Universe_ffaa = ユニバース
|
|
||||||
# Checkbox label for using wallet only for current account
|
|
||||||
Use_this_wallet_for_the_current_account_only_61dc = このウォレットを現在のアカウントにのみ使用する
|
|
||||||
# Username and domain identification message
|
|
||||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $domain }" の "{ $username }" が識別に使用されます
|
|
||||||
# Profile username field label
|
|
||||||
Username_daa7 = ユーザー名
|
|
||||||
# Label for view folder button, Storage settings section
|
|
||||||
View_folder_9742 = フォルダを表示
|
|
||||||
# Column title for wallet management
|
|
||||||
Wallet_5e50 = ウォレット
|
|
||||||
# Hint for deck name input field
|
|
||||||
We_recommend_short_names_083e = 短い名前を推奨しています
|
|
||||||
# Profile website field label
|
|
||||||
Website_7980 = Web サイト
|
|
||||||
# Placeholder for note input field
|
|
||||||
Write_a_banger_note_here_bad2 = アツい一言をどうぞ...
|
|
||||||
# Placeholder text for key input field
|
|
||||||
Your_key_here_81bd = ここに鍵を入力...
|
|
||||||
# Title for your notes column
|
|
||||||
Your_Notes_f6db = 投稿
|
|
||||||
# Title for your notifications column
|
|
||||||
Your_Notifications_080d = 通知
|
|
||||||
# Heading for zap (tip) action
|
|
||||||
Zap_16b4 = Zap
|
|
||||||
# Hover text for zap button
|
|
||||||
Zap_this_note_42b2 = この投稿に Zap
|
|
||||||
# Label for zoom level, Appearance settings section
|
|
||||||
Zoom_Level_29a8 = 拡大率:
|
|
||||||
|
|
||||||
# Pluralized strings
|
|
||||||
|
|
||||||
# Search results count
|
|
||||||
Got__count__results_for___query_85fb =
|
|
||||||
{ $count ->
|
|
||||||
[one] { $query } の結果を '{ $count }' 件取得しました
|
|
||||||
*[other] ' { $query } の結果を '{ $count }' 件取得しました
|
|
||||||
}
|
|
||||||
@@ -55,6 +55,8 @@ Ask_dave_anything_33d1 = Perguntar ao Dave
|
|||||||
Banner_52ef = Destaque
|
Banner_52ef = Destaque
|
||||||
# Beta version label
|
# Beta version label
|
||||||
BETA_8e5d = Beta
|
BETA_8e5d = Beta
|
||||||
|
# Option in settings section to show the source client label at the bottom of the note
|
||||||
|
Bottom_33c8 = Abaixo
|
||||||
# Broadcast the note to all connected relays
|
# Broadcast the note to all connected relays
|
||||||
Broadcast_fe43 = Encaminhar
|
Broadcast_fe43 = Encaminhar
|
||||||
# Broadcast the note only to local network relays
|
# Broadcast the note only to local network relays
|
||||||
@@ -161,10 +163,10 @@ Enter_your_key_0fca = Sua chave aqui
|
|||||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insira sua chave pública (npub), endereço do Nostr (e.g. { $address }), ou chave privada (nsec). Você deve digitar sua chave privada para conseguir publicar, responder, etc.
|
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insira sua chave pública (npub), endereço do Nostr (e.g. { $address }), ou chave privada (nsec). Você deve digitar sua chave privada para conseguir publicar, responder, etc.
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = Pesquisar usuário
|
Find_User_bd12 = Pesquisar usuário
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = Tamanho da letra
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = #
|
Hashtags_f8e0 = #
|
||||||
|
# Option in settings section to hide the source client label in note display
|
||||||
|
Hide_281d = Ocultar
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = Início
|
Home_8c19 = Início
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
@@ -235,10 +237,6 @@ Notifications_d673 = Notificações
|
|||||||
Notifications_ef56 = Notificações
|
Notifications_ef56 = Notificações
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
# Relative time for very recent events (less than 3 seconds)
|
||||||
now_2181 = Agora
|
now_2181 = Agora
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
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
|
||||||
@@ -291,8 +289,6 @@ replying_to_a_note_e0bc = Respondendo nota
|
|||||||
Repost_this_note_8e56 = Republicar nota
|
Repost_this_note_8e56 = Republicar nota
|
||||||
# Label for reposted notes
|
# Label for reposted notes
|
||||||
Reposted_61c8 = Publicada
|
Reposted_61c8 = Publicada
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = Redefinir
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
# Label for reset zoom level, Appearance settings section
|
||||||
Reset_62d4 = Resetar
|
Reset_62d4 = Resetar
|
||||||
# Heading for support section
|
# Heading for support section
|
||||||
@@ -315,12 +311,12 @@ 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
|
||||||
Settings_7a4f = Configurações
|
Settings_7a4f = Configurações
|
||||||
|
# Label for Show source client, others settings section
|
||||||
|
Show_source_client_9e31 = Mostrar cliente de origem
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
|
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada usuário de uma lista
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -329,8 +325,6 @@ Sign_out_337b = Sair
|
|||||||
Someone_else_s_Notes_7e5f = Notas de outra pessoa
|
Someone_else_s_Notes_7e5f = Notas de outra pessoa
|
||||||
# Title for someone else's notifications column
|
# Title for someone else's notifications column
|
||||||
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
|
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes primeiro:
|
|
||||||
# Description for contact list column
|
# Description for contact list column
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Fonte da última nota para cada usuário em sua lista de contatos
|
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Fonte da última nota para cada usuário em sua lista de contatos
|
||||||
# Description for hashtags column
|
# Description for hashtags column
|
||||||
@@ -355,8 +349,6 @@ Storage_ed65 = Armazenamento
|
|||||||
Subscribe_to_someone_else_s_notes_d1e9 = Inscrever-se em notas de outra pessoa
|
Subscribe_to_someone_else_s_notes_d1e9 = Inscrever-se em notas de outra pessoa
|
||||||
# Column title for subscribing to individual user
|
# Column title for subscribing to individual user
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Inscrever-se nas notas de alguém
|
Subscribe_to_someone_s_notes_b3c8 = Inscrever-se nas notas de alguém
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = E-mail de suporte
|
|
||||||
# Hover text for dark mode toggle button
|
# Hover text for dark mode toggle button
|
||||||
Switch_to_dark_mode_4dec = Mudar para modo escuro
|
Switch_to_dark_mode_4dec = Mudar para modo escuro
|
||||||
# Hover text for light mode toggle button
|
# Hover text for light mode toggle button
|
||||||
@@ -371,6 +363,8 @@ Theme_4aac = Tema:
|
|||||||
Thread_0f20 = Fio
|
Thread_0f20 = Fio
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
thread_ad1f = Fio
|
thread_ad1f = Fio
|
||||||
|
# Option in settings section to show the source client label at the top of the note
|
||||||
|
Top_6aeb = Topo
|
||||||
# Title for universe column
|
# Title for universe column
|
||||||
Universe_e01e = Universo
|
Universe_e01e = Universo
|
||||||
# Column title for universe feed
|
# Column title for universe feed
|
||||||
@@ -382,7 +376,7 @@ username___at___domain___will_be_used_for_identification_a4fd = d = "{ $username
|
|||||||
# Profile username field label
|
# Profile username field label
|
||||||
Username_daa7 = Usuário
|
Username_daa7 = Usuário
|
||||||
# Label for view folder button, Storage settings section
|
# Label for view folder button, Storage settings section
|
||||||
View_folder_9742 = Visualizar pasta
|
View_folder_9742 = Visualizar pasta:
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = Carteira
|
Wallet_5e50 = Carteira
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
# Main translation file for Notedeck
|
|
||||||
# This file contains common UI strings used throughout the application
|
|
||||||
# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY
|
|
||||||
|
|
||||||
|
|
||||||
# Regular strings
|
|
||||||
|
|
||||||
# Profile about/bio field label
|
|
||||||
About_00c0 = Sobre
|
|
||||||
# Column title for account management
|
|
||||||
Accounts_f018 = Contas
|
|
||||||
# Button label to add a relay
|
|
||||||
Add_269d = Adicionar
|
|
||||||
# Label for add column button
|
|
||||||
Add_47df = Adicionar
|
|
||||||
# Button label to add a different wallet
|
|
||||||
Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Adicionar uma carteira diferente que será usada apenas para esta conta
|
|
||||||
# Error message for missing wallet
|
|
||||||
Add_a_wallet_to_continue_d170 = Adicionar uma carteira para continuar
|
|
||||||
# Button label to add a new account
|
|
||||||
Add_account_1cfc = Adicionar conta
|
|
||||||
# Column title for adding new account
|
|
||||||
Add_Account_d06c = Adicionar conta
|
|
||||||
# Column title for adding algorithm column
|
|
||||||
Add_Algo_Column_0d75 = Adicionar coluna de algoritmo
|
|
||||||
# Column title for adding new column
|
|
||||||
Add_Column_c764 = Adicionar coluna
|
|
||||||
# Column title for adding new deck
|
|
||||||
Add_Deck_fabf = Adicionar aba
|
|
||||||
# Column title for adding external notifications column
|
|
||||||
Add_External_Notifications_Column_41ae = Adicionar coluna de notificações externas
|
|
||||||
# Column title for adding hashtag column
|
|
||||||
Add_Hashtag_Column_ebf4 = Adicionar coluna de marcadores
|
|
||||||
# Column title for adding last notes column
|
|
||||||
Add_Last_Notes_Column_bbad = Adicionar coluna de últimas notas
|
|
||||||
# Column title for adding notifications column
|
|
||||||
Add_Notifications_Column_79f8 = Adicionar coluna de notificações
|
|
||||||
# Button label to add a relay
|
|
||||||
Add_relay_269d = Adicionar relay
|
|
||||||
# Button label to add a wallet
|
|
||||||
Add_Wallet_d1be = Adicionar carteira
|
|
||||||
# Title for algorithmic feeds column
|
|
||||||
Algo_2452 = Algoritmo
|
|
||||||
# Description for algorithmic feeds column
|
|
||||||
Algorithmic_feeds_to_aid_in_note_discovery_d344 = Fontes de algoritmo para ajudar na descoberta de notas
|
|
||||||
# Label for zap amount input field
|
|
||||||
Amount_70f0 = Quantia
|
|
||||||
# Label for appearance settings section
|
|
||||||
Appearance_4c7f = Aparência
|
|
||||||
# Button to send message to Dave AI assistant
|
|
||||||
Ask_b7f4 = Perguntar
|
|
||||||
# Placeholder text for Dave AI input field
|
|
||||||
Ask_dave_anything_33d1 = Perguntar qualquer coisa...
|
|
||||||
# Profile banner URL field label
|
|
||||||
Banner_52ef = Faixa
|
|
||||||
# Beta version label
|
|
||||||
BETA_8e5d = BETA
|
|
||||||
# Broadcast the note to all connected relays
|
|
||||||
Broadcast_fe43 = Transmissão
|
|
||||||
# Broadcast the note only to local network relays
|
|
||||||
Broadcast_Local_7e50 = Transmissão local
|
|
||||||
# Button label to cancel an action
|
|
||||||
Cancel_ed3b = Cancelar
|
|
||||||
# Label for cancel clear cache, Storage settings section
|
|
||||||
Cancel_fd8b = Cancelar
|
|
||||||
# Label for clear cache button, Storage settings section
|
|
||||||
Clear_cache_dccb = Limpar cache
|
|
||||||
# Hover text for editable zap amount
|
|
||||||
Click_to_edit_0414 = Clica para editar
|
|
||||||
# Column title for note composition
|
|
||||||
Compose_Note_c094 = Compor nota
|
|
||||||
# Label for configure relays, settings section
|
|
||||||
Configure_relays_d156 = Configurar relays
|
|
||||||
# Label for confirm clear cache, Storage settings section
|
|
||||||
Confirm_9d9d = Confirmar
|
|
||||||
# Button label to confirm an action
|
|
||||||
Confirm_f8a6 = Confirmar
|
|
||||||
# Status label for connected relay
|
|
||||||
Connected_f8cc = Conectado
|
|
||||||
# Status label for connecting relay
|
|
||||||
Connecting_6b7e = A conectar...
|
|
||||||
# Title for contact list column
|
|
||||||
Contact_List_f85a = Lista de contactos
|
|
||||||
# Column title for contact lists
|
|
||||||
Contacts_7533 = Contactos
|
|
||||||
# Column title for last notes per contact
|
|
||||||
Contacts__last_notes_3f84 = Contactos (últimas notas)
|
|
||||||
# Button label to copy logs
|
|
||||||
Copy_a688 = Copiar
|
|
||||||
# Button to copy media link to clipboard
|
|
||||||
Copy_Link_dc7c = Copiar link
|
|
||||||
# Copy the unique note identifier to clipboard
|
|
||||||
Copy_Note_ID_6b45 = Copiar ID da nota
|
|
||||||
# Copy the raw note data in JSON format to clipboard
|
|
||||||
Copy_Note_JSON_9e4e = Copiar JSON da nota
|
|
||||||
# Copy the author's public key to clipboard
|
|
||||||
Copy_Pubkey_9cc4 = Copiar chave pública
|
|
||||||
# Copy the text content of the note to clipboard
|
|
||||||
Copy_Text_f81c = Copiar texto
|
|
||||||
# Relative time in days
|
|
||||||
count_d_b9be = { $count }d
|
|
||||||
# Relative time in hours
|
|
||||||
count_h_3ecb = { $count }h
|
|
||||||
# Relative time in minutes
|
|
||||||
count_m_b41e = { $count }m
|
|
||||||
# Relative time in months
|
|
||||||
count_mo_7aba = { $count } mês(es)
|
|
||||||
# Relative time in seconds
|
|
||||||
count_s_aa26 = { $count } s
|
|
||||||
# Relative time in weeks
|
|
||||||
count_w_7468 = { $count } semana(s)
|
|
||||||
# Relative time in years
|
|
||||||
count_y_9408 = { $count } ano(s)
|
|
||||||
# Button to create a new account
|
|
||||||
Create_Account_6994 = Criar conta
|
|
||||||
# Button label to create a new deck
|
|
||||||
Create_Deck_16b7 = Criar aba
|
|
||||||
# Column title for custom timelines
|
|
||||||
Custom_a69e = Personalizadas
|
|
||||||
# Column title for zap amount customization
|
|
||||||
Customize_Zap_Amount_cfc4 = Personalizar valor do zap
|
|
||||||
# Column title for support page
|
|
||||||
Damus_Support_27c0 = Suporte Damus
|
|
||||||
# Label for Theme Dark, Appearance settings section
|
|
||||||
Dark_85fe = Modo escuro
|
|
||||||
# Label for deck name input field
|
|
||||||
Deck_name_cd32 = Nome da aba
|
|
||||||
# Label for decks section in side panel
|
|
||||||
DECKS_1fad = ABAS
|
|
||||||
# Label for default zap amount input
|
|
||||||
Default_amount_per_zap_399d = Valor padrão por zap:
|
|
||||||
# Name of the default deck feed
|
|
||||||
Default_Deck_fcca = Aba padrão
|
|
||||||
# Button label to delete a deck
|
|
||||||
Delete_Deck_bb29 = Excluir aba
|
|
||||||
# Tooltip for deleting a column
|
|
||||||
Delete_this_column_8d5a = Apagar esta coluna
|
|
||||||
# Button label to delete a wallet
|
|
||||||
Delete_Wallet_d1d4 = Eliminar carteira
|
|
||||||
# Profile display name field label
|
|
||||||
Display_name_f9d9 = Nome a mostrar
|
|
||||||
# Domain identification message
|
|
||||||
domain___will_be_used_for_identification_b67e = "{ $domain }" será usado para identificação
|
|
||||||
# Column title for editing deck
|
|
||||||
Edit_Deck_4018 = Editar aba
|
|
||||||
# Button label to edit a deck
|
|
||||||
Edit_Deck_fd93 = Editar aba
|
|
||||||
# Button label to edit user profile
|
|
||||||
Edit_Profile_49e6 = Editar perfil
|
|
||||||
# Column title for profile editing
|
|
||||||
Edit_Profile_8ad4 = Editar perfil
|
|
||||||
# Placeholder for hashtag input field
|
|
||||||
Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Insere aqui os marcadores desejados (para múltiplos com espaços separados)
|
|
||||||
# Placeholder for relay input field
|
|
||||||
Enter_the_relay_here_1c8b = Insere aqui o relay
|
|
||||||
# Hint text to prompt entering the user's public key.
|
|
||||||
Enter_the_user_s_key__npub__hex__nip05__here_650c = Insere aqui a chave de utilizador (npub, hex, nip05)
|
|
||||||
# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05).
|
|
||||||
Enter_your_key_0fca = Insere a tua chave
|
|
||||||
# Instructions for entering Nostr credentials
|
|
||||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = Insere a tua chave públca (npub), endereço nostr (por exemplo { $address }), ou chave privada (nsec). Tens de inserir a tua chave pública para publicar, responder, etc.
|
|
||||||
# Label for find user button
|
|
||||||
Find_User_bd12 = Encontrar utilizador
|
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = Tamanho da letra:
|
|
||||||
# Title for hashtags column
|
|
||||||
Hashtags_f8e0 = Marcadores
|
|
||||||
# Title for Home column
|
|
||||||
Home_8c19 = Início
|
|
||||||
# Label for deck icon selection
|
|
||||||
Icon_b0ab = Ícone
|
|
||||||
# Label for Image cache size, Storage settings section
|
|
||||||
Image_cache_size_3004 = Tamanho do cache da imagem:
|
|
||||||
# Title for individual user column
|
|
||||||
Individual_b776 = Individual
|
|
||||||
# Error message for invalid zap amount
|
|
||||||
Invalid_amount_6630 = Quantia inválida
|
|
||||||
# Error message for invalid key input
|
|
||||||
Invalid_key_4726 = Chave inválida.
|
|
||||||
# Error message for invalid Nostr Wallet Connect URI
|
|
||||||
Invalid_NWC_URI_031b = NWC URI inválido.
|
|
||||||
# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_100K_686c = 100K
|
|
||||||
# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_10K_f7e6 = 10K
|
|
||||||
# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_20K_4977 = 20K
|
|
||||||
# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_50K_c2dc = 50K
|
|
||||||
# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.
|
|
||||||
k_5K_f7e6 = 5K
|
|
||||||
# Description for your notes column
|
|
||||||
Keep_track_of_your_notes___replies_a334 = Acompanha as tuas notas e respostas
|
|
||||||
# Label for language, Appearance settings section
|
|
||||||
Language_e264 = Idioma:
|
|
||||||
# Title for last note per user column
|
|
||||||
Last_Note_per_User_17ad = Última nota por utilizador
|
|
||||||
# Label for Theme Light, Appearance settings section
|
|
||||||
Light_7475 = Modo claro
|
|
||||||
# Bitcoin Lightning network address field label
|
|
||||||
Lightning_network_address__lud16_ea51 = Endereço da rede Lightning (lud16)
|
|
||||||
# Login page title
|
|
||||||
Login_9eef = Iniciar sessão
|
|
||||||
# Login button text
|
|
||||||
Login_now___let_s_do_this_5630 = Entra agora — vamos fazer isto!
|
|
||||||
# Text shown on blurred media from unfollowed users
|
|
||||||
Media_from_someone_you_don_t_follow_5611 = Conteúdo de alguém que não segues
|
|
||||||
# Tooltip for moving a column
|
|
||||||
Moves_this_column_to_another_position_0d4b = Mover esta coluna para outra posição
|
|
||||||
# Title for the user's deck
|
|
||||||
My_Deck_4ac5 = Minha aba
|
|
||||||
# Label asking if the user is new to Nostr. Underneath this label is a button to create an account.
|
|
||||||
New_to_Nostr_a2fd = Nov@ no Nostr?
|
|
||||||
# NIP-05 identity field label
|
|
||||||
Nostr_address__NIP-05_identity_74a2 = Endereço Nostr (identificação NIP-05)
|
|
||||||
# Default username when profile is not available
|
|
||||||
nostrich_df29 = nostrich
|
|
||||||
# Status label for disconnected relay
|
|
||||||
Not_Connected_6292 = Não conectado
|
|
||||||
# Link text for note references
|
|
||||||
note_cad6 = nota
|
|
||||||
# Beta product warning message
|
|
||||||
Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck é um produto beta. Espere bugs e contacte-nos quando tiver problemas.
|
|
||||||
# Filter label for notes only view
|
|
||||||
Notes_03fb = Notas
|
|
||||||
# Label for notes-only filter
|
|
||||||
Notes_60d2 = Notas
|
|
||||||
# Filter label for notes and replies view
|
|
||||||
Notes___Replies_1ec2 = Notas e respostas
|
|
||||||
# Label for notes and replies filter
|
|
||||||
Notes___Replies_6e3b = Notas e respostas
|
|
||||||
# Column title for notifications
|
|
||||||
Notifications_d673 = Notificações
|
|
||||||
# Title for notifications column
|
|
||||||
Notifications_ef56 = Notificações
|
|
||||||
# Relative time for very recent events (less than 3 seconds)
|
|
||||||
now_2181 = agora
|
|
||||||
# Setting to turn on sorting replies so that the newest are shown first
|
|
||||||
On_f412 = Ativado
|
|
||||||
# Column title for finding users to follow
|
|
||||||
Onboarding_4a25 = Introdução
|
|
||||||
# Button label to open email client
|
|
||||||
Open_Email_25e9 = Abrir e-mail
|
|
||||||
# Instruction to open email client
|
|
||||||
Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Abre o teu cliente de e-mail padrão para obteres ajuda da equipa Damus
|
|
||||||
# Label for others settings section
|
|
||||||
Others_7267 = Outros
|
|
||||||
# Placeholder text for NWC URI input
|
|
||||||
Paste_your_NWC_URI_here_b471 = Cola o teu NWC URI aqui...
|
|
||||||
# Error message for missing deck name
|
|
||||||
Please_create_a_name_for_the_deck_38e7 = Cria um nome para a aba.
|
|
||||||
# Error message for missing deck name and icon
|
|
||||||
Please_create_a_name_for_the_deck_and_select_an_icon_0add = Cria um nome para a aba e seleciona um ícone.
|
|
||||||
# Error message for missing deck icon
|
|
||||||
Please_select_an_icon_655b = Seleciona um ícone.
|
|
||||||
# Button label to post a note
|
|
||||||
Post_now_8a49 = Publicar agora
|
|
||||||
# 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 = Prime o botão abaixo para copiar os teus registos mais recentes para a área de transferência do teu sistema. Depois cola-os no teu e-mail.
|
|
||||||
# Profile picture URL field label
|
|
||||||
Profile_picture_81ff = Foto de perfil
|
|
||||||
# Column title for quote composition
|
|
||||||
Quote_475c = Citação
|
|
||||||
# Error message when quote note cannot be found
|
|
||||||
Quote_of_unknown_note_e4f0 = Citação de nota desconhecida
|
|
||||||
# Label for read-only profile mode
|
|
||||||
Read_only_82ff = Somente leitura
|
|
||||||
# Column title for relay management
|
|
||||||
Relays_9d89 = Relays
|
|
||||||
# Label for relay list section
|
|
||||||
Relays_ad5e = Relays
|
|
||||||
# Column title for reply composition
|
|
||||||
Reply_3bf1 = Responder
|
|
||||||
# Hover text for reply button
|
|
||||||
Reply_to_this_note_f5de = Responder a esta nota
|
|
||||||
# Error message when reply note cannot be found
|
|
||||||
Reply_to_unknown_note_4401 = Responder a nota desconhecida
|
|
||||||
# Fallback template for replying to user
|
|
||||||
replying_to__user_15ab = responder a { $user }
|
|
||||||
# Template for replying to user in unknown thread
|
|
||||||
replying_to__user__in_someone_s_thread_e148 = responder a { $user } no tópico de alguém
|
|
||||||
# Template for replying to note in different user's thread
|
|
||||||
replying_to__user__s__note__in__thread_user__s__thread_daa8 = respondendo à { $note } de { $user } no { $thread } de { $thread_user }
|
|
||||||
# Template for replying to user's note
|
|
||||||
replying_to__user__s__note_ccba = respondendo à { $note } de { $user }
|
|
||||||
# Template for replying to root thread
|
|
||||||
replying_to__user__s__thread_444d = respondendo ao { $thread } de { $user }
|
|
||||||
# Fallback text when reply note is not found
|
|
||||||
replying_to_a_note_e0bc = respondendo a uma nota
|
|
||||||
# Hover text for repost button
|
|
||||||
Repost_this_note_8e56 = Republicar esta nota
|
|
||||||
# Label for reposted notes
|
|
||||||
Reposted_61c8 = Republicado
|
|
||||||
# Label for reset note body font size, Appearance settings section
|
|
||||||
Reset_4e60 = Redefinir
|
|
||||||
# Label for reset zoom level, Appearance settings section
|
|
||||||
Reset_62d4 = Redefinir
|
|
||||||
# Heading for support section
|
|
||||||
Running_into_a_bug_1796 = Encontraste um bug?
|
|
||||||
# Label for satoshis (Bitcoin unit) for custom zap amount input field
|
|
||||||
SATS_45d7 = SATS
|
|
||||||
# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings.
|
|
||||||
sats_e5ec = sats
|
|
||||||
# Button to save default zap amount
|
|
||||||
Save_6f7c = Guardar
|
|
||||||
# Button label to save profile changes
|
|
||||||
Save_changes_00db = Guardar alterações
|
|
||||||
# Column title for search page
|
|
||||||
Search_c573 = Procurar
|
|
||||||
# Placeholder for search notes input field
|
|
||||||
Search_notes_42a6 = Procurar notas...
|
|
||||||
# Search in progress message
|
|
||||||
Searching_for___query_5d18 = Procurando por '{ $query }'
|
|
||||||
# Description for Home column
|
|
||||||
See_notes_from_your_contacts_ac16 = Ver notas dos meus contactos
|
|
||||||
# Description for universe column
|
|
||||||
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
|
|
||||||
Send_1ea4 = Enviar
|
|
||||||
# Column title for app settings
|
|
||||||
Settings_7a4f = Configurações
|
|
||||||
# Description for last note per user column
|
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = Mostrar a última nota para cada utilizador a partir de uma lista
|
|
||||||
# Button label to sign out of account
|
|
||||||
Sign_out_337b = Terminar sessão
|
|
||||||
# Title for someone else's notes column
|
|
||||||
Someone_else_s_Notes_7e5f = Notas de outra pessoa
|
|
||||||
# Title for someone else's notifications column
|
|
||||||
Someone_else_s_Notifications_82e6 = Notificações de outra pessoa
|
|
||||||
# Label for Sort replies newest first, others settings section
|
|
||||||
Sort_replies_newest_first_b6c3 = Ordenar respostas mais recentes antes:
|
|
||||||
# Description for contact list column
|
|
||||||
Source_the_last_note_for_each_user_in_your_contact_list_e157 = Origem da última nota para cada utilizador na minha lista
|
|
||||||
# Description for hashtags column
|
|
||||||
Stay_up_to_date_with_a_certain_hashtag_88e3 = Atualizações com um dado marcador
|
|
||||||
# Description for notifications column
|
|
||||||
Stay_up_to_date_with_notifications_and_mentions_6f4e = Atualizações com notificações e menções
|
|
||||||
# Description for someone else's notes column
|
|
||||||
Stay_up_to_date_with_someone_else_s_notes___replies_464c = Atualizar-me de notas e respostas de outra pessoa
|
|
||||||
# Description for someone else's notifications column
|
|
||||||
Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Atualizar-me de notificações e menções de outra pessoa
|
|
||||||
# Description for individual user column
|
|
||||||
Stay_up_to_date_with_someone_s_notes___replies_aa78 = Atualizar-me de notas e respostas de outra pessoa
|
|
||||||
# Description for your notifications column
|
|
||||||
Stay_up_to_date_with_your_notifications_and_mentions_e73e = Atualizar-me de notificações e menções
|
|
||||||
# Step 1 label in support instructions
|
|
||||||
Step_1_8656 = Passo 1
|
|
||||||
# Step 2 label in support instructions
|
|
||||||
Step_2_d08d = Passo 2
|
|
||||||
# Label for storage settings section
|
|
||||||
Storage_ed65 = Armazenamento
|
|
||||||
# Column title for subscribing to external user
|
|
||||||
Subscribe_to_someone_else_s_notes_d1e9 = Subscrever as notas de outra pessoa
|
|
||||||
# Column title for subscribing to individual user
|
|
||||||
Subscribe_to_someone_s_notes_b3c8 = Subscrever as notas de alguém
|
|
||||||
# Support email address
|
|
||||||
Support_email_44d9 = E-mail de suporte:
|
|
||||||
# Hover text for dark mode toggle button
|
|
||||||
Switch_to_dark_mode_4dec = Mudar para o modo escuro
|
|
||||||
# Hover text for light mode toggle button
|
|
||||||
Switch_to_light_mode_72ce = Mudar para o modo claro
|
|
||||||
# Button text to load blurred media
|
|
||||||
Tap_to_Load_4b05 = Toca para carregar
|
|
||||||
# Message shown when Dave trial period has ended
|
|
||||||
The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = O teste do assistente de IA Dave Nost terminou :(. Obrigado por testares! Dave com ativação de ZAPS em breve!
|
|
||||||
# Label for theme, Appearance settings section
|
|
||||||
Theme_4aac = Tema:
|
|
||||||
# Column title for note thread view
|
|
||||||
Thread_0f20 = Tópico
|
|
||||||
# Link text for thread references
|
|
||||||
thread_ad1f = tópico
|
|
||||||
# Title for universe column
|
|
||||||
Universe_e01e = Universo
|
|
||||||
# Column title for universe feed
|
|
||||||
Universe_ffaa = Universo
|
|
||||||
# Checkbox label for using wallet only for current account
|
|
||||||
Use_this_wallet_for_the_current_account_only_61dc = Usar esta carteira apenas para a conta atual
|
|
||||||
# Username and domain identification message
|
|
||||||
username___at___domain___will_be_used_for_identification_a4fd = "{ $username }" em "{ $domain }" será usado para identificação
|
|
||||||
# Profile username field label
|
|
||||||
Username_daa7 = Nome de utilizador
|
|
||||||
# Label for view folder button, Storage settings section
|
|
||||||
View_folder_9742 = Ver pasta
|
|
||||||
# Column title for wallet management
|
|
||||||
Wallet_5e50 = Carteira
|
|
||||||
# Hint for deck name input field
|
|
||||||
We_recommend_short_names_083e = Recomendamos nomes curtos
|
|
||||||
# Profile website field label
|
|
||||||
Website_7980 = Website
|
|
||||||
# Placeholder for note input field
|
|
||||||
Write_a_banger_note_here_bad2 = Escreve uma nota sonante aqui...
|
|
||||||
# Placeholder text for key input field
|
|
||||||
Your_key_here_81bd = A tua chave aqui...
|
|
||||||
# Title for your notes column
|
|
||||||
Your_Notes_f6db = Minhas notas
|
|
||||||
# Title for your notifications column
|
|
||||||
Your_Notifications_080d = Minhas notificações
|
|
||||||
# Heading for zap (tip) action
|
|
||||||
Zap_16b4 = Zap
|
|
||||||
# Hover text for zap button
|
|
||||||
Zap_this_note_42b2 = Enviar zaps a esta nota
|
|
||||||
# Label for zoom level, Appearance settings section
|
|
||||||
Zoom_Level_29a8 = Nível de zoom:
|
|
||||||
|
|
||||||
# Pluralized strings
|
|
||||||
|
|
||||||
# Search results count
|
|
||||||
Got__count__results_for___query_85fb =
|
|
||||||
{ $count ->
|
|
||||||
[one] { $count } resultado obtido para '{ $query }'
|
|
||||||
*[other] { $count } resultados obtidos para '{ $query }'
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -55,6 +55,8 @@ Ask_dave_anything_33d1 = ถามเดฟได้ทุกเรื่อง.
|
|||||||
Banner_52ef = ภาพปก
|
Banner_52ef = ภาพปก
|
||||||
# Beta version label
|
# Beta version label
|
||||||
BETA_8e5d = เบต้า
|
BETA_8e5d = เบต้า
|
||||||
|
# Option in settings section to show the source client label at the bottom of the note
|
||||||
|
Bottom_33c8 = ด้านล่าง
|
||||||
# Broadcast the note to all connected relays
|
# Broadcast the note to all connected relays
|
||||||
Broadcast_fe43 = เผยแพร่
|
Broadcast_fe43 = เผยแพร่
|
||||||
# Broadcast the note only to local network relays
|
# Broadcast the note only to local network relays
|
||||||
@@ -90,11 +92,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 = คัดลอก npub
|
Copy_Pubkey_9cc4 = คัดลอก Pubkey
|
||||||
# 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
|
||||||
@@ -163,10 +165,10 @@ 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
|
|
||||||
Font_size_dd73 = ขนาดตัวอักษร:
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = แฮชแท็ก
|
Hashtags_f8e0 = แฮชแท็ก
|
||||||
|
# Option in settings section to hide the source client label in note display
|
||||||
|
Hide_281d = ซ่อน
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = หน้าแรก
|
Home_8c19 = หน้าแรก
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
@@ -237,10 +239,6 @@ Notifications_d673 = การแจ้งเตือน
|
|||||||
Notifications_ef56 = การแจ้งเตือน
|
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
|
|
||||||
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
|
||||||
@@ -256,7 +254,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
|
||||||
@@ -293,8 +291,6 @@ replying_to_a_note_e0bc = ตอบกลับโน้ต
|
|||||||
Repost_this_note_8e56 = รีโพสต์โน้ตนี้
|
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
|
|
||||||
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
|
||||||
@@ -317,12 +313,12 @@ 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
|
||||||
Settings_7a4f = การตั้งค่า
|
Settings_7a4f = การตั้งค่า
|
||||||
|
# Label for Show source client, others settings section
|
||||||
|
Show_source_client_9e31 = แสดงไคลเอนต์ต้นทาง
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
|
Show_the_last_note_for_each_user_from_a_list_50e7 = แสดงโน้ตล่าสุดของผู้ใช้แต่ละคนจากรายการ
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -331,8 +327,6 @@ Sign_out_337b = ออกจากระบบ
|
|||||||
Someone_else_s_Notes_7e5f = โน้ตของผู้อื่น
|
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
|
|
||||||
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
|
||||||
@@ -357,8 +351,6 @@ Storage_ed65 = พื้นที่จัดเก็บ
|
|||||||
Subscribe_to_someone_else_s_notes_d1e9 = ติดตามโน้ตของผู้อื่น
|
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_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
|
||||||
@@ -373,6 +365,8 @@ Theme_4aac = ธีม:
|
|||||||
Thread_0f20 = เธรด
|
Thread_0f20 = เธรด
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
thread_ad1f = เธรด
|
thread_ad1f = เธรด
|
||||||
|
# Option in settings section to show the source client label at the top of the note
|
||||||
|
Top_6aeb = ด้านบน
|
||||||
# Title for universe column
|
# Title for universe column
|
||||||
Universe_e01e = จักรวาล
|
Universe_e01e = จักรวาล
|
||||||
# Column title for universe feed
|
# Column title for universe feed
|
||||||
@@ -380,11 +374,11 @@ 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
|
||||||
View_folder_9742 = ดูโฟลเดอร์
|
View_folder_9742 = ดูโฟลเดอร์:
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = วอลเล็ต
|
Wallet_5e50 = วอลเล็ต
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
@@ -392,7 +386,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
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ Ask_dave_anything_33d1 = 向 Dave 提问任何问题…
|
|||||||
Banner_52ef = 横幅
|
Banner_52ef = 横幅
|
||||||
# Beta version label
|
# Beta version label
|
||||||
BETA_8e5d = BETA
|
BETA_8e5d = BETA
|
||||||
|
# Option in settings section to show the source client label at the bottom of the note
|
||||||
|
Bottom_33c8 = 底部
|
||||||
# Broadcast the note to all connected relays
|
# Broadcast the note to all connected relays
|
||||||
Broadcast_fe43 = 广播
|
Broadcast_fe43 = 广播
|
||||||
# Broadcast the note only to local network relays
|
# Broadcast the note only to local network relays
|
||||||
@@ -161,10 +163,10 @@ Enter_your_key_0fca = 请输入你的密钥
|
|||||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 请输入你的公钥(npub)、nostr 地址(如 { $address })、或私钥(nsec)。 你必须输入你的私钥才能发帖、回复等等。
|
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 请输入你的公钥(npub)、nostr 地址(如 { $address })、或私钥(nsec)。 你必须输入你的私钥才能发帖、回复等等。
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = 查找用户
|
Find_User_bd12 = 查找用户
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = 字体大小:
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = 标签
|
Hashtags_f8e0 = 标签
|
||||||
|
# Option in settings section to hide the source client label in note display
|
||||||
|
Hide_281d = 隐藏
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = 主页
|
Home_8c19 = 主页
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
@@ -235,8 +237,6 @@ Notifications_d673 = 通知
|
|||||||
Notifications_ef56 = 通知
|
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
|
|
||||||
On_f412 = 开启
|
|
||||||
# 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
|
||||||
@@ -289,8 +289,6 @@ replying_to_a_note_e0bc = 正在回复笔记
|
|||||||
Repost_this_note_8e56 = 转发此笔记
|
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
|
|
||||||
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
|
||||||
@@ -317,6 +315,8 @@ See_the_whole_nostr_universe_7694 = 查看整个 nostr 宇宙
|
|||||||
Send_1ea4 = 发送
|
Send_1ea4 = 发送
|
||||||
# Column title for app settings
|
# Column title for app settings
|
||||||
Settings_7a4f = 设置
|
Settings_7a4f = 设置
|
||||||
|
# Label for Show source client, others settings section
|
||||||
|
Show_source_client_9e31 = 显示来源客户端
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
|
Show_the_last_note_for_each_user_from_a_list_50e7 = 显示列表中每个用户的最新一条笔记
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -325,8 +325,6 @@ Sign_out_337b = 登出
|
|||||||
Someone_else_s_Notes_7e5f = 其他人的笔记
|
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
|
|
||||||
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
|
||||||
@@ -351,8 +349,6 @@ Storage_ed65 = 存储
|
|||||||
Subscribe_to_someone_else_s_notes_d1e9 = 订阅他人的笔记
|
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_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
|
||||||
@@ -367,6 +363,8 @@ Theme_4aac = 主题:
|
|||||||
Thread_0f20 = 帖子
|
Thread_0f20 = 帖子
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
thread_ad1f = 帖子
|
thread_ad1f = 帖子
|
||||||
|
# Option in settings section to show the source client label at the top of the note
|
||||||
|
Top_6aeb = 顶部
|
||||||
# Title for universe column
|
# Title for universe column
|
||||||
Universe_e01e = 宇宙
|
Universe_e01e = 宇宙
|
||||||
# Column title for universe feed
|
# Column title for universe feed
|
||||||
@@ -378,7 +376,7 @@ username___at___domain___will_be_used_for_identification_a4fd = "{ $username }"
|
|||||||
# 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
|
||||||
View_folder_9742 = 查看文件夹
|
View_folder_9742 = 查看文件夹:
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = 钱包
|
Wallet_5e50 = 钱包
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ Ask_dave_anything_33d1 = 向 Dave 提問任何問題...
|
|||||||
Banner_52ef = 橫幅
|
Banner_52ef = 橫幅
|
||||||
# Beta version label
|
# Beta version label
|
||||||
BETA_8e5d = 測試版
|
BETA_8e5d = 測試版
|
||||||
|
# Option in settings section to show the source client label at the bottom of the note
|
||||||
|
Bottom_33c8 = 底部
|
||||||
# Broadcast the note to all connected relays
|
# Broadcast the note to all connected relays
|
||||||
Broadcast_fe43 = 廣播
|
Broadcast_fe43 = 廣播
|
||||||
# Broadcast the note only to local network relays
|
# Broadcast the note only to local network relays
|
||||||
@@ -161,10 +163,10 @@ Enter_your_key_0fca = 請輸入你的密鑰
|
|||||||
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 請輸入你的公鑰(npub)、nostr 地址(如 { $address })、或私鑰(nsec)。你必須輸入你的私鑰才能發貼、回覆等等。
|
Enter_your_public_key__npub___nostr_address__e_g___address____or_private_key__nsec___You_must_enter_your_private_key_to_be_able_to_post__reply__etc_48e9 = 請輸入你的公鑰(npub)、nostr 地址(如 { $address })、或私鑰(nsec)。你必須輸入你的私鑰才能發貼、回覆等等。
|
||||||
# Label for find user button
|
# Label for find user button
|
||||||
Find_User_bd12 = 查找用戶
|
Find_User_bd12 = 查找用戶
|
||||||
# Label for font size, Appearance settings section
|
|
||||||
Font_size_dd73 = 字體大小:
|
|
||||||
# Title for hashtags column
|
# Title for hashtags column
|
||||||
Hashtags_f8e0 = 標籤
|
Hashtags_f8e0 = 標籤
|
||||||
|
# Option in settings section to hide the source client label in note display
|
||||||
|
Hide_281d = 隱藏
|
||||||
# Title for Home column
|
# Title for Home column
|
||||||
Home_8c19 = 主頁
|
Home_8c19 = 主頁
|
||||||
# Label for deck icon selection
|
# Label for deck icon selection
|
||||||
@@ -235,8 +237,6 @@ Notifications_d673 = 通知
|
|||||||
Notifications_ef56 = 通知
|
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
|
|
||||||
On_f412 = 開啟
|
|
||||||
# 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
|
||||||
@@ -289,8 +289,6 @@ replying_to_a_note_e0bc = 正在回覆筆記
|
|||||||
Repost_this_note_8e56 = 轉發此筆記
|
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
|
|
||||||
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
|
||||||
@@ -317,6 +315,8 @@ See_the_whole_nostr_universe_7694 = 查看整個 nostr 宇宙
|
|||||||
Send_1ea4 = 發送
|
Send_1ea4 = 發送
|
||||||
# Column title for app settings
|
# Column title for app settings
|
||||||
Settings_7a4f = 設置
|
Settings_7a4f = 設置
|
||||||
|
# Label for Show source client, others settings section
|
||||||
|
Show_source_client_9e31 = 顯示來源客戶端
|
||||||
# Description for last note per user column
|
# Description for last note per user column
|
||||||
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
|
Show_the_last_note_for_each_user_from_a_list_50e7 = 顯示列表中每個用戶的最後一條筆記
|
||||||
# Button label to sign out of account
|
# Button label to sign out of account
|
||||||
@@ -325,8 +325,6 @@ Sign_out_337b = 登出
|
|||||||
Someone_else_s_Notes_7e5f = 其他人的筆記
|
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
|
|
||||||
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
|
||||||
@@ -351,8 +349,6 @@ Storage_ed65 = 儲存
|
|||||||
Subscribe_to_someone_else_s_notes_d1e9 = 訂閱他人的筆記
|
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_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
|
||||||
@@ -367,6 +363,8 @@ Theme_4aac = 主題:
|
|||||||
Thread_0f20 = 串文
|
Thread_0f20 = 串文
|
||||||
# Link text for thread references
|
# Link text for thread references
|
||||||
thread_ad1f = 串文
|
thread_ad1f = 串文
|
||||||
|
# Option in settings section to show the source client label at the top of the note
|
||||||
|
Top_6aeb = 頂部
|
||||||
# Title for universe column
|
# Title for universe column
|
||||||
Universe_e01e = 宇宙
|
Universe_e01e = 宇宙
|
||||||
# Column title for universe feed
|
# Column title for universe feed
|
||||||
@@ -378,7 +376,7 @@ username___at___domain___will_be_used_for_identification_a4fd = "{ $username }"
|
|||||||
# 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
|
||||||
View_folder_9742 = 查看文件夾
|
View_folder_9742 = 查看文件夾:
|
||||||
# Column title for wallet management
|
# Column title for wallet management
|
||||||
Wallet_5e50 = 錢包
|
Wallet_5e50 = 錢包
|
||||||
# Hint for deck name input field
|
# Hint for deck name input field
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ pub fn setup_multicast_relay(
|
|||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut events = Events::with_capacity(1);
|
let mut events = Events::with_capacity(1);
|
||||||
loop {
|
loop {
|
||||||
if let Err(err) = poll.poll(&mut events, None) {
|
if let Err(err) = poll.poll(&mut events, Some(Duration::from_millis(100))) {
|
||||||
error!("multicast socket poll error: {err}. ending multicast poller.");
|
error!("multicast socket poll error: {err}. ending multicast poller.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ impl From<RelayEvent<'_>> for OwnedRelayEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||||
pub struct _RelaySub {
|
pub struct RelaySub {
|
||||||
pub(crate) subid: String,
|
pub(crate) subid: String,
|
||||||
pub(crate) filter: String,
|
pub(crate) filter: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,13 +45,11 @@ fluent = { workspace = true }
|
|||||||
fluent-resmgr = { workspace = true }
|
fluent-resmgr = { workspace = true }
|
||||||
fluent-langneg = { workspace = true }
|
fluent-langneg = { workspace = true }
|
||||||
unic-langid = { workspace = true }
|
unic-langid = { workspace = true }
|
||||||
|
sys-locale = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
md5 = { workspace = true }
|
md5 = { workspace = true }
|
||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
chrono = { workspace = true }
|
|
||||||
indexmap = {workspace = true}
|
|
||||||
crossbeam-channel = "0.5"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
@@ -59,8 +57,6 @@ 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 }
|
|
||||||
ndk-context = "0.1"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
puffin = ["puffin_egui", "dep:puffin"]
|
puffin = ["puffin_egui", "dep:puffin"]
|
||||||
|
|||||||
@@ -267,11 +267,6 @@ 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
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ impl Contacts {
|
|||||||
|
|
||||||
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
|
||||||
let binding = ndb
|
let binding = ndb
|
||||||
.query(txn, std::slice::from_ref(&self.filter), 1)
|
.query(txn, &[self.filter.clone()], 1)
|
||||||
.expect("query user relays results");
|
.expect("query user relays results");
|
||||||
|
|
||||||
let Some(res) = binding.first() else {
|
let Some(res) = binding.first() else {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ impl AccountMutedData {
|
|||||||
.limit()
|
.limit()
|
||||||
.unwrap_or(crate::filter::default_limit()) as i32;
|
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||||
let nks = ndb
|
let nks = ndb
|
||||||
.query(txn, std::slice::from_ref(&self.filter), lim)
|
.query(txn, &[self.filter.clone()], lim)
|
||||||
.expect("query user muted results")
|
.expect("query user muted results")
|
||||||
.iter()
|
.iter()
|
||||||
.map(|qr| qr.note_key)
|
.map(|qr| qr.note_key)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ impl AccountRelayData {
|
|||||||
.limit()
|
.limit()
|
||||||
.unwrap_or(crate::filter::default_limit()) as i32;
|
.unwrap_or(crate::filter::default_limit()) as i32;
|
||||||
let nks = ndb
|
let nks = ndb
|
||||||
.query(txn, std::slice::from_ref(&self.filter), lim)
|
.query(txn, &[self.filter.clone()], lim)
|
||||||
.expect("query user relays results")
|
.expect("query user relays results")
|
||||||
.iter()
|
.iter()
|
||||||
.map(|qr| qr.note_key)
|
.map(|qr| qr.note_key)
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ use std::rc::Rc;
|
|||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
use android_activity::AndroidApp;
|
|
||||||
|
|
||||||
pub enum AppAction {
|
pub enum AppAction {
|
||||||
Note(NoteAction),
|
Note(NoteAction),
|
||||||
ToggleChrome,
|
ToggleChrome,
|
||||||
@@ -54,9 +51,6 @@ pub struct Notedeck {
|
|||||||
frame_history: FrameHistory,
|
frame_history: FrameHistory,
|
||||||
job_pool: JobPool,
|
job_pool: JobPool,
|
||||||
i18n: Localization,
|
i18n: Localization,
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
android_app: Option<AndroidApp>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Our chrome, which is basically nothing
|
/// Our chrome, which is basically nothing
|
||||||
@@ -144,11 +138,6 @@ fn setup_puffin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Notedeck {
|
impl Notedeck {
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
pub fn set_android_context(&mut self, context: AndroidApp) {
|
|
||||||
self.android_app = Some(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
|
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
setup_puffin();
|
setup_puffin();
|
||||||
@@ -252,8 +241,8 @@ impl Notedeck {
|
|||||||
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
|
let setting_locale: Result<LanguageIdentifier, LanguageIdentifierError> =
|
||||||
settings.locale().parse();
|
settings.locale().parse();
|
||||||
|
|
||||||
if let Ok(setting_locale) = setting_locale {
|
if setting_locale.is_ok() {
|
||||||
if let Err(err) = i18n.set_locale(setting_locale) {
|
if let Err(err) = i18n.set_locale(setting_locale.unwrap()) {
|
||||||
error!("{err}");
|
error!("{err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,8 +272,6 @@ impl Notedeck {
|
|||||||
zaps,
|
zaps,
|
||||||
job_pool,
|
job_pool,
|
||||||
i18n,
|
i18n,
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
android_app: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +296,7 @@ impl Notedeck {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
if !completely_unrecognized.is_empty() {
|
if !completely_unrecognized.is_empty() {
|
||||||
let err = format!("Unrecognized arguments: {completely_unrecognized:?}");
|
let err = format!("Unrecognized arguments: {:?}", completely_unrecognized);
|
||||||
tracing::error!("{}", &err);
|
tracing::error!("{}", &err);
|
||||||
return Err(Error::Generic(err));
|
return Err(Error::Generic(err));
|
||||||
}
|
}
|
||||||
@@ -348,8 +335,6 @@ impl Notedeck {
|
|||||||
frame_history: &mut self.frame_history,
|
frame_history: &mut self.frame_history,
|
||||||
job_pool: &mut self.job_pool,
|
job_pool: &mut self.job_pool,
|
||||||
i18n: &mut self.i18n,
|
i18n: &mut self.i18n,
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
android: self.android_app.as_ref().unwrap().clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,10 +124,10 @@ impl Args {
|
|||||||
res.options.set(NotedeckOptions::UseKeystore, true);
|
res.options.set(NotedeckOptions::UseKeystore, true);
|
||||||
} else if arg == "--relay-debug" {
|
} else if arg == "--relay-debug" {
|
||||||
res.options.set(NotedeckOptions::RelayDebug, true);
|
res.options.set(NotedeckOptions::RelayDebug, true);
|
||||||
|
} else if arg == "--show-client" {
|
||||||
|
res.options.set(NotedeckOptions::ShowClient, true);
|
||||||
} else if arg == "--notebook" {
|
} else if arg == "--notebook" {
|
||||||
res.options.set(NotedeckOptions::FeatureNotebook, true);
|
res.options.set(NotedeckOptions::FeatureNotebook, true);
|
||||||
} else if arg == "--clndash" {
|
|
||||||
res.options.set(NotedeckOptions::FeatureClnDash, true);
|
|
||||||
} else {
|
} else {
|
||||||
unrecognized_args.insert(arg.clone());
|
unrecognized_args.insert(arg.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ use egui_winit::clipboard::Clipboard;
|
|||||||
use enostr::RelayPool;
|
use enostr::RelayPool;
|
||||||
use nostrdb::Ndb;
|
use nostrdb::Ndb;
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
use android_activity::AndroidApp;
|
|
||||||
use egui::{Pos2, Rect};
|
|
||||||
// TODO: make this interface more sandboxed
|
// TODO: make this interface more sandboxed
|
||||||
|
|
||||||
pub struct AppContext<'a> {
|
pub struct AppContext<'a> {
|
||||||
@@ -29,62 +26,4 @@ pub struct AppContext<'a> {
|
|||||||
pub frame_history: &'a mut FrameHistory,
|
pub frame_history: &'a mut FrameHistory,
|
||||||
pub job_pool: &'a mut JobPool,
|
pub job_pool: &'a mut JobPool,
|
||||||
pub i18n: &'a mut Localization,
|
pub i18n: &'a mut Localization,
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
pub android: AndroidApp,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum SoftKeyboardContext {
|
|
||||||
Virtual,
|
|
||||||
Platform { ppp: f32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SoftKeyboardContext {
|
|
||||||
pub fn platform(context: &egui::Context) -> Self {
|
|
||||||
Self::Platform {
|
|
||||||
ppp: context.pixels_per_point(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> AppContext<'a> {
|
|
||||||
pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> {
|
|
||||||
match ctx {
|
|
||||||
SoftKeyboardContext::Virtual => {
|
|
||||||
let height = 400.0;
|
|
||||||
skb_rect_from_screen_rect(screen_rect, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
SoftKeyboardContext::Platform { ppp } => {
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
{
|
|
||||||
use android_activity::InsetType;
|
|
||||||
|
|
||||||
// not sure why I need this, it seems to be consistently off by some amount of
|
|
||||||
// pixels ?
|
|
||||||
let fudge = 0.0;
|
|
||||||
|
|
||||||
let inset = self.android.get_window_insets(InsetType::Ime);
|
|
||||||
let height = (inset.bottom as f32 / ppp) - fudge;
|
|
||||||
skb_rect_from_screen_rect(screen_rect, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
{
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn skb_rect_from_screen_rect(screen_rect: Rect, height: f32) -> Option<Rect> {
|
|
||||||
if height == 0.0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let min = Pos2::new(0.0, screen_rect.max.y - height);
|
|
||||||
Some(Rect::from_min_max(min, screen_rect.max))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,13 +86,6 @@ impl FilterStates {
|
|||||||
}
|
}
|
||||||
self.states.insert(relay, state);
|
self.states.insert(relay, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For contacts, since that sub is managed elsewhere
|
|
||||||
pub fn set_all_states(&mut self, state: FilterState) {
|
|
||||||
for cur_state in self.states.values_mut() {
|
|
||||||
*cur_state = state.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// We may need to fetch some data from relays before our filter is ready.
|
/// We may need to fetch some data from relays before our filter is ready.
|
||||||
@@ -183,24 +176,21 @@ 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(
|
pub fn since_optimize_filter_with(filter: Filter, notes: &[NoteRef], since_gap: u64) -> Filter {
|
||||||
filter: Filter,
|
|
||||||
latest_note: Option<&NoteRef>,
|
|
||||||
since_gap: u64,
|
|
||||||
) -> Filter {
|
|
||||||
// Get the latest entry in the events
|
// Get the latest entry in the events
|
||||||
let Some(latest) = latest_note else {
|
if notes.is_empty() {
|
||||||
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, latest: Option<&NoteRef>) -> Filter {
|
pub fn since_optimize_filter(filter: Filter, notes: &[NoteRef]) -> Filter {
|
||||||
since_optimize_filter_with(filter, latest, 60)
|
since_optimize_filter_with(filter, notes, 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_limit() -> u64 {
|
pub fn default_limit() -> u64 {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use fluent::{FluentArgs, FluentBundle, FluentResource};
|
|||||||
use fluent_langneg::negotiate_languages;
|
use fluent_langneg::negotiate_languages;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use sys_locale;
|
||||||
use unic_langid::{langid, LanguageIdentifier};
|
use unic_langid::{langid, LanguageIdentifier};
|
||||||
|
|
||||||
const EN_US: LanguageIdentifier = langid!("en-US");
|
const EN_US: LanguageIdentifier = langid!("en-US");
|
||||||
@@ -11,13 +12,11 @@ const DE: LanguageIdentifier = langid!("de");
|
|||||||
const ES_419: LanguageIdentifier = langid!("es-419");
|
const ES_419: LanguageIdentifier = langid!("es-419");
|
||||||
const ES_ES: LanguageIdentifier = langid!("es-ES");
|
const ES_ES: LanguageIdentifier = langid!("es-ES");
|
||||||
const FR: LanguageIdentifier = langid!("fr");
|
const FR: LanguageIdentifier = langid!("fr");
|
||||||
const JA: LanguageIdentifier = langid!("ja");
|
|
||||||
const PT_BR: LanguageIdentifier = langid!("pt-BR");
|
const PT_BR: LanguageIdentifier = langid!("pt-BR");
|
||||||
const PT_PT: LanguageIdentifier = langid!("pt-PT");
|
|
||||||
const TH: LanguageIdentifier = langid!("th");
|
const TH: LanguageIdentifier = langid!("th");
|
||||||
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
|
const ZH_CN: LanguageIdentifier = langid!("zh-CN");
|
||||||
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
|
const ZH_TW: LanguageIdentifier = langid!("zh-TW");
|
||||||
const NUM_FTLS: usize = 12;
|
const NUM_FTLS: usize = 10;
|
||||||
|
|
||||||
const EN_US_NATIVE_NAME: &str = "English (US)";
|
const EN_US_NATIVE_NAME: &str = "English (US)";
|
||||||
const EN_XA_NATIVE_NAME: &str = "Éñglísh (Pséúdólóçàlé)";
|
const EN_XA_NATIVE_NAME: &str = "Éñglísh (Pséúdólóçàlé)";
|
||||||
@@ -25,9 +24,7 @@ const DE_NATIVE_NAME: &str = "Deutsch";
|
|||||||
const ES_419_NATIVE_NAME: &str = "Español (Latinoamérica)";
|
const ES_419_NATIVE_NAME: &str = "Español (Latinoamérica)";
|
||||||
const ES_ES_NATIVE_NAME: &str = "Español (España)";
|
const ES_ES_NATIVE_NAME: &str = "Español (España)";
|
||||||
const FR_NATIVE_NAME: &str = "Français";
|
const FR_NATIVE_NAME: &str = "Français";
|
||||||
const JA_NATIVE_NAME: &str = "日本語";
|
|
||||||
const PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
|
const PT_BR_NATIVE_NAME: &str = "Português (Brasil)";
|
||||||
const PT_PT_NATIVE_NAME: &str = "Português (Portugal)";
|
|
||||||
const TH_NATIVE_NAME: &str = "ภาษาไทย";
|
const TH_NATIVE_NAME: &str = "ภาษาไทย";
|
||||||
const ZH_CN_NATIVE_NAME: &str = "简体中文";
|
const ZH_CN_NATIVE_NAME: &str = "简体中文";
|
||||||
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
|
const ZH_TW_NATIVE_NAME: &str = "繁體中文";
|
||||||
@@ -62,18 +59,10 @@ const FTLS: [StaticBundle; NUM_FTLS] = [
|
|||||||
identifier: FR,
|
identifier: FR,
|
||||||
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
|
ftl: include_str!("../../../../assets/translations/fr/main.ftl"),
|
||||||
},
|
},
|
||||||
StaticBundle {
|
|
||||||
identifier: JA,
|
|
||||||
ftl: include_str!("../../../../assets/translations/ja/main.ftl"),
|
|
||||||
},
|
|
||||||
StaticBundle {
|
StaticBundle {
|
||||||
identifier: PT_BR,
|
identifier: PT_BR,
|
||||||
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
|
ftl: include_str!("../../../../assets/translations/pt-BR/main.ftl"),
|
||||||
},
|
},
|
||||||
StaticBundle {
|
|
||||||
identifier: PT_PT,
|
|
||||||
ftl: include_str!("../../../../assets/translations/pt-PT/main.ftl"),
|
|
||||||
},
|
|
||||||
StaticBundle {
|
StaticBundle {
|
||||||
identifier: TH,
|
identifier: TH,
|
||||||
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
|
ftl: include_str!("../../../../assets/translations/th/main.ftl"),
|
||||||
@@ -113,10 +102,6 @@ pub struct Localization {
|
|||||||
|
|
||||||
impl Default for Localization {
|
impl Default for Localization {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
// Default to English (US)
|
|
||||||
let default_locale = &EN_US;
|
|
||||||
let fallback_locale = default_locale.to_owned();
|
|
||||||
|
|
||||||
// Build available locales list
|
// Build available locales list
|
||||||
let available_locales = vec![
|
let available_locales = vec![
|
||||||
EN_US.clone(),
|
EN_US.clone(),
|
||||||
@@ -125,9 +110,7 @@ impl Default for Localization {
|
|||||||
ES_419.clone(),
|
ES_419.clone(),
|
||||||
ES_ES.clone(),
|
ES_ES.clone(),
|
||||||
FR.clone(),
|
FR.clone(),
|
||||||
JA.clone(),
|
|
||||||
PT_BR.clone(),
|
PT_BR.clone(),
|
||||||
PT_PT.clone(),
|
|
||||||
TH.clone(),
|
TH.clone(),
|
||||||
ZH_CN.clone(),
|
ZH_CN.clone(),
|
||||||
ZH_TW.clone(),
|
ZH_TW.clone(),
|
||||||
@@ -140,16 +123,26 @@ impl Default for Localization {
|
|||||||
(ES_419, ES_419_NATIVE_NAME.to_owned()),
|
(ES_419, ES_419_NATIVE_NAME.to_owned()),
|
||||||
(ES_ES, ES_ES_NATIVE_NAME.to_owned()),
|
(ES_ES, ES_ES_NATIVE_NAME.to_owned()),
|
||||||
(FR, FR_NATIVE_NAME.to_owned()),
|
(FR, FR_NATIVE_NAME.to_owned()),
|
||||||
(JA, JA_NATIVE_NAME.to_owned()),
|
|
||||||
(PT_BR, PT_BR_NATIVE_NAME.to_owned()),
|
(PT_BR, PT_BR_NATIVE_NAME.to_owned()),
|
||||||
(PT_PT, PT_PT_NATIVE_NAME.to_owned()),
|
|
||||||
(TH, TH_NATIVE_NAME.to_owned()),
|
(TH, TH_NATIVE_NAME.to_owned()),
|
||||||
(ZH_CN, ZH_CN_NATIVE_NAME.to_owned()),
|
(ZH_CN, ZH_CN_NATIVE_NAME.to_owned()),
|
||||||
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
|
(ZH_TW, ZH_TW_NATIVE_NAME.to_owned()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Detect system locale and find best match
|
||||||
|
let current_locale = Self::negotiate_system_locale_with_preferences(&available_locales);
|
||||||
|
|
||||||
|
// Fallback locale is always EN_US
|
||||||
|
let fallback_locale = EN_US.clone();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Localization initialized - Selected locale: {}, Fallback: {}",
|
||||||
|
current_locale,
|
||||||
|
fallback_locale
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
current_locale: default_locale.to_owned(),
|
current_locale,
|
||||||
available_locales,
|
available_locales,
|
||||||
fallback_locale,
|
fallback_locale,
|
||||||
locale_native_names,
|
locale_native_names,
|
||||||
@@ -175,6 +168,150 @@ impl Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract just the language and region from locale string (e.g., "fr-FR-u-mu-celsius" -> "fr-FR")
|
||||||
|
fn extract_language_region(locale_str: &str) -> String {
|
||||||
|
// Split by '-' and analyze the parts
|
||||||
|
let parts: Vec<&str> = locale_str.split('-').collect();
|
||||||
|
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
// Check if the second part looks like a region
|
||||||
|
let second_part = parts[1];
|
||||||
|
if (second_part.len() >= 2) {
|
||||||
|
format!("{}-{}", parts[0], parts[1])
|
||||||
|
} else {
|
||||||
|
// Second part is not a region, probably an extension (e.g., "u", "t", "x")
|
||||||
|
// Just return the language part
|
||||||
|
parts[0].to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only one part, return as is
|
||||||
|
locale_str.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Negotiate the best locale from all system preferences against available locales
|
||||||
|
fn negotiate_system_locale_with_preferences(
|
||||||
|
available_locales: &[LanguageIdentifier],
|
||||||
|
) -> LanguageIdentifier {
|
||||||
|
// Get all system preferred locales in descending order
|
||||||
|
let mut system_locales: Vec<String> = sys_locale::get_locales().collect();
|
||||||
|
if system_locales.is_empty() {
|
||||||
|
tracing::info!("No system locales detected, using fallback: en-US");
|
||||||
|
return EN_US.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("System preferred locales: {:?}", system_locales);
|
||||||
|
|
||||||
|
// If we only got one locale, it might be that the system only returns the primary locale
|
||||||
|
// In this case, we can try to add common fallbacks based on the detected locale
|
||||||
|
if system_locales.len() == 1 {
|
||||||
|
let primary = &system_locales[0];
|
||||||
|
|
||||||
|
// Try to parse the primary locale, handling extensions
|
||||||
|
let primary_lang = if let Ok(locale) = primary.parse::<LanguageIdentifier>() {
|
||||||
|
locale.language.as_str().to_string()
|
||||||
|
} else {
|
||||||
|
// If parsing fails, try extracting language-region
|
||||||
|
// let stripped = Self::extract_language_region(primary);
|
||||||
|
// if let Ok(locale) = stripped.parse::<LanguageIdentifier>() {
|
||||||
|
// locale.language.as_str().to_string()
|
||||||
|
// } else {
|
||||||
|
tracing::info!("Could not parse primary locale: {}", primary);
|
||||||
|
"unknown".to_string()
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Only one system locale detected: {} (language: {})",
|
||||||
|
primary,
|
||||||
|
primary_lang
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add common fallbacks for the detected language
|
||||||
|
match primary_lang.as_str() {
|
||||||
|
"uk" => {
|
||||||
|
// For Ukrainian, add common fallbacks
|
||||||
|
system_locales.push("es-ES".to_string());
|
||||||
|
system_locales.push("en-US".to_string());
|
||||||
|
tracing::info!("Added fallbacks for Ukrainian: {:?}", system_locales);
|
||||||
|
}
|
||||||
|
"es" => {
|
||||||
|
// For Spanish, add English fallback
|
||||||
|
system_locales.push("en-US".to_string());
|
||||||
|
tracing::info!("Added fallback for Spanish: {:?}", system_locales);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// For other languages, add English fallback
|
||||||
|
system_locales.push("en-US".to_string());
|
||||||
|
tracing::info!("Added fallback for {}: {:?}", primary_lang, system_locales);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert system locale strings to LanguageIdentifiers, handling extensions
|
||||||
|
let mut parsed_system_locales = Vec::new();
|
||||||
|
for locale_str in system_locales {
|
||||||
|
// Try to parse the locale string directly first
|
||||||
|
if let Ok(locale) = locale_str.parse::<LanguageIdentifier>() {
|
||||||
|
parsed_system_locales.push(locale);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If parsing fails, try extracting just language-region
|
||||||
|
// let stripped_locale = Self::extract_language_region(&locale_str);
|
||||||
|
// if let Ok(locale) = stripped_locale.parse::<LanguageIdentifier>() {
|
||||||
|
// parsed_system_locales.push(locale);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
tracing::info!("Failed to parse locale string: {}", locale_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed_system_locales.is_empty() {
|
||||||
|
tracing::info!("No valid system locales parsed, using fallback: en-US");
|
||||||
|
return EN_US.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try exact matches with fluent_langneg
|
||||||
|
let fallback = &EN_US;
|
||||||
|
let negotiated = negotiate_languages(
|
||||||
|
&parsed_system_locales,
|
||||||
|
available_locales,
|
||||||
|
Some(fallback),
|
||||||
|
fluent_langneg::NegotiationStrategy::Filtering,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(result) = negotiated.first() {
|
||||||
|
tracing::info!(
|
||||||
|
"Exact match found: {} from preferences: {:?}",
|
||||||
|
result,
|
||||||
|
parsed_system_locales
|
||||||
|
);
|
||||||
|
return (*result).clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no exact match, try language-only fallbacks
|
||||||
|
tracing::info!("No exact matches found, trying language-only fallbacks");
|
||||||
|
for system_locale in &parsed_system_locales {
|
||||||
|
let system_lang = system_locale.language.as_str();
|
||||||
|
|
||||||
|
// Look for any available locale with the same language
|
||||||
|
for available_locale in available_locales {
|
||||||
|
if available_locale.language.as_str() == system_lang {
|
||||||
|
tracing::debug!(
|
||||||
|
"Language match found: {} (system: {})",
|
||||||
|
available_locale,
|
||||||
|
system_locale
|
||||||
|
);
|
||||||
|
return available_locale.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("No language matches found, using fallback: en-US");
|
||||||
|
EN_US.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets a localized string by its ID
|
/// Gets a localized string by its ID
|
||||||
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
|
pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
|
||||||
self.get_cached_string(id, None)
|
self.get_cached_string(id, None)
|
||||||
@@ -474,20 +611,6 @@ impl Localization {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Negotiates the best locale from a list of preferred locales
|
|
||||||
pub fn negotiate_locale(&self, preferred: &[LanguageIdentifier]) -> LanguageIdentifier {
|
|
||||||
let available = self.available_locales.clone();
|
|
||||||
let negotiated = negotiate_languages(
|
|
||||||
preferred,
|
|
||||||
&available,
|
|
||||||
Some(&self.fallback_locale),
|
|
||||||
fluent_langneg::NegotiationStrategy::Filtering,
|
|
||||||
);
|
|
||||||
negotiated
|
|
||||||
.first()
|
|
||||||
.map_or(self.fallback_locale.clone(), |v| (*v).clone())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Statistics about cache usage
|
/// Statistics about cache usage
|
||||||
@@ -500,6 +623,80 @@ pub struct CacheStats {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_language_region() {
|
||||||
|
// Test that we extract just language and region from various locale formats
|
||||||
|
|
||||||
|
// Test locales with extensions
|
||||||
|
let unicode_locale = "fr-FR-u-mu-celsius";
|
||||||
|
let extracted = Localization::extract_language_region(unicode_locale);
|
||||||
|
assert_eq!(extracted, "fr-FR");
|
||||||
|
|
||||||
|
let transformed_locale = "en-US-t-0-abc123";
|
||||||
|
let extracted = Localization::extract_language_region(transformed_locale);
|
||||||
|
assert_eq!(extracted, "en-US");
|
||||||
|
|
||||||
|
let private_locale = "de-DE-x-phonebk";
|
||||||
|
let extracted = Localization::extract_language_region(private_locale);
|
||||||
|
assert_eq!(extracted, "de-DE");
|
||||||
|
|
||||||
|
// Test simple locale (no extensions)
|
||||||
|
let simple_locale = "en-US";
|
||||||
|
let extracted = Localization::extract_language_region(simple_locale);
|
||||||
|
assert_eq!(extracted, "en-US");
|
||||||
|
|
||||||
|
// Test language-only locale
|
||||||
|
let lang_only = "en";
|
||||||
|
let extracted = Localization::extract_language_region(lang_only);
|
||||||
|
assert_eq!(extracted, "en");
|
||||||
|
|
||||||
|
// Test language with extensions (no region)
|
||||||
|
let lang_with_extensions = "fr-u-mu-celsius";
|
||||||
|
let extracted = Localization::extract_language_region(lang_with_extensions);
|
||||||
|
assert_eq!(extracted, "fr");
|
||||||
|
|
||||||
|
// Test language with other extension types (no region)
|
||||||
|
let lang_with_t_ext = "en-t-0-abc123";
|
||||||
|
let extracted = Localization::extract_language_region(lang_with_t_ext);
|
||||||
|
assert_eq!(extracted, "en");
|
||||||
|
|
||||||
|
let lang_with_x_ext = "de-x-phonebk";
|
||||||
|
let extracted = Localization::extract_language_region(lang_with_x_ext);
|
||||||
|
assert_eq!(extracted, "de");
|
||||||
|
|
||||||
|
// Test locale with numeric region code
|
||||||
|
let numeric_region = "es-419-u-mu-celsius";
|
||||||
|
let extracted = Localization::extract_language_region(numeric_region);
|
||||||
|
assert_eq!(extracted, "es-419");
|
||||||
|
|
||||||
|
// Test locale with 3-letter region code
|
||||||
|
let three_letter_region = "en-USA-t-0-abc123";
|
||||||
|
let extracted = Localization::extract_language_region(three_letter_region);
|
||||||
|
assert_eq!(extracted, "en-USA");
|
||||||
|
|
||||||
|
// Test locale with 2-letter region code
|
||||||
|
let two_letter_region = "fr-FR-u-mu-celsius";
|
||||||
|
let extracted = Localization::extract_language_region(two_letter_region);
|
||||||
|
assert_eq!(extracted, "fr-FR");
|
||||||
|
|
||||||
|
// Test complex locale with multiple parts
|
||||||
|
let complex_locale = "zh-CN-u-ca-chinese-x-private";
|
||||||
|
let extracted = Localization::extract_language_region(complex_locale);
|
||||||
|
assert_eq!(extracted, "zh-CN");
|
||||||
|
|
||||||
|
// Verify that extracted locales can be parsed
|
||||||
|
let test_cases = ["fr-FR", "en-US", "de-DE", "en", "zh-CN"];
|
||||||
|
for extracted in test_cases {
|
||||||
|
if let Ok(locale) = extracted.parse::<LanguageIdentifier>() {
|
||||||
|
tracing::info!("Successfully parsed extracted locale: {}", locale);
|
||||||
|
} else {
|
||||||
|
tracing::error!("Failed to parse extracted locale: {}", extracted);
|
||||||
|
panic!("Should parse locale after extraction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// TODO(jb55): write tests that work, i broke all these during the refacto
|
// TODO(jb55): write tests that work, i broke all these during the refacto
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::media::gif::ensure_latest_texture_from_cache;
|
use crate::media::gif::ensure_latest_texture_from_cache;
|
||||||
use crate::media::images::ImageType;
|
use crate::media::images::ImageType;
|
||||||
use crate::media::AnimationMode;
|
|
||||||
use crate::urls::{UrlCache, UrlMimes};
|
use crate::urls::{UrlCache, UrlMimes};
|
||||||
use crate::ImageMetadata;
|
use crate::ImageMetadata;
|
||||||
use crate::ObfuscationType;
|
use crate::ObfuscationType;
|
||||||
@@ -35,7 +34,7 @@ impl TexturesCache {
|
|||||||
&mut self,
|
&mut self,
|
||||||
url: &str,
|
url: &str,
|
||||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||||
) -> LoadableTextureState<'_> {
|
) -> LoadableTextureState {
|
||||||
let internal = self.handle_and_get_state_internal(url, true, closure);
|
let internal = self.handle_and_get_state_internal(url, true, closure);
|
||||||
|
|
||||||
internal.into()
|
internal.into()
|
||||||
@@ -45,7 +44,7 @@ impl TexturesCache {
|
|||||||
&mut self,
|
&mut self,
|
||||||
url: &str,
|
url: &str,
|
||||||
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>,
|
||||||
) -> TextureState<'_> {
|
) -> TextureState {
|
||||||
let internal = self.handle_and_get_state_internal(url, false, closure);
|
let internal = self.handle_and_get_state_internal(url, false, closure);
|
||||||
|
|
||||||
internal.into()
|
internal.into()
|
||||||
@@ -96,7 +95,7 @@ impl TexturesCache {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState<'_>> {
|
pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState> {
|
||||||
self.cache.get_mut(url).map(|state| {
|
self.cache.get_mut(url).map(|state| {
|
||||||
handle_occupied(state, true);
|
handle_occupied(state, true);
|
||||||
state.into()
|
state.into()
|
||||||
@@ -465,7 +464,6 @@ impl Images {
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
url: &str,
|
url: &str,
|
||||||
img_type: ImageType,
|
img_type: ImageType,
|
||||||
animation_mode: AnimationMode,
|
|
||||||
) -> Option<TextureHandle> {
|
) -> Option<TextureHandle> {
|
||||||
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
|
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
|
||||||
|
|
||||||
@@ -487,13 +485,7 @@ impl Images {
|
|||||||
MediaCacheType::Gif => &mut self.gifs,
|
MediaCacheType::Gif => &mut self.gifs,
|
||||||
};
|
};
|
||||||
|
|
||||||
ensure_latest_texture_from_cache(
|
ensure_latest_texture_from_cache(ui, url, &mut self.gif_states, &mut cache.textures_cache)
|
||||||
ui,
|
|
||||||
url,
|
|
||||||
&mut self.gif_states,
|
|
||||||
&mut cache.textures_cache,
|
|
||||||
animation_mode,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ mod jobs;
|
|||||||
pub mod media;
|
pub mod media;
|
||||||
mod muted;
|
mod muted;
|
||||||
pub mod name;
|
pub mod name;
|
||||||
mod nip51_set;
|
|
||||||
pub mod note;
|
pub mod note;
|
||||||
mod notecache;
|
mod notecache;
|
||||||
mod options;
|
mod options;
|
||||||
@@ -46,7 +45,7 @@ pub use account::relay::RelayAction;
|
|||||||
pub use account::FALLBACK_PUBKEY;
|
pub use account::FALLBACK_PUBKEY;
|
||||||
pub use app::{App, AppAction, Notedeck};
|
pub use app::{App, AppAction, Notedeck};
|
||||||
pub use args::Args;
|
pub use args::Args;
|
||||||
pub use context::{AppContext, SoftKeyboardContext};
|
pub use context::AppContext;
|
||||||
pub use error::{show_one_error_message, Error, FilterError, ZapError};
|
pub use error::{show_one_error_message, Error, FilterError, ZapError};
|
||||||
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
|
||||||
pub use fonts::NamedFontFamily;
|
pub use fonts::NamedFontFamily;
|
||||||
@@ -66,7 +65,6 @@ pub use media::{
|
|||||||
};
|
};
|
||||||
pub use muted::{MuteFun, Muted};
|
pub use muted::{MuteFun, Muted};
|
||||||
pub use name::NostrName;
|
pub use name::NostrName;
|
||||||
pub use nip51_set::{create_nip51_set, Nip51Set, Nip51SetCache};
|
|
||||||
pub use note::{
|
pub use note::{
|
||||||
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
|
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
|
||||||
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
|
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
|
||||||
@@ -82,7 +80,6 @@ pub use storage::{AccountStorage, DataPath, DataPathType, Directory};
|
|||||||
pub use style::NotedeckTextStyle;
|
pub use style::NotedeckTextStyle;
|
||||||
pub use theme::ColorTheme;
|
pub use theme::ColorTheme;
|
||||||
pub use time::time_ago_since;
|
pub use time::time_ago_since;
|
||||||
pub use time::time_format;
|
|
||||||
pub use timecache::TimeCached;
|
pub use timecache::TimeCached;
|
||||||
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
|
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
|
||||||
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
|
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
|
||||||
|
|||||||
@@ -3,18 +3,14 @@ use std::{
|
|||||||
time::{Instant, SystemTime},
|
time::{Instant, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::media::AnimationMode;
|
|
||||||
use crate::Animation;
|
|
||||||
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
|
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
|
||||||
use egui::TextureHandle;
|
use egui::TextureHandle;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub fn ensure_latest_texture_from_cache(
|
pub fn ensure_latest_texture_from_cache(
|
||||||
ui: &egui::Ui,
|
ui: &egui::Ui,
|
||||||
url: &str,
|
url: &str,
|
||||||
gifs: &mut GifStateMap,
|
gifs: &mut GifStateMap,
|
||||||
textures: &mut TexturesCache,
|
textures: &mut TexturesCache,
|
||||||
animation_mode: AnimationMode,
|
|
||||||
) -> Option<TextureHandle> {
|
) -> Option<TextureHandle> {
|
||||||
let tstate = textures.cache.get_mut(url)?;
|
let tstate = textures.cache.get_mut(url)?;
|
||||||
|
|
||||||
@@ -22,102 +18,7 @@ pub fn ensure_latest_texture_from_cache(
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(ensure_latest_texture(ui, url, gifs, img, animation_mode))
|
Some(ensure_latest_texture(ui, url, gifs, img))
|
||||||
}
|
|
||||||
|
|
||||||
struct ProcessedGifFrame {
|
|
||||||
texture: TextureHandle,
|
|
||||||
maybe_new_state: Option<GifState>,
|
|
||||||
repaint_at: Option<SystemTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process a gif state frame, and optionally present a new
|
|
||||||
/// state and when to repaint it
|
|
||||||
fn process_gif_frame(
|
|
||||||
animation: &Animation,
|
|
||||||
frame_state: Option<&GifState>,
|
|
||||||
animation_mode: AnimationMode,
|
|
||||||
) -> ProcessedGifFrame {
|
|
||||||
let now = Instant::now();
|
|
||||||
|
|
||||||
match frame_state {
|
|
||||||
Some(prev_state) => {
|
|
||||||
let should_advance = animation_mode.can_animate()
|
|
||||||
&& (now - prev_state.last_frame_rendered >= prev_state.last_frame_duration);
|
|
||||||
|
|
||||||
if should_advance {
|
|
||||||
let maybe_new_index = if animation.receiver.is_some()
|
|
||||||
|| prev_state.last_frame_index < animation.num_frames() - 1
|
|
||||||
{
|
|
||||||
prev_state.last_frame_index + 1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
match animation.get_frame(maybe_new_index) {
|
|
||||||
Some(frame) => {
|
|
||||||
let next_frame_time = match animation_mode {
|
|
||||||
AnimationMode::Continuous { fps } => match fps {
|
|
||||||
Some(fps) => {
|
|
||||||
let max_delay_ms = Duration::from_millis((1000.0 / fps) as u64);
|
|
||||||
SystemTime::now().checked_add(frame.delay.max(max_delay_ms))
|
|
||||||
}
|
|
||||||
None => SystemTime::now().checked_add(frame.delay),
|
|
||||||
},
|
|
||||||
|
|
||||||
AnimationMode::NoAnimation | AnimationMode::Reactive => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
ProcessedGifFrame {
|
|
||||||
texture: frame.texture.clone(),
|
|
||||||
maybe_new_state: Some(GifState {
|
|
||||||
last_frame_rendered: now,
|
|
||||||
last_frame_duration: frame.delay,
|
|
||||||
next_frame_time,
|
|
||||||
last_frame_index: maybe_new_index,
|
|
||||||
}),
|
|
||||||
repaint_at: next_frame_time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let (texture, maybe_new_state) =
|
|
||||||
match animation.get_frame(prev_state.last_frame_index) {
|
|
||||||
Some(frame) => (frame.texture.clone(), None),
|
|
||||||
None => (animation.first_frame.texture.clone(), None),
|
|
||||||
};
|
|
||||||
|
|
||||||
ProcessedGifFrame {
|
|
||||||
texture,
|
|
||||||
maybe_new_state,
|
|
||||||
repaint_at: prev_state.next_frame_time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let (texture, maybe_new_state) =
|
|
||||||
match animation.get_frame(prev_state.last_frame_index) {
|
|
||||||
Some(frame) => (frame.texture.clone(), None),
|
|
||||||
None => (animation.first_frame.texture.clone(), None),
|
|
||||||
};
|
|
||||||
|
|
||||||
ProcessedGifFrame {
|
|
||||||
texture,
|
|
||||||
maybe_new_state,
|
|
||||||
repaint_at: prev_state.next_frame_time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => ProcessedGifFrame {
|
|
||||||
texture: animation.first_frame.texture.clone(),
|
|
||||||
maybe_new_state: Some(GifState {
|
|
||||||
last_frame_rendered: now,
|
|
||||||
last_frame_duration: animation.first_frame.delay,
|
|
||||||
next_frame_time: None,
|
|
||||||
last_frame_index: 0,
|
|
||||||
}),
|
|
||||||
repaint_at: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_latest_texture(
|
pub fn ensure_latest_texture(
|
||||||
@@ -125,7 +26,6 @@ pub fn ensure_latest_texture(
|
|||||||
url: &str,
|
url: &str,
|
||||||
gifs: &mut GifStateMap,
|
gifs: &mut GifStateMap,
|
||||||
img: &mut TexturedImage,
|
img: &mut TexturedImage,
|
||||||
animation_mode: AnimationMode,
|
|
||||||
) -> TextureHandle {
|
) -> TextureHandle {
|
||||||
match img {
|
match img {
|
||||||
TexturedImage::Static(handle) => handle.clone(),
|
TexturedImage::Static(handle) => handle.clone(),
|
||||||
@@ -145,20 +45,77 @@ pub fn ensure_latest_texture(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
|
let now = Instant::now();
|
||||||
|
let (texture, maybe_new_state, request_next_repaint) = match gifs.get(url) {
|
||||||
|
Some(prev_state) => {
|
||||||
|
let should_advance =
|
||||||
|
now - prev_state.last_frame_rendered >= prev_state.last_frame_duration;
|
||||||
|
|
||||||
if let Some(new_state) = next_state.maybe_new_state {
|
if should_advance {
|
||||||
|
let maybe_new_index = if animation.receiver.is_some()
|
||||||
|
|| prev_state.last_frame_index < animation.num_frames() - 1
|
||||||
|
{
|
||||||
|
prev_state.last_frame_index + 1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
match animation.get_frame(maybe_new_index) {
|
||||||
|
Some(frame) => {
|
||||||
|
let next_frame_time = SystemTime::now().checked_add(frame.delay);
|
||||||
|
(
|
||||||
|
&frame.texture,
|
||||||
|
Some(GifState {
|
||||||
|
last_frame_rendered: now,
|
||||||
|
last_frame_duration: frame.delay,
|
||||||
|
next_frame_time,
|
||||||
|
last_frame_index: maybe_new_index,
|
||||||
|
}),
|
||||||
|
next_frame_time,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let (tex, state) =
|
||||||
|
match animation.get_frame(prev_state.last_frame_index) {
|
||||||
|
Some(frame) => (&frame.texture, None),
|
||||||
|
None => (&animation.first_frame.texture, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
(tex, state, prev_state.next_frame_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (tex, state) = match animation.get_frame(prev_state.last_frame_index) {
|
||||||
|
Some(frame) => (&frame.texture, None),
|
||||||
|
None => (&animation.first_frame.texture, None),
|
||||||
|
};
|
||||||
|
(tex, state, prev_state.next_frame_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
&animation.first_frame.texture,
|
||||||
|
Some(GifState {
|
||||||
|
last_frame_rendered: now,
|
||||||
|
last_frame_duration: animation.first_frame.delay,
|
||||||
|
next_frame_time: None,
|
||||||
|
last_frame_index: 0,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(new_state) = maybe_new_state {
|
||||||
gifs.insert(url.to_owned(), new_state);
|
gifs.insert(url.to_owned(), new_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(repaint) = next_state.repaint_at {
|
if let Some(req) = request_next_repaint {
|
||||||
tracing::trace!("requesting repaint for {url} after {repaint:?}");
|
tracing::trace!("requesting repaint for {url} after {req:?}");
|
||||||
if let Ok(dur) = repaint.duration_since(SystemTime::now()) {
|
// 24fps for gif is fine
|
||||||
ui.ctx().request_repaint_after(dur);
|
ui.ctx()
|
||||||
}
|
.request_repaint_after(std::time::Duration::from_millis(41));
|
||||||
}
|
}
|
||||||
|
|
||||||
next_state.texture
|
texture.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,21 +12,3 @@ pub use blur::{
|
|||||||
};
|
};
|
||||||
pub use images::ImageType;
|
pub use images::ImageType;
|
||||||
pub use renderable::RenderableMedia;
|
pub use renderable::RenderableMedia;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum AnimationMode {
|
|
||||||
/// Only render when scrolling, network activity, etc
|
|
||||||
Reactive,
|
|
||||||
|
|
||||||
/// Continuous with an optional target fps
|
|
||||||
Continuous { fps: Option<f32> },
|
|
||||||
|
|
||||||
/// Disable animation
|
|
||||||
NoAnimation,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnimationMode {
|
|
||||||
pub fn can_animate(&self) -> bool {
|
|
||||||
!matches!(self, Self::NoAnimation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -80,8 +80,4 @@ impl Muted {
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_pk_muted(&self, pk: &[u8; 32]) -> bool {
|
|
||||||
self.pubkeys.contains(pk)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
use enostr::{Pubkey, RelayPool};
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use nostrdb::{Filter, Ndb, Note, Transaction};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{UnifiedSubscription, UnknownIds};
|
|
||||||
|
|
||||||
/// Keeps track of most recent NIP-51 sets
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Nip51SetCache {
|
|
||||||
pub sub: UnifiedSubscription,
|
|
||||||
cached_notes: IndexMap<PackId, Nip51Set>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type PackId = String;
|
|
||||||
|
|
||||||
impl Nip51SetCache {
|
|
||||||
pub fn new(
|
|
||||||
pool: &mut RelayPool,
|
|
||||||
ndb: &Ndb,
|
|
||||||
txn: &Transaction,
|
|
||||||
unknown_ids: &mut UnknownIds,
|
|
||||||
nip51_set_filter: Vec<Filter>,
|
|
||||||
) -> Option<Self> {
|
|
||||||
let subid = Uuid::new_v4().to_string();
|
|
||||||
let mut cached_notes = IndexMap::default();
|
|
||||||
|
|
||||||
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())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(notes) = notes {
|
|
||||||
add(notes, &mut cached_notes, ndb, txn, unknown_ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sub = match ndb.subscribe(&nip51_set_filter) {
|
|
||||||
Ok(sub) => sub,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Could not ndb subscribe: {e}");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pool.subscribe(subid.clone(), nip51_set_filter);
|
|
||||||
|
|
||||||
Some(Self {
|
|
||||||
sub: UnifiedSubscription {
|
|
||||||
local: sub,
|
|
||||||
remote: subid,
|
|
||||||
},
|
|
||||||
cached_notes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll_for_notes(&mut self, ndb: &Ndb, unknown_ids: &mut UnknownIds) {
|
|
||||||
let new_notes = ndb.poll_for_notes(self.sub.local, 5);
|
|
||||||
|
|
||||||
if new_notes.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
|
||||||
let notes: Vec<Note> = new_notes
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|new_note_key| ndb.get_note_by_key(&txn, new_note_key).ok())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
add(notes, &mut self.cached_notes, ndb, &txn, unknown_ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> {
|
|
||||||
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(
|
|
||||||
notes: Vec<Note>,
|
|
||||||
cache: &mut IndexMap<PackId, Nip51Set>,
|
|
||||||
ndb: &Ndb,
|
|
||||||
txn: &Transaction,
|
|
||||||
unknown_ids: &mut UnknownIds,
|
|
||||||
) {
|
|
||||||
for note in notes {
|
|
||||||
let Some(new_pack) = create_nip51_set(note) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(cur_cached) = cache.get(&new_pack.identifier) {
|
|
||||||
if new_pack.created_at <= cur_cached.created_at {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for pk in &new_pack.pks {
|
|
||||||
unknown_ids.add_pubkey_if_missing(ndb, txn, pk);
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.insert(new_pack.identifier.clone(), new_pack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_nip51_set(note: Note) -> Option<Nip51Set> {
|
|
||||||
let mut identifier = None;
|
|
||||||
let mut title = None;
|
|
||||||
let mut image = None;
|
|
||||||
let mut description = None;
|
|
||||||
let mut pks = Vec::new();
|
|
||||||
|
|
||||||
for tag in note.tags() {
|
|
||||||
if tag.count() < 2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(first) = tag.get_str(0) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
match first {
|
|
||||||
"p" => {
|
|
||||||
let Some(pk) = tag.get_id(1) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
pks.push(Pubkey::new(*pk));
|
|
||||||
}
|
|
||||||
"d" => {
|
|
||||||
let Some(id) = tag.get_str(1) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
identifier = Some(id.to_owned());
|
|
||||||
}
|
|
||||||
"image" => {
|
|
||||||
let Some(cur_img) = tag.get_str(1) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
image = Some(cur_img.to_owned());
|
|
||||||
}
|
|
||||||
"title" => {
|
|
||||||
let Some(cur_title) = tag.get_str(1) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
title = Some(cur_title.to_owned());
|
|
||||||
}
|
|
||||||
"description" => {
|
|
||||||
let Some(cur_desc) = tag.get_str(1) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
description = Some(cur_desc.to_owned());
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let identifier = identifier?;
|
|
||||||
|
|
||||||
Some(Nip51Set {
|
|
||||||
identifier,
|
|
||||||
title,
|
|
||||||
image,
|
|
||||||
description,
|
|
||||||
pks,
|
|
||||||
created_at: note.created_at(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NIP-51 Set. Read only (do not use for writing)
|
|
||||||
pub struct Nip51Set {
|
|
||||||
pub identifier: String, // 'd' tag
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub image: Option<String>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub pks: Vec<Pubkey>,
|
|
||||||
created_at: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for Nip51Set {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("Nip51Set")
|
|
||||||
.field("identifier", &self.identifier)
|
|
||||||
.field("title", &self.title)
|
|
||||||
.field("image", &self.image)
|
|
||||||
.field("description", &self.description)
|
|
||||||
.field("pks", &self.pks.len())
|
|
||||||
.field("created_at", &self.created_at)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,11 +24,7 @@ pub enum NoteAction {
|
|||||||
Profile(Pubkey),
|
Profile(Pubkey),
|
||||||
|
|
||||||
/// User has clicked a note link
|
/// User has clicked a note link
|
||||||
Note {
|
Note { note_id: NoteId, preview: bool },
|
||||||
note_id: NoteId,
|
|
||||||
preview: bool,
|
|
||||||
scroll_offset: f32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// User has selected some context option
|
/// User has selected some context option
|
||||||
Context(ContextSelection),
|
Context(ContextSelection),
|
||||||
@@ -48,7 +44,6 @@ impl NoteAction {
|
|||||||
NoteAction::Note {
|
NoteAction::Note {
|
||||||
note_id: id,
|
note_id: id,
|
||||||
preview: false,
|
preview: false,
|
||||||
scroll_offset: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ bitflags! {
|
|||||||
/// Use keystore?
|
/// Use keystore?
|
||||||
const UseKeystore = 1 << 4;
|
const UseKeystore = 1 << 4;
|
||||||
|
|
||||||
|
/// Show client on notes?
|
||||||
|
const ShowClient = 1 << 5;
|
||||||
|
|
||||||
/// Simulate is_compiled_as_mobile ?
|
/// Simulate is_compiled_as_mobile ?
|
||||||
const Mobile = 1 << 6;
|
const Mobile = 1 << 6;
|
||||||
|
|
||||||
// ===== Feature Flags ======
|
// ===== Feature Flags ======
|
||||||
/// Is notebook enabled?
|
/// Is notebook enabled?
|
||||||
const FeatureNotebook = 1 << 32;
|
const FeatureNotebook = 1 << 32;
|
||||||
|
|
||||||
/// Is clndash enabled?
|
|
||||||
const FeatureClnDash = 1 << 33;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
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, error, info};
|
use tracing::debug;
|
||||||
|
|
||||||
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);
|
||||||
@@ -25,7 +16,7 @@ pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHei
|
|||||||
debug!("updating virtual keyboard height {}", height);
|
debug!("updating virtual keyboard height {}", height);
|
||||||
|
|
||||||
// Convert and store atomically
|
// Convert and store atomically
|
||||||
KEYBOARD_HEIGHT.store(height.max(0), Ordering::SeqCst);
|
KEYBOARD_HEIGHT.store(height, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the current Android virtual keyboard height. Useful for transforming
|
/// Gets the current Android virtual keyboard height. Useful for transforming
|
||||||
@@ -33,80 +24,3 @@ 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(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
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,39 +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;
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub fn virtual_keyboard_height(virt: bool) -> i32 {
|
pub fn virtual_keyboard_height() -> i32 {
|
||||||
if virt {
|
android::virtual_keyboard_height()
|
||||||
VIRT_HEIGHT
|
|
||||||
} else {
|
|
||||||
android::virtual_keyboard_height()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn virtual_keyboard_height(virt: bool) -> i32 {
|
pub fn virtual_keyboard_height() -> i32 {
|
||||||
if virt {
|
0
|
||||||
VIRT_HEIGHT
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn virtual_keyboard_rect(ui: &egui::Ui, virt: bool) -> Option<egui::Rect> {
|
|
||||||
let height = virtual_keyboard_height(virt);
|
|
||||||
if height <= 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let screen_rect = ui.ctx().screen_rect();
|
|
||||||
let min = egui::Pos2::new(0.0, screen_rect.max.y - height as f32);
|
|
||||||
Some(egui::Rect::from_min_max(min, screen_rect.max))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ impl PartialEq for RelaySpec {
|
|||||||
|
|
||||||
impl Eq for RelaySpec {}
|
impl Eq for RelaySpec {}
|
||||||
|
|
||||||
#[allow(clippy::non_canonical_partial_ord_impl)]
|
|
||||||
impl PartialOrd for RelaySpec {
|
impl PartialOrd for RelaySpec {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.url.cmp(&other.url))
|
Some(self.url.cmp(&other.url))
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ pub fn setup_egui_context(
|
|||||||
zoom_factor: f32,
|
zoom_factor: f32,
|
||||||
) {
|
) {
|
||||||
let is_mobile = options.contains(NotedeckOptions::Mobile) || crate::ui::is_compiled_as_mobile();
|
let is_mobile = options.contains(NotedeckOptions::Mobile) || crate::ui::is_compiled_as_mobile();
|
||||||
let is_oled = crate::ui::is_oled(is_mobile);
|
|
||||||
|
let is_oled = crate::ui::is_oled();
|
||||||
|
|
||||||
ctx.options_mut(|o| {
|
ctx.options_mut(|o| {
|
||||||
tracing::info!("Loaded theme {:?} from disk", theme);
|
tracing::info!("Loaded theme {:?} from disk", theme);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::{tr, Localization};
|
use crate::{tr, Localization};
|
||||||
use chrono::DateTime;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
// Time duration constants in seconds
|
// Time duration constants in seconds
|
||||||
@@ -84,14 +83,6 @@ fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time_format(_i18n: &mut Localization, timestamp: u64) -> String {
|
|
||||||
// TODO: format this using the selected locale
|
|
||||||
DateTime::from_timestamp(timestamp as i64, 0)
|
|
||||||
.unwrap()
|
|
||||||
.format("%l:%M %p %b %d, %Y")
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
|
pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ pub fn is_narrow(ctx: &egui::Context) -> bool {
|
|||||||
screen_size.x < NARROW_SCREEN_WIDTH
|
screen_size.x < NARROW_SCREEN_WIDTH
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_oled(is_mobile_override: bool) -> bool {
|
pub fn is_oled() -> bool {
|
||||||
is_mobile_override || is_compiled_as_mobile()
|
is_compiled_as_mobile()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -195,13 +195,13 @@ impl UnknownIds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &[u8; 32]) {
|
pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &Pubkey) {
|
||||||
// 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::new(*pubkey));
|
let unknown_id = UnknownId::Pubkey(*pubkey);
|
||||||
if self.ids.contains_key(&unknown_id) {
|
if self.ids.contains_key(&unknown_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,9 +238,7 @@ impl SupportedMimeType {
|
|||||||
{
|
{
|
||||||
Ok(Self { mime })
|
Ok(Self { mime })
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Generic(
|
Err(Error::Generic("Unsupported mime type".to_owned()))
|
||||||
format!("{extension} Unsupported mime type",),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl UserAccount {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keypair(&self) -> KeypairUnowned<'_> {
|
pub fn keypair(&self) -> KeypairUnowned {
|
||||||
KeypairUnowned {
|
KeypairUnowned {
|
||||||
pubkey: &self.key.pubkey,
|
pubkey: &self.key.pubkey,
|
||||||
secret_key: self.key.secret_key.as_ref(),
|
secret_key: self.key.secret_key.as_ref(),
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ license = "GPLv3"
|
|||||||
description = "The nostr browser"
|
description = "The nostr browser"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = { workspace = true }
|
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
egui_tabs = { workspace = true }
|
egui_tabs = { workspace = true }
|
||||||
egui_extras = { workspace = true }
|
egui_extras = { workspace = true }
|
||||||
@@ -18,7 +17,6 @@ notedeck_columns = { workspace = true }
|
|||||||
notedeck_ui = { workspace = true }
|
notedeck_ui = { workspace = true }
|
||||||
notedeck_dave = { workspace = true }
|
notedeck_dave = { workspace = true }
|
||||||
notedeck_notebook = { workspace = true }
|
notedeck_notebook = { workspace = true }
|
||||||
notedeck_clndash = { workspace = true }
|
|
||||||
notedeck = { workspace = true }
|
notedeck = { workspace = true }
|
||||||
nostrdb = { workspace = true }
|
nostrdb = { workspace = true }
|
||||||
puffin = { workspace = true, optional = true }
|
puffin = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -8,9 +8,8 @@
|
|||||||
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
|
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode|fontScale|smallestScreenSize"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
>
|
>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -38,4 +37,4 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
</manifest>
|
</manifest>
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
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
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
-198
@@ -1,18 +1,13 @@
|
|||||||
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;
|
||||||
@@ -20,38 +15,52 @@ 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 final int REQUEST_CODE_PICK_FILE = 420;
|
static {
|
||||||
|
System.loadLibrary("notedeck_chrome");
|
||||||
|
}
|
||||||
|
|
||||||
private native void nativeOnFilePickedFailed(String uri, String e);
|
private native void nativeOnKeyboardHeightChanged(int height);
|
||||||
private native void nativeOnFilePickedWithContent(Object[] uri_info, byte[] content);
|
private KeyboardHeightHelper keyboardHelper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
// Shrink view so it does not get covered by insets.
|
||||||
|
|
||||||
public void openFilePicker() {
|
setupInsets();
|
||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
//setupFullscreen()
|
||||||
intent.setType("*/*");
|
keyboardHelper = new KeyboardHeightHelper(this);
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
super.onCreate(savedInstanceState);
|
||||||
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() {
|
||||||
|
|
||||||
// NOTE(jb55): This is needed for keyboard visibility. Without this the
|
|
||||||
// window still gets the right insets, but they’re consumed before they
|
|
||||||
// reach the NDK side.
|
|
||||||
//WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
|
||||||
|
|
||||||
// NOTE(jb55): This is needed for keyboard visibility. If the bars are
|
|
||||||
// permanently gone, Android routes the keyboard over the GL surface and
|
|
||||||
// doesn’t change insets.
|
|
||||||
//WindowInsetsControllerCompat ic = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
|
||||||
//ic.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
||||||
|
|
||||||
View content = getContent();
|
View content = getContent();
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
||||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
@@ -63,176 +72,38 @@ public class MainActivity extends GameActivity {
|
|||||||
mlp.rightMargin = insets.right;
|
mlp.rightMargin = insets.right;
|
||||||
v.setLayoutParams(mlp);
|
v.setLayoutParams(mlp);
|
||||||
|
|
||||||
return windowInsets;
|
return WindowInsetsCompat.CONSUMED;
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
keyboardHelper.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
keyboardHelper.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
keyboardHelper.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processSelectedFile(Uri uri) {
|
@Override
|
||||||
try {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
nativeOnFilePickedWithContent(this.getUriInfo(uri), readUriContent(uri));
|
// Offset the location so it fits the view with margins caused by insets.
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("MainActivity", "Error processing file: " + uri.toString(), e);
|
|
||||||
|
|
||||||
nativeOnFilePickedFailed(uri.toString(), e.toString());
|
int[] location = new int[2];
|
||||||
}
|
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,15 +8,16 @@ use notedeck::Notedeck;
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn android_main(android_app: AndroidApp) {
|
pub async fn android_main(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",
|
||||||
"egui=debug,egui-winit=debug,winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
|
"egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
|
||||||
);
|
);
|
||||||
|
|
||||||
//std::env::set_var(
|
//std::env::set_var(
|
||||||
@@ -41,7 +42,7 @@ pub async fn android_main(android_app: AndroidApp) {
|
|||||||
.with(fmt_layer)
|
.with(fmt_layer)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let path = android_app.internal_data_path().expect("data path");
|
let path = 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()
|
||||||
@@ -54,18 +55,16 @@ pub async fn android_main(android_app: AndroidApp) {
|
|||||||
// builder.with_android_app(app_clone_for_event_loop);
|
// builder.with_android_app(app_clone_for_event_loop);
|
||||||
//}));
|
//}));
|
||||||
|
|
||||||
options.android_app = Some(android_app.clone());
|
options.android_app = Some(app.clone());
|
||||||
|
|
||||||
let app_args = get_app_args();
|
let app_args = get_app_args(app);
|
||||||
|
|
||||||
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(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 +103,7 @@ Using internal storage would be better but it seems hard to get the config file
|
|||||||
the device ...
|
the device ...
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fn get_app_args() -> Vec<String> {
|
fn get_app_args(_app: AndroidApp) -> Vec<String> {
|
||||||
vec!["argv0-placeholder".to_string()]
|
vec!["argv0-placeholder".to_string()]
|
||||||
/*
|
/*
|
||||||
use serde_json::value;
|
use serde_json::value;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use notedeck::{AppAction, AppContext};
|
use notedeck::{AppAction, AppContext};
|
||||||
use notedeck_clndash::ClnDash;
|
|
||||||
use notedeck_columns::Damus;
|
use notedeck_columns::Damus;
|
||||||
use notedeck_dave::Dave;
|
use notedeck_dave::Dave;
|
||||||
use notedeck_notebook::Notebook;
|
use notedeck_notebook::Notebook;
|
||||||
@@ -9,7 +8,6 @@ pub enum NotedeckApp {
|
|||||||
Dave(Box<Dave>),
|
Dave(Box<Dave>),
|
||||||
Columns(Box<Damus>),
|
Columns(Box<Damus>),
|
||||||
Notebook(Box<Notebook>),
|
Notebook(Box<Notebook>),
|
||||||
ClnDash(Box<ClnDash>),
|
|
||||||
Other(Box<dyn notedeck::App>),
|
Other(Box<dyn notedeck::App>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +17,6 @@ impl notedeck::App for NotedeckApp {
|
|||||||
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
|
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
|
||||||
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
|
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
|
||||||
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
|
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
|
||||||
NotedeckApp::ClnDash(clndash) => clndash.update(ctx, ui),
|
|
||||||
NotedeckApp::Other(other) => other.update(ctx, ui),
|
NotedeckApp::Other(other) => other.update(ctx, ui),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,6 @@ mod android;
|
|||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod chrome;
|
mod chrome;
|
||||||
mod options;
|
|
||||||
|
|
||||||
pub use app::NotedeckApp;
|
pub use app::NotedeckApp;
|
||||||
pub use chrome::Chrome;
|
pub use chrome::Chrome;
|
||||||
pub use options::ChromeOptions;
|
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ 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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
use bitflags::bitflags;
|
|
||||||
|
|
||||||
bitflags! {
|
|
||||||
#[repr(transparent)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ChromeOptions: u64 {
|
|
||||||
/// Is the chrome currently open?
|
|
||||||
const NoOptions = 0;
|
|
||||||
|
|
||||||
/// Is the chrome currently open?
|
|
||||||
const IsOpen = 1 << 0;
|
|
||||||
|
|
||||||
/// Are we simulating a virtual keyboard? This is mostly for debugging
|
|
||||||
/// if we are too lazy to open up a real mobile device with soft
|
|
||||||
/// keyboard
|
|
||||||
const VirtualKeyboard = 1 << 1;
|
|
||||||
|
|
||||||
/// Are we showing the memory debug window?
|
|
||||||
const MemoryDebug = 1 << 2;
|
|
||||||
|
|
||||||
/// Repaint debug
|
|
||||||
const RepaintDebug = 1 << 3;
|
|
||||||
|
|
||||||
/// We need soft keyboard visibility
|
|
||||||
const KeyboardVisibility = 1 << 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ChromeOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
let mut options = ChromeOptions::NoOptions;
|
|
||||||
options.set(
|
|
||||||
ChromeOptions::IsOpen,
|
|
||||||
!notedeck::ui::is_compiled_as_mobile(),
|
|
||||||
);
|
|
||||||
options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "notedeck_clndash"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
egui = { workspace = true }
|
|
||||||
notedeck = { workspace = true }
|
|
||||||
#notedeck_ui = { workspace = true }
|
|
||||||
eframe = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
egui_extras = { workspace = true }
|
|
||||||
lightning-invoice = { workspace = true }
|
|
||||||
hex = { workspace = true }
|
|
||||||
nostrdb = { workspace = true }
|
|
||||||
notedeck_ui = { workspace = true }
|
|
||||||
|
|
||||||
lnsocket = "0.5.1"
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# ⚡ clndash
|
|
||||||
|
|
||||||
Your Core Lightning dashboard, **without the server nonsense**.
|
|
||||||
|
|
||||||
clndash is a weird little experiment: a [notedeck][notedeck] app that talks to your node **directly over the Lightning Network** using [lnsocket][lnsocket] + [Commando][commando] RPCs.
|
|
||||||
|
|
||||||
No HTTP. No nginx. No VPS.
|
|
||||||
Just open clndash, point it at your node, and boom — you’re in.
|
|
||||||
|
|
||||||
<img src="https://jb55.com/s/476285c50d06c3ce.png" width="50%" />
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤯 Why?
|
|
||||||
|
|
||||||
Because sometimes you just want to *see your channels* and *check invoices* without SSH-ing into a box and typing `lightning-cli`.
|
|
||||||
|
|
||||||
And because LN is already a secure, encrypted connection layer — why not just use that?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔥 Features (as of today)
|
|
||||||
|
|
||||||
* **Plug-and-play LN connection** – powered by [lnsocket][lnsocket]
|
|
||||||
* **Commando RPC** – all dashboard data is fetched directly from your CLN node over Lightning
|
|
||||||
* **Channel overview** – total capacity, inbound/outbound liquidity, largest channel, and pretty bars
|
|
||||||
* **Invoices** – shows recent paid invoices (with zap previews if they came from Nostr)
|
|
||||||
* **No extra daemons** – you don’t need to run a server to use it
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🪄 Nostr Bonus
|
|
||||||
|
|
||||||
Because it’s a notedeck app, clndash can **render zaps** inline.
|
|
||||||
Yes, your Core Lightning dashboard can now show you when someone on Nostr just sent you sats and why.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗 Still Baking
|
|
||||||
|
|
||||||
This is WIP.
|
|
||||||
You’ll probably hit bugs. UI might be janky. Some features may vanish or suddenly mutate.
|
|
||||||
|
|
||||||
If you’re reading this and still excited — you’re the exact audience.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠 How to connect
|
|
||||||
|
|
||||||
1. Get your node’s **public address** (host\:port) and a **Commando rune** with safe permissions.
|
|
||||||
2. Set them as environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export CLNDASH_ID="03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71"
|
|
||||||
export CLNDASH_HOST="node.example.com:9735"
|
|
||||||
export CLNDASH_RUNE="your_rune_here"
|
|
||||||
```
|
|
||||||
3. Run clndash inside notedeck by calling notedeck with the `--clndash` argument.
|
|
||||||
4. Bask in the glow of real-time LN data over an LN connection.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
|
||||||
|
|
||||||
* Don’t give it a rune that can spend your funds.
|
|
||||||
* Don’t blame me if you break something — this is experimental territory.
|
|
||||||
* If it connects on the first try, buy yourself a beer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
If you like living on the edge of LN/Nostr tooling, you’ll like this.
|
|
||||||
If you don’t… you’ll probably want to wait a bit.
|
|
||||||
|
|
||||||
|
|
||||||
[commando]: https://docs.corelightning.org/reference/commando
|
|
||||||
[lnsocket]: https://github.com/jb55/lnsocket-rs
|
|
||||||
[notedeck]: https://github.com/damus-io/notedeck
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
use crate::event::LoadingState;
|
|
||||||
use crate::ui;
|
|
||||||
use egui::Color32;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct ListPeerChannel {
|
|
||||||
pub short_channel_id: String,
|
|
||||||
pub our_reserve_msat: i64,
|
|
||||||
pub to_us_msat: i64,
|
|
||||||
pub total_msat: i64,
|
|
||||||
pub their_reserve_msat: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Channel {
|
|
||||||
pub to_us: i64,
|
|
||||||
pub to_them: i64,
|
|
||||||
pub original: ListPeerChannel,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Channels {
|
|
||||||
pub max_total_msat: i64,
|
|
||||||
pub avail_in: i64,
|
|
||||||
pub avail_out: i64,
|
|
||||||
pub channels: Vec<Channel>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn channels_ui(ui: &mut egui::Ui, channels: &LoadingState<Channels, lnsocket::Error>) {
|
|
||||||
match channels {
|
|
||||||
LoadingState::Loaded(channels) => {
|
|
||||||
if channels.channels.is_empty() {
|
|
||||||
ui.label("no channels yet...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for channel in &channels.channels {
|
|
||||||
channel_ui(ui, channel, channels.max_total_msat);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.label(format!(
|
|
||||||
"available out {}",
|
|
||||||
ui::human_sat(channels.avail_out)
|
|
||||||
));
|
|
||||||
ui.label(format!("available in {}", ui::human_sat(channels.avail_in)));
|
|
||||||
}
|
|
||||||
LoadingState::Failed(err) => {
|
|
||||||
ui.label(format!("error fetching channels: {err}"));
|
|
||||||
}
|
|
||||||
LoadingState::Loading => {
|
|
||||||
ui.label("fetching channels...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn channel_ui(ui: &mut egui::Ui, c: &Channel, max_total_msat: i64) {
|
|
||||||
// ---------- numbers ----------
|
|
||||||
let short_channel_id = &c.original.short_channel_id;
|
|
||||||
|
|
||||||
let cap_ratio = (c.original.total_msat as f32 / max_total_msat.max(1) as f32).clamp(0.0, 1.0);
|
|
||||||
// Feel free to switch to log scaling if you have whales:
|
|
||||||
//let cap_ratio = ((c.original.total_msat as f32 + 1.0).log10() / (max_total_msat as f32 + 1.0).log10()).clamp(0.0, 1.0);
|
|
||||||
|
|
||||||
// ---------- colors & style ----------
|
|
||||||
let out_color = Color32::from_rgb(84, 69, 201); // blue
|
|
||||||
let in_color = Color32::from_rgb(158, 56, 180); // purple
|
|
||||||
|
|
||||||
// Thickness scales with capacity, but keeps a nice minimum
|
|
||||||
let thickness = 10.0 + cap_ratio * 22.0; // 10 → 32 px
|
|
||||||
let row_h = thickness + 14.0;
|
|
||||||
|
|
||||||
// ---------- layout ----------
|
|
||||||
let (rect, response) = ui.allocate_exact_size(
|
|
||||||
egui::vec2(ui.available_width(), row_h),
|
|
||||||
egui::Sense::hover(),
|
|
||||||
);
|
|
||||||
let painter = ui.painter_at(rect);
|
|
||||||
|
|
||||||
let bar_rect = egui::Rect::from_min_max(
|
|
||||||
egui::pos2(rect.left(), rect.center().y - thickness * 0.5),
|
|
||||||
egui::pos2(rect.right(), rect.center().y + thickness * 0.5),
|
|
||||||
);
|
|
||||||
let corner_radius = (thickness * 0.5) as u8;
|
|
||||||
let out_radius = egui::CornerRadius {
|
|
||||||
ne: 0,
|
|
||||||
nw: corner_radius,
|
|
||||||
sw: corner_radius,
|
|
||||||
se: 0,
|
|
||||||
};
|
|
||||||
let in_radius = egui::CornerRadius {
|
|
||||||
ne: corner_radius,
|
|
||||||
nw: 0,
|
|
||||||
sw: 0,
|
|
||||||
se: corner_radius,
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
painter.rect_filled(bar_rect, rounding, track_color);
|
|
||||||
painter.rect_stroke(bar_rect, rounding, track_stroke, egui::StrokeKind::Middle);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Split widths
|
|
||||||
let usable = (c.to_us + c.to_them).max(1) as f32;
|
|
||||||
let out_w = (bar_rect.width() * (c.to_us as f32 / usable)).round();
|
|
||||||
let split_x = bar_rect.left() + out_w;
|
|
||||||
|
|
||||||
// Outbound fill (left)
|
|
||||||
let out_rect = egui::Rect::from_min_max(bar_rect.min, egui::pos2(split_x, bar_rect.max.y));
|
|
||||||
if out_rect.width() > 0.5 {
|
|
||||||
painter.rect_filled(out_rect, out_radius, out_color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inbound fill (right)
|
|
||||||
let in_rect = egui::Rect::from_min_max(egui::pos2(split_x, bar_rect.min.y), bar_rect.max);
|
|
||||||
if in_rect.width() > 0.5 {
|
|
||||||
painter.rect_filled(in_rect, in_radius, in_color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tooltip
|
|
||||||
response.on_hover_text_at_pointer(format!(
|
|
||||||
"Channel ID {short_channel_id}\nOutbound (ours): {} sats\nInbound (theirs): {} sats\nCapacity: {} sats",
|
|
||||||
ui::human_sat(c.to_us),
|
|
||||||
ui::human_sat(c.to_them),
|
|
||||||
ui::human_sat(c.original.total_msat),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
use crate::channels::Channels;
|
|
||||||
use crate::invoice::Invoice;
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
pub enum ConnectionState {
|
|
||||||
Dead(String),
|
|
||||||
Connecting,
|
|
||||||
Active,
|
|
||||||
}
|
|
||||||
pub enum LoadingState<T, E> {
|
|
||||||
Loading,
|
|
||||||
Failed(E),
|
|
||||||
Loaded(T),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E> Default for LoadingState<T, E> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Loading
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E> LoadingState<T, E> {
|
|
||||||
fn _as_ref(&self) -> LoadingState<&T, &E> {
|
|
||||||
match self {
|
|
||||||
Self::Loading => LoadingState::<&T, &E>::Loading,
|
|
||||||
Self::Failed(err) => LoadingState::<&T, &E>::Failed(err),
|
|
||||||
Self::Loaded(t) => LoadingState::<&T, &E>::Loaded(t),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_result(res: Result<T, E>) -> LoadingState<T, E> {
|
|
||||||
match res {
|
|
||||||
Ok(r) => LoadingState::Loaded(r),
|
|
||||||
Err(err) => LoadingState::Failed(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
fn unwrap(self) -> T {
|
|
||||||
let Self::Loaded(t) = self else {
|
|
||||||
panic!("unwrap in LoadingState");
|
|
||||||
};
|
|
||||||
|
|
||||||
t
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
pub struct _WaitRequest {
|
|
||||||
pub indexname: String,
|
|
||||||
pub subsystem: String,
|
|
||||||
pub nextvalue: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum Request {
|
|
||||||
GetInfo,
|
|
||||||
ListPeerChannels,
|
|
||||||
PaidInvoices(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Responses from the socket
|
|
||||||
pub enum ClnResponse {
|
|
||||||
GetInfo(Value),
|
|
||||||
ListPeerChannels(Result<Channels, lnsocket::Error>),
|
|
||||||
PaidInvoices(Result<Vec<Invoice>, lnsocket::Error>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Event {
|
|
||||||
/// We lost the socket somehow
|
|
||||||
Ended {
|
|
||||||
reason: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
Connected,
|
|
||||||
|
|
||||||
Response(ClnResponse),
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
use crate::event::LoadingState;
|
|
||||||
use crate::ui;
|
|
||||||
use lightning_invoice::Bolt11Invoice;
|
|
||||||
use notedeck::AppContext;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Invoice {
|
|
||||||
pub lastpay_index: Option<u64>,
|
|
||||||
pub label: String,
|
|
||||||
pub bolt11: Bolt11Invoice,
|
|
||||||
pub payment_hash: String,
|
|
||||||
pub amount_msat: u64,
|
|
||||||
pub status: String,
|
|
||||||
pub description: String,
|
|
||||||
pub expires_at: u64,
|
|
||||||
pub created_index: u64,
|
|
||||||
pub updated_index: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn invoices_ui(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
invoice_notes: &HashMap<String, [u8; 32]>,
|
|
||||||
ctx: &mut AppContext,
|
|
||||||
invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>,
|
|
||||||
) {
|
|
||||||
match invoices {
|
|
||||||
LoadingState::Loading => {
|
|
||||||
ui.label("loading invoices...");
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingState::Failed(err) => {
|
|
||||||
ui.label(format!("failed to load invoices: {err}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingState::Loaded(invoices) => {
|
|
||||||
use egui_extras::{Column, TableBuilder};
|
|
||||||
|
|
||||||
TableBuilder::new(ui)
|
|
||||||
.column(Column::auto().resizable(true))
|
|
||||||
.column(Column::remainder())
|
|
||||||
.vscroll(false)
|
|
||||||
.header(20.0, |mut header| {
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.strong("description");
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.strong("amount");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.body(|mut body| {
|
|
||||||
for invoice in invoices {
|
|
||||||
body.row(20.0, |mut row| {
|
|
||||||
row.col(|ui| {
|
|
||||||
if invoice.description.starts_with("{") {
|
|
||||||
ui.label("Zap!").on_hover_ui_at_pointer(|ui| {
|
|
||||||
ui::note_hover_ui(ui, &invoice.label, ctx, invoice_notes);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.label(&invoice.description);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
row.col(|ui| match invoice.bolt11.amount_milli_satoshis() {
|
|
||||||
None => {
|
|
||||||
ui.label("any");
|
|
||||||
}
|
|
||||||
Some(amt) => {
|
|
||||||
ui.label(ui::human_verbose_sat(amt as i64));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
use crate::channels::Channel;
|
|
||||||
use crate::channels::Channels;
|
|
||||||
use crate::channels::ListPeerChannel;
|
|
||||||
use crate::event::ClnResponse;
|
|
||||||
use crate::event::ConnectionState;
|
|
||||||
use crate::event::Event;
|
|
||||||
use crate::event::LoadingState;
|
|
||||||
use crate::event::Request;
|
|
||||||
use crate::invoice::Invoice;
|
|
||||||
use crate::summary::Summary;
|
|
||||||
use crate::watch::fetch_paid_invoices;
|
|
||||||
|
|
||||||
use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
|
|
||||||
use lnsocket::{CommandoClient, LNSocket};
|
|
||||||
use nostrdb::Ndb;
|
|
||||||
use notedeck::{AppAction, AppContext};
|
|
||||||
use serde_json::json;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
|
|
||||||
|
|
||||||
mod channels;
|
|
||||||
mod event;
|
|
||||||
mod invoice;
|
|
||||||
mod summary;
|
|
||||||
mod ui;
|
|
||||||
mod watch;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ClnDash {
|
|
||||||
initialized: bool,
|
|
||||||
connection_state: ConnectionState,
|
|
||||||
summary: LoadingState<Summary, lnsocket::Error>,
|
|
||||||
get_info: LoadingState<String, lnsocket::Error>,
|
|
||||||
channels: LoadingState<Channels, lnsocket::Error>,
|
|
||||||
invoices: LoadingState<Vec<Invoice>, lnsocket::Error>,
|
|
||||||
channel: Option<CommChannel>,
|
|
||||||
last_summary: Option<Summary>,
|
|
||||||
// invoice label to zapreq id
|
|
||||||
invoice_zap_reqs: HashMap<String, [u8; 32]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct ZapReqId {
|
|
||||||
#[serde(with = "hex::serde")]
|
|
||||||
id: [u8; 32],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ConnectionState {
|
|
||||||
fn default() -> Self {
|
|
||||||
ConnectionState::Dead("uninitialized".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CommChannel {
|
|
||||||
req_tx: UnboundedSender<Request>,
|
|
||||||
event_rx: UnboundedReceiver<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl notedeck::App for ClnDash {
|
|
||||||
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
|
|
||||||
if !self.initialized {
|
|
||||||
self.connection_state = ConnectionState::Connecting;
|
|
||||||
|
|
||||||
self.setup_connection();
|
|
||||||
self.initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.process_events(ctx.ndb);
|
|
||||||
|
|
||||||
self.show(ui, ctx);
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClnDash {
|
|
||||||
fn show(&mut self, ui: &mut egui::Ui, ctx: &mut AppContext) {
|
|
||||||
egui::Frame::new()
|
|
||||||
.inner_margin(egui::Margin::same(20))
|
|
||||||
.show(ui, |ui| {
|
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
|
||||||
ui::connection_state_ui(ui, &self.connection_state);
|
|
||||||
crate::summary::summary_ui(ui, self.last_summary.as_ref(), &self.summary);
|
|
||||||
crate::invoice::invoices_ui(ui, &self.invoice_zap_reqs, ctx, &self.invoices);
|
|
||||||
crate::channels::channels_ui(ui, &self.channels);
|
|
||||||
crate::ui::get_info_ui(ui, &self.get_info);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_connection(&mut self) {
|
|
||||||
let (req_tx, mut req_rx) = unbounded_channel::<Request>();
|
|
||||||
let (event_tx, event_rx) = unbounded_channel::<Event>();
|
|
||||||
self.channel = Some(CommChannel { req_tx, event_rx });
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let key = SecretKey::new(&mut rand::thread_rng());
|
|
||||||
let their_pubkey = PublicKey::from_str(&std::env::var("CLNDASH_ID").unwrap_or(
|
|
||||||
"03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71".to_string(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let host = std::env::var("CLNDASH_HOST").unwrap_or("ln.damus.io:9735".to_string());
|
|
||||||
let lnsocket = match LNSocket::connect_and_init(key, their_pubkey, &host).await {
|
|
||||||
Err(err) => {
|
|
||||||
let _ = event_tx.send(Event::Ended {
|
|
||||||
reason: err.to_string(),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(lnsocket) => {
|
|
||||||
let _ = event_tx.send(Event::Connected);
|
|
||||||
lnsocket
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let rune = std::env::var("CLNDASH_RUNE").unwrap_or(
|
|
||||||
"Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8=".to_string(),
|
|
||||||
);
|
|
||||||
let commando = Arc::new(CommandoClient::spawn(lnsocket, &rune));
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match req_rx.recv().await {
|
|
||||||
None => {
|
|
||||||
let _ = event_tx.send(Event::Ended {
|
|
||||||
reason: "channel dead?".to_string(),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(req) => {
|
|
||||||
tracing::debug!("calling {req:?}");
|
|
||||||
match req {
|
|
||||||
Request::GetInfo => {
|
|
||||||
let event_tx = event_tx.clone();
|
|
||||||
let commando = commando.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
match commando.call("getinfo", json!({})).await {
|
|
||||||
Ok(v) => {
|
|
||||||
let _ = event_tx
|
|
||||||
.send(Event::Response(ClnResponse::GetInfo(v)));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("get_info error {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Request::PaidInvoices(n) => {
|
|
||||||
let event_tx = event_tx.clone();
|
|
||||||
let commando = commando.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let invoices = fetch_paid_invoices(commando, n).await;
|
|
||||||
let _ = event_tx
|
|
||||||
.send(Event::Response(ClnResponse::PaidInvoices(invoices)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Request::ListPeerChannels => {
|
|
||||||
let event_tx = event_tx.clone();
|
|
||||||
let commando = commando.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let peer_channels =
|
|
||||||
commando.call("listpeerchannels", json!({})).await;
|
|
||||||
let channels = peer_channels.map(|v| {
|
|
||||||
let peer_channels: Vec<ListPeerChannel> =
|
|
||||||
serde_json::from_value(v["channels"].clone()).unwrap();
|
|
||||||
to_channels(peer_channels)
|
|
||||||
});
|
|
||||||
let _ = event_tx.send(Event::Response(
|
|
||||||
ClnResponse::ListPeerChannels(channels),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_events(&mut self, ndb: &Ndb) {
|
|
||||||
let Some(channel) = &mut self.channel else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
while let Ok(event) = channel.event_rx.try_recv() {
|
|
||||||
match event {
|
|
||||||
Event::Ended { reason } => {
|
|
||||||
self.connection_state = ConnectionState::Dead(reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
Event::Connected => {
|
|
||||||
self.connection_state = ConnectionState::Active;
|
|
||||||
let _ = channel.req_tx.send(Request::GetInfo);
|
|
||||||
let _ = channel.req_tx.send(Request::ListPeerChannels);
|
|
||||||
let _ = channel.req_tx.send(Request::PaidInvoices(100));
|
|
||||||
}
|
|
||||||
|
|
||||||
Event::Response(resp) => match resp {
|
|
||||||
ClnResponse::ListPeerChannels(chans) => {
|
|
||||||
if let LoadingState::Loaded(prev) = &self.channels {
|
|
||||||
self.last_summary = Some(crate::summary::compute_summary(prev));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.summary = match &chans {
|
|
||||||
Ok(chans) => {
|
|
||||||
LoadingState::Loaded(crate::summary::compute_summary(chans))
|
|
||||||
}
|
|
||||||
Err(err) => LoadingState::Failed(err.clone()),
|
|
||||||
};
|
|
||||||
self.channels = LoadingState::from_result(chans);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClnResponse::GetInfo(value) => {
|
|
||||||
let res = serde_json::to_string_pretty(&value);
|
|
||||||
self.get_info =
|
|
||||||
LoadingState::from_result(res.map_err(|_| lnsocket::Error::Json));
|
|
||||||
}
|
|
||||||
|
|
||||||
ClnResponse::PaidInvoices(invoices) => {
|
|
||||||
// process zap requests
|
|
||||||
|
|
||||||
if let Ok(invoices) = &invoices {
|
|
||||||
for invoice in invoices {
|
|
||||||
let zap_req_id: Option<ZapReqId> =
|
|
||||||
serde_json::from_str(&invoice.description).ok();
|
|
||||||
if let Some(zap_req_id) = zap_req_id {
|
|
||||||
self.invoice_zap_reqs
|
|
||||||
.insert(invoice.label.clone(), zap_req_id.id);
|
|
||||||
let _ = ndb.process_event(&format!(
|
|
||||||
"[\"EVENT\",\"a\",{}]",
|
|
||||||
&invoice.description
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.invoices = LoadingState::from_result(invoices);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_channels(peer_channels: Vec<ListPeerChannel>) -> Channels {
|
|
||||||
let mut avail_out: i64 = 0;
|
|
||||||
let mut avail_in: i64 = 0;
|
|
||||||
let mut max_total_msat: i64 = 0;
|
|
||||||
|
|
||||||
let mut channels: Vec<Channel> = peer_channels
|
|
||||||
.into_iter()
|
|
||||||
.map(|c| {
|
|
||||||
let to_us = (c.to_us_msat - c.our_reserve_msat).max(0);
|
|
||||||
let to_them_raw = (c.total_msat - c.to_us_msat).max(0);
|
|
||||||
let to_them = (to_them_raw - c.their_reserve_msat).max(0);
|
|
||||||
|
|
||||||
avail_out += to_us;
|
|
||||||
avail_in += to_them;
|
|
||||||
if c.total_msat > max_total_msat {
|
|
||||||
max_total_msat = c.total_msat; // <-- max, not sum
|
|
||||||
}
|
|
||||||
|
|
||||||
Channel {
|
|
||||||
to_us,
|
|
||||||
to_them,
|
|
||||||
original: c,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
channels.sort_by(|a, b| {
|
|
||||||
let a_capacity = a.to_them + a.to_us;
|
|
||||||
let b_capacity = b.to_them + b.to_us;
|
|
||||||
|
|
||||||
a_capacity.partial_cmp(&b_capacity).unwrap().reverse()
|
|
||||||
});
|
|
||||||
|
|
||||||
Channels {
|
|
||||||
max_total_msat,
|
|
||||||
avail_out,
|
|
||||||
avail_in,
|
|
||||||
channels,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
use crate::channels::Channels;
|
|
||||||
use crate::event::LoadingState;
|
|
||||||
use crate::ui;
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct Summary {
|
|
||||||
pub total_msat: i64,
|
|
||||||
pub avail_out_msat: i64,
|
|
||||||
pub avail_in_msat: i64,
|
|
||||||
pub channel_count: usize,
|
|
||||||
pub largest_msat: i64,
|
|
||||||
pub outbound_pct: f32, // fraction of total capacity
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compute_summary(ch: &Channels) -> Summary {
|
|
||||||
let total_msat: i64 = ch.channels.iter().map(|c| c.original.total_msat).sum();
|
|
||||||
let largest_msat: i64 = ch
|
|
||||||
.channels
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.original.total_msat)
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0);
|
|
||||||
let outbound_pct = if total_msat > 0 {
|
|
||||||
ch.avail_out as f32 / total_msat as f32
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
Summary {
|
|
||||||
total_msat,
|
|
||||||
avail_out_msat: ch.avail_out,
|
|
||||||
avail_in_msat: ch.avail_in,
|
|
||||||
channel_count: ch.channels.len(),
|
|
||||||
largest_msat,
|
|
||||||
outbound_pct,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn summary_ui(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
last_summary: Option<&Summary>,
|
|
||||||
summary: &LoadingState<Summary, lnsocket::Error>,
|
|
||||||
) {
|
|
||||||
match summary {
|
|
||||||
LoadingState::Loading => {
|
|
||||||
ui.label("loading summary");
|
|
||||||
}
|
|
||||||
LoadingState::Failed(err) => {
|
|
||||||
ui.label(format!("Failed to get summary: {err}"));
|
|
||||||
}
|
|
||||||
LoadingState::Loaded(summary) => {
|
|
||||||
summary_cards_ui(ui, summary, last_summary);
|
|
||||||
ui.add_space(8.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn summary_cards_ui(ui: &mut egui::Ui, s: &Summary, prev: Option<&Summary>) {
|
|
||||||
let old = prev.cloned().unwrap_or_default();
|
|
||||||
let items: [(&str, String, Option<String>); 6] = [
|
|
||||||
(
|
|
||||||
"Total capacity",
|
|
||||||
ui::human_sat(s.total_msat),
|
|
||||||
prev.map(|_| ui::delta_str(s.total_msat, old.total_msat)),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Avail out",
|
|
||||||
ui::human_sat(s.avail_out_msat),
|
|
||||||
prev.map(|_| ui::delta_str(s.avail_out_msat, old.avail_out_msat)),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Avail in",
|
|
||||||
ui::human_sat(s.avail_in_msat),
|
|
||||||
prev.map(|_| ui::delta_str(s.avail_in_msat, old.avail_in_msat)),
|
|
||||||
),
|
|
||||||
("# Channels", s.channel_count.to_string(), None),
|
|
||||||
("Largest", ui::human_sat(s.largest_msat), None),
|
|
||||||
(
|
|
||||||
"Outbound %",
|
|
||||||
format!("{:.0}%", s.outbound_pct * 100.0),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- responsive columns ---
|
|
||||||
let min_card = 160.0;
|
|
||||||
let cols = ((ui.available_width() / min_card).floor() as usize).max(1);
|
|
||||||
|
|
||||||
egui::Grid::new("summary_grid")
|
|
||||||
.num_columns(cols)
|
|
||||||
.min_col_width(min_card)
|
|
||||||
.spacing(egui::vec2(8.0, 8.0))
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let items_len = items.len();
|
|
||||||
for (i, (t, v, d)) in items.into_iter().enumerate() {
|
|
||||||
card_cell(ui, t, v, d, min_card);
|
|
||||||
|
|
||||||
// End the row when we filled a row worth of cells
|
|
||||||
if (i + 1) % cols == 0 {
|
|
||||||
ui.end_row();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the last row wasn't full, close it anyway
|
|
||||||
if items_len % cols != 0 {
|
|
||||||
ui.end_row();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn card_cell(ui: &mut egui::Ui, title: &str, value: String, delta: Option<String>, min_card: f32) {
|
|
||||||
let weak = ui.visuals().weak_text_color();
|
|
||||||
egui::Frame::group(ui.style())
|
|
||||||
.fill(ui.visuals().extreme_bg_color)
|
|
||||||
.corner_radius(egui::CornerRadius::same(10))
|
|
||||||
.inner_margin(egui::Margin::same(10))
|
|
||||||
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.set_min_width(min_card);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add(
|
|
||||||
egui::Label::new(egui::RichText::new(title).small().color(weak))
|
|
||||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
|
||||||
);
|
|
||||||
ui.add_space(4.0);
|
|
||||||
ui.add(
|
|
||||||
egui::Label::new(egui::RichText::new(value).strong().size(18.0))
|
|
||||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
|
||||||
);
|
|
||||||
if let Some(d) = delta {
|
|
||||||
ui.add_space(2.0);
|
|
||||||
ui.add(
|
|
||||||
egui::Label::new(egui::RichText::new(d).small().color(weak))
|
|
||||||
.wrap_mode(egui::TextWrapMode::Wrap),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.set_min_height(20.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
use crate::event::ConnectionState;
|
|
||||||
use crate::event::LoadingState;
|
|
||||||
use egui::Color32;
|
|
||||||
use egui::Label;
|
|
||||||
use egui::RichText;
|
|
||||||
use egui::Widget;
|
|
||||||
use notedeck::AppContext;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub fn note_hover_ui(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
label: &str,
|
|
||||||
ctx: &mut AppContext,
|
|
||||||
invoice_notes: &HashMap<String, [u8; 32]>,
|
|
||||||
) -> Option<notedeck::NoteAction> {
|
|
||||||
let zap_req_id = invoice_notes.get(label)?;
|
|
||||||
|
|
||||||
let Ok(txn) = nostrdb::Transaction::new(ctx.ndb) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(zapreq_note) = ctx.ndb.get_note_by_id(&txn, zap_req_id) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
for tag in zapreq_note.tags() {
|
|
||||||
let Some("e") = tag.get_str(0) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(target_id) = tag.get_id(1) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(note) = ctx.ndb.get_note_by_id(&txn, target_id) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let author = ctx
|
|
||||||
.ndb
|
|
||||||
.get_profile_by_pubkey(&txn, zapreq_note.pubkey())
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// TODO(jb55): make this less horrible
|
|
||||||
let mut note_context = notedeck::NoteContext {
|
|
||||||
ndb: ctx.ndb,
|
|
||||||
accounts: ctx.accounts,
|
|
||||||
img_cache: ctx.img_cache,
|
|
||||||
note_cache: ctx.note_cache,
|
|
||||||
zaps: ctx.zaps,
|
|
||||||
pool: ctx.pool,
|
|
||||||
job_pool: ctx.job_pool,
|
|
||||||
unknown_ids: ctx.unknown_ids,
|
|
||||||
clipboard: ctx.clipboard,
|
|
||||||
i18n: ctx.i18n,
|
|
||||||
global_wallet: ctx.global_wallet,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut jobs = notedeck::JobsCache::default();
|
|
||||||
let options = notedeck_ui::NoteOptions::default();
|
|
||||||
|
|
||||||
notedeck_ui::ProfilePic::from_profile_or_default(note_context.img_cache, author.as_ref())
|
|
||||||
.ui(ui);
|
|
||||||
|
|
||||||
let nostr_name = notedeck::name::get_display_name(author.as_ref());
|
|
||||||
ui.label(format!("{} zapped you", nostr_name.name()));
|
|
||||||
|
|
||||||
return notedeck_ui::NoteView::new(&mut note_context, ¬e, options, &mut jobs)
|
|
||||||
.preview_style()
|
|
||||||
.hide_media(true)
|
|
||||||
.show(ui)
|
|
||||||
.action;
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_info_ui(ui: &mut egui::Ui, info: &LoadingState<String, lnsocket::Error>) {
|
|
||||||
ui.horizontal_wrapped(|ui| match info {
|
|
||||||
LoadingState::Loading => {}
|
|
||||||
LoadingState::Failed(err) => {
|
|
||||||
ui.label(format!("failed to fetch node info: {err}"));
|
|
||||||
}
|
|
||||||
LoadingState::Loaded(info) => {
|
|
||||||
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
|
|
||||||
match state {
|
|
||||||
ConnectionState::Active => {
|
|
||||||
ui.add(Label::new(RichText::new("Connected").color(Color32::GREEN)));
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionState::Connecting => {
|
|
||||||
ui.add(Label::new(
|
|
||||||
RichText::new("Connecting").color(Color32::YELLOW),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionState::Dead(reason) => {
|
|
||||||
ui.add(Label::new(
|
|
||||||
RichText::new(format!("Disconnected: {reason}")).color(Color32::RED),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- helper ----------
|
|
||||||
pub fn human_sat(msat: i64) -> String {
|
|
||||||
let sats = msat / 1000;
|
|
||||||
if sats >= 1_000_000 {
|
|
||||||
format!("{:.1}M", sats as f64 / 1_000_000.0)
|
|
||||||
} else if sats >= 1_000 {
|
|
||||||
format!("{:.1}k", sats as f64 / 1_000.0)
|
|
||||||
} else {
|
|
||||||
sats.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn human_verbose_sat(msat: i64) -> String {
|
|
||||||
if msat < 1_000 {
|
|
||||||
// less than 1 sat
|
|
||||||
format!("{msat} msat")
|
|
||||||
} else {
|
|
||||||
let sats = msat / 1_000;
|
|
||||||
if sats < 100_000_000 {
|
|
||||||
// less than 1 BTC
|
|
||||||
format!("{sats} sat")
|
|
||||||
} else {
|
|
||||||
let btc = sats / 100_000_000;
|
|
||||||
format!("{btc} BTC")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delta_str(new: i64, old: i64) -> String {
|
|
||||||
let d = new - old;
|
|
||||||
match d.cmp(&0) {
|
|
||||||
std::cmp::Ordering::Greater => format!("↑ {}", human_sat(d)),
|
|
||||||
std::cmp::Ordering::Less => format!("↓ {}", human_sat(-d)),
|
|
||||||
std::cmp::Ordering::Equal => "·".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
use crate::invoice::Invoice;
|
|
||||||
use lnsocket::CallOpts;
|
|
||||||
use lnsocket::CommandoClient;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct UpdatedInvoicesResponse {
|
|
||||||
updated: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PayIndexInvoices {
|
|
||||||
invoices: Vec<PayIndexScan>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PayIndexScan {
|
|
||||||
pay_index: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_lastpay_index(commando: Arc<CommandoClient>) -> Result<Option<u64>, lnsocket::Error> {
|
|
||||||
const PAGE: u64 = 250;
|
|
||||||
// 1) get the current updated tail
|
|
||||||
let created_value = commando
|
|
||||||
.call(
|
|
||||||
"wait",
|
|
||||||
json!({"subsystem":"invoices","indexname":"updated","nextvalue":0}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let response: UpdatedInvoicesResponse =
|
|
||||||
serde_json::from_value(created_value).map_err(|_| lnsocket::Error::Json)?;
|
|
||||||
|
|
||||||
// start our window at the tail
|
|
||||||
let mut start_at = response
|
|
||||||
.updated
|
|
||||||
.saturating_add(1) // +1 because we want max(1, updated - PAGE + 1)
|
|
||||||
.saturating_sub(PAGE)
|
|
||||||
.max(1);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// 2) fetch a window (indexed by "updated")
|
|
||||||
let val = commando
|
|
||||||
.call_with_opts(
|
|
||||||
"listinvoices",
|
|
||||||
json!({
|
|
||||||
"index": "updated",
|
|
||||||
"start": start_at,
|
|
||||||
"limit": PAGE,
|
|
||||||
}),
|
|
||||||
// only fetch the one field we care about
|
|
||||||
CallOpts::default().filter(json!({
|
|
||||||
"invoices": [{"pay_index": true}]
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let parsed: PayIndexInvoices =
|
|
||||||
serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
|
|
||||||
|
|
||||||
if let Some(pi) = parsed.invoices.iter().filter_map(|inv| inv.pay_index).max() {
|
|
||||||
return Ok(Some(pi));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) no paid invoice in this slice—step back or bail
|
|
||||||
if start_at == 1 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
start_at = start_at.saturating_sub(PAGE).max(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_paid_invoices(
|
|
||||||
commando: Arc<CommandoClient>,
|
|
||||||
limit: u32,
|
|
||||||
) -> Result<Vec<Invoice>, lnsocket::Error> {
|
|
||||||
use tokio::task::JoinSet;
|
|
||||||
|
|
||||||
// look for an invoice with the last paid index
|
|
||||||
let Some(lastpay_index) = find_lastpay_index(commando.clone()).await? else {
|
|
||||||
// no paid invoices
|
|
||||||
return Ok(vec![]);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut set: JoinSet<Result<Invoice, lnsocket::Error>> = JoinSet::new();
|
|
||||||
let start = lastpay_index.saturating_sub(limit as u64);
|
|
||||||
|
|
||||||
// 3) Fire off at most `concurrency` `waitanyinvoice` calls at a time,
|
|
||||||
// collect all successful responses into a Vec.
|
|
||||||
// fire them ALL at once
|
|
||||||
for idx in start..lastpay_index {
|
|
||||||
let c = commando.clone();
|
|
||||||
set.spawn(async move {
|
|
||||||
let val = c
|
|
||||||
.call(
|
|
||||||
"waitanyinvoice",
|
|
||||||
serde_json::json!({ "lastpay_index": idx }),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let parsed: Invoice = serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
|
|
||||||
Ok(parsed)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut results = Vec::with_capacity(limit as usize);
|
|
||||||
while let Some(res) = set.join_next().await {
|
|
||||||
results.push(res.map_err(|_| lnsocket::Error::Io(std::io::ErrorKind::Interrupted))??);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort_by(|a, b| a.updated_index.cmp(&b.updated_index).reverse());
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wip watch subsystem
|
|
||||||
/*
|
|
||||||
async fn watch_subsystem(
|
|
||||||
commando: CommandoClient,
|
|
||||||
subsystem: WaitSubsystem,
|
|
||||||
index: WaitIndex,
|
|
||||||
event_tx: UnboundedSender<Event>,
|
|
||||||
mut cancel_rx: Receiver<()>,
|
|
||||||
) {
|
|
||||||
// Step 1: Fetch current index value so we can back up ~20
|
|
||||||
let mut nextvalue: u64 = match commando
|
|
||||||
.call(
|
|
||||||
"wait",
|
|
||||||
serde_json::json!({
|
|
||||||
"indexname": index.as_str(),
|
|
||||||
"subsystem": subsystem.as_str(),
|
|
||||||
"nextvalue": 0
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(v) => {
|
|
||||||
// You showed the result has `updated` as the current highest index
|
|
||||||
let current = v.get("updated").and_then(|x| x.as_u64()).unwrap_or(0);
|
|
||||||
current.saturating_sub(20) // back up 20, clamp at 0
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!("initial wait(…nextvalue=0) failed: {}", err);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// You can add a timeout to avoid hanging forever in weird network states.
|
|
||||||
let fut = commando.call(
|
|
||||||
"wait",
|
|
||||||
serde_json::to_value(WaitRequest {
|
|
||||||
indexname: "invoices".into(),
|
|
||||||
subsystem: "lightningd".into(),
|
|
||||||
nextvalue,
|
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = &mut cancel_rx => {
|
|
||||||
// graceful shutdown
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
res = fut => {
|
|
||||||
match res {
|
|
||||||
Ok(v) => {
|
|
||||||
// Typical shape: { "nextvalue": n, "invoicestatus": { ... } } (varies by plugin/index)
|
|
||||||
// Adjust these lookups for your node’s actual wait payload.
|
|
||||||
if let Some(nv) = v.get("nextvalue").and_then(|x| x.as_u64()) {
|
|
||||||
nextvalue = nv + 1;
|
|
||||||
} else {
|
|
||||||
// Defensive: never get stuck — bump at least by 1
|
|
||||||
nextvalue += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inspect/route
|
|
||||||
let kind = v.get("status").and_then(|s| s.as_str());
|
|
||||||
let ev = match kind {
|
|
||||||
Some("paid") => ClnResponse::Invoice(InvoiceEvent::Paid(v.clone())),
|
|
||||||
Some("created") => ClnResponse::Invoice(InvoiceEvent::Created(v.clone())),
|
|
||||||
_ => ClnResponse::Invoice(InvoiceEvent::Other(v.clone())),
|
|
||||||
};
|
|
||||||
let _ = event_tx.send(Event::Response(ev));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!("wait(invoices) error: {err}");
|
|
||||||
// small backoff so we don't tight-loop on persistent errors
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@@ -10,10 +10,6 @@ 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 }
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
use enostr::{FullKeypair, Pubkey};
|
use enostr::{FullKeypair, Pubkey};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
|
|
||||||
use notedeck::{Accounts, AppContext, JobsCache, Localization, SingleUnkIdAction, UnknownIds};
|
use notedeck::{Accounts, AppContext, Localization, SingleUnkIdAction, UnknownIds};
|
||||||
use notedeck_ui::nip51_set::Nip51SetUiCache;
|
|
||||||
|
|
||||||
pub use crate::accounts::route::AccountsResponse;
|
|
||||||
use crate::app::get_active_columns_mut;
|
use crate::app::get_active_columns_mut;
|
||||||
use crate::decks::DecksCache;
|
use crate::decks::DecksCache;
|
||||||
use crate::onboarding::Onboarding;
|
|
||||||
use crate::profile::send_new_contact_list;
|
use crate::profile::send_new_contact_list;
|
||||||
use crate::subscriptions::Subscriptions;
|
|
||||||
use crate::ui::onboarding::{FollowPackOnboardingView, FollowPacksResponse, OnboardingResponse};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
login_manager::AcquireKeyState,
|
login_manager::AcquireKeyState,
|
||||||
route::Route,
|
route::Route,
|
||||||
|
timeline::TimelineCache,
|
||||||
ui::{
|
ui::{
|
||||||
account_login_view::{AccountLoginResponse, AccountLoginView},
|
account_login_view::{AccountLoginResponse, AccountLoginView},
|
||||||
accounts::{AccountsView, AccountsViewResponse},
|
accounts::{AccountsView, AccountsViewResponse},
|
||||||
@@ -41,7 +37,6 @@ pub struct SwitchAccountAction {
|
|||||||
|
|
||||||
/// The account to switch to
|
/// The account to switch to
|
||||||
pub switch_to: Pubkey,
|
pub switch_to: Pubkey,
|
||||||
pub switching_to_new: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SwitchAccountAction {
|
impl SwitchAccountAction {
|
||||||
@@ -49,14 +44,8 @@ impl SwitchAccountAction {
|
|||||||
SwitchAccountAction {
|
SwitchAccountAction {
|
||||||
source_column,
|
source_column,
|
||||||
switch_to,
|
switch_to,
|
||||||
switching_to_new: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn switching_to_new(mut self) -> Self {
|
|
||||||
self.switching_to_new = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -76,13 +65,13 @@ pub struct AddAccountAction {
|
|||||||
pub fn render_accounts_route(
|
pub fn render_accounts_route(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
app_ctx: &mut AppContext,
|
app_ctx: &mut AppContext,
|
||||||
jobs: &mut JobsCache,
|
col: usize,
|
||||||
|
decks: &mut DecksCache,
|
||||||
|
timeline_cache: &mut TimelineCache,
|
||||||
login_state: &mut AcquireKeyState,
|
login_state: &mut AcquireKeyState,
|
||||||
onboarding: &mut Onboarding,
|
|
||||||
follow_packs_ui: &mut Nip51SetUiCache,
|
|
||||||
route: AccountsRoute,
|
route: AccountsRoute,
|
||||||
) -> Option<AccountsResponse> {
|
) -> AddAccountAction {
|
||||||
match route {
|
let resp = match route {
|
||||||
AccountsRoute::Accounts => AccountsView::new(
|
AccountsRoute::Accounts => AccountsView::new(
|
||||||
app_ctx.ndb,
|
app_ctx.ndb,
|
||||||
app_ctx.accounts,
|
app_ctx.accounts,
|
||||||
@@ -91,33 +80,47 @@ pub fn render_accounts_route(
|
|||||||
)
|
)
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
.inner
|
.inner
|
||||||
.map(AccountsRouteResponse::Accounts)
|
.map(AccountsRouteResponse::Accounts),
|
||||||
.map(AccountsResponse::Account),
|
|
||||||
AccountsRoute::AddAccount => {
|
AccountsRoute::AddAccount => {
|
||||||
AccountLoginView::new(login_state, app_ctx.clipboard, app_ctx.i18n)
|
AccountLoginView::new(login_state, app_ctx.clipboard, app_ctx.i18n)
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
.inner
|
.inner
|
||||||
.map(AccountsRouteResponse::AddAccount)
|
.map(AccountsRouteResponse::AddAccount)
|
||||||
.map(AccountsResponse::Account)
|
|
||||||
}
|
}
|
||||||
AccountsRoute::Onboarding => FollowPackOnboardingView::new(
|
};
|
||||||
onboarding,
|
|
||||||
follow_packs_ui,
|
if let Some(resp) = resp {
|
||||||
app_ctx.ndb,
|
match resp {
|
||||||
app_ctx.img_cache,
|
AccountsRouteResponse::Accounts(response) => {
|
||||||
app_ctx.i18n,
|
let action = process_accounts_view_response(
|
||||||
app_ctx.job_pool,
|
app_ctx.i18n,
|
||||||
jobs,
|
app_ctx.accounts,
|
||||||
)
|
decks,
|
||||||
.ui(ui)
|
col,
|
||||||
.map(|r| match r {
|
response,
|
||||||
OnboardingResponse::FollowPacks(follow_packs_response) => {
|
);
|
||||||
AccountsResponse::Account(AccountsRouteResponse::AddAccount(
|
AddAccountAction {
|
||||||
AccountLoginResponse::Onboarding(follow_packs_response),
|
accounts_action: action,
|
||||||
))
|
unk_id_action: SingleUnkIdAction::no_action(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OnboardingResponse::ViewProfile(pubkey) => AccountsResponse::ViewProfile(pubkey),
|
AccountsRouteResponse::AddAccount(response) => {
|
||||||
}),
|
let action =
|
||||||
|
process_login_view_response(app_ctx, timeline_cache, decks, col, response);
|
||||||
|
*login_state = Default::default();
|
||||||
|
let router = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, decks)
|
||||||
|
.column_mut(col)
|
||||||
|
.router_mut();
|
||||||
|
router.go_back();
|
||||||
|
action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AddAccountAction {
|
||||||
|
accounts_action: None,
|
||||||
|
unk_id_action: SingleUnkIdAction::no_action(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,53 +155,31 @@ pub fn process_accounts_view_response(
|
|||||||
|
|
||||||
pub fn process_login_view_response(
|
pub fn process_login_view_response(
|
||||||
app_ctx: &mut AppContext,
|
app_ctx: &mut AppContext,
|
||||||
|
timeline_cache: &mut TimelineCache,
|
||||||
decks: &mut DecksCache,
|
decks: &mut DecksCache,
|
||||||
subs: &mut Subscriptions,
|
|
||||||
onboarding: &mut Onboarding,
|
|
||||||
col: usize,
|
col: usize,
|
||||||
response: AccountLoginResponse,
|
response: AccountLoginResponse,
|
||||||
) -> AddAccountAction {
|
) -> AddAccountAction {
|
||||||
let cur_router = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, decks)
|
let (r, pubkey) = match response {
|
||||||
.column_mut(col)
|
AccountLoginResponse::CreateNew => {
|
||||||
.router_mut();
|
let kp = FullKeypair::generate();
|
||||||
|
let pubkey = kp.pubkey;
|
||||||
let r = match response {
|
send_new_contact_list(kp.to_filled(), app_ctx.ndb, app_ctx.pool);
|
||||||
|
(app_ctx.accounts.add_account(kp.to_keypair()), pubkey)
|
||||||
|
}
|
||||||
AccountLoginResponse::LoginWith(keypair) => {
|
AccountLoginResponse::LoginWith(keypair) => {
|
||||||
cur_router.go_back();
|
let pubkey = keypair.pubkey;
|
||||||
app_ctx.accounts.add_account(keypair)
|
(app_ctx.accounts.add_account(keypair), pubkey)
|
||||||
}
|
}
|
||||||
AccountLoginResponse::CreatingNew => {
|
|
||||||
cur_router.route_to(Route::Accounts(AccountsRoute::Onboarding));
|
|
||||||
|
|
||||||
onboarding.process(app_ctx.pool, app_ctx.ndb, subs, app_ctx.unknown_ids);
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
AccountLoginResponse::Onboarding(onboarding_response) => match onboarding_response {
|
|
||||||
FollowPacksResponse::NoFollowPacks => {
|
|
||||||
onboarding.process(app_ctx.pool, app_ctx.ndb, subs, app_ctx.unknown_ids);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
FollowPacksResponse::UserSelectedPacks(nip51_sets_ui_state) => {
|
|
||||||
let pks_to_follow = nip51_sets_ui_state.get_all_selected();
|
|
||||||
|
|
||||||
let kp = FullKeypair::generate();
|
|
||||||
|
|
||||||
send_new_contact_list(kp.to_filled(), app_ctx.ndb, app_ctx.pool, pks_to_follow);
|
|
||||||
cur_router.go_back();
|
|
||||||
onboarding.end_onboarding(app_ctx.pool, app_ctx.ndb);
|
|
||||||
|
|
||||||
app_ctx.accounts.add_account(kp.to_keypair())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
decks.add_deck_default(app_ctx, timeline_cache, pubkey);
|
||||||
|
|
||||||
if let Some(action) = r {
|
if let Some(action) = r {
|
||||||
AddAccountAction {
|
AddAccountAction {
|
||||||
accounts_action: Some(AccountsAction::Switch(SwitchAccountAction {
|
accounts_action: Some(AccountsAction::Switch(SwitchAccountAction {
|
||||||
source_column: col,
|
source_column: col,
|
||||||
switch_to: action.switch_to,
|
switch_to: action.switch_to,
|
||||||
switching_to_new: true,
|
|
||||||
})),
|
})),
|
||||||
unk_id_action: action.unk_id_action,
|
unk_id_action: action.unk_id_action,
|
||||||
}
|
}
|
||||||
@@ -209,41 +190,3 @@ pub fn process_login_view_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountsRouteResponse {
|
|
||||||
pub fn process(
|
|
||||||
self,
|
|
||||||
app_ctx: &mut AppContext,
|
|
||||||
app: &mut crate::Damus,
|
|
||||||
col: usize,
|
|
||||||
) -> AddAccountAction {
|
|
||||||
match self {
|
|
||||||
AccountsRouteResponse::Accounts(response) => {
|
|
||||||
let action = process_accounts_view_response(
|
|
||||||
app_ctx.i18n,
|
|
||||||
app_ctx.accounts,
|
|
||||||
&mut app.decks_cache,
|
|
||||||
col,
|
|
||||||
response,
|
|
||||||
);
|
|
||||||
AddAccountAction {
|
|
||||||
accounts_action: action,
|
|
||||||
unk_id_action: notedeck::SingleUnkIdAction::no_action(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AccountsRouteResponse::AddAccount(response) => {
|
|
||||||
let action = process_login_view_response(
|
|
||||||
app_ctx,
|
|
||||||
&mut app.decks_cache,
|
|
||||||
&mut app.subscriptions,
|
|
||||||
&mut app.onboarding,
|
|
||||||
col,
|
|
||||||
response,
|
|
||||||
);
|
|
||||||
app.view_state.login = Default::default();
|
|
||||||
|
|
||||||
action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,16 +7,10 @@ pub enum AccountsRouteResponse {
|
|||||||
AddAccount(AccountLoginResponse),
|
AddAccount(AccountLoginResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum AccountsResponse {
|
|
||||||
ViewProfile(enostr::Pubkey),
|
|
||||||
Account(AccountsRouteResponse),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||||
pub enum AccountsRoute {
|
pub enum AccountsRoute {
|
||||||
Accounts,
|
Accounts,
|
||||||
AddAccount,
|
AddAccount,
|
||||||
Onboarding,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountsRoute {
|
impl AccountsRoute {
|
||||||
@@ -25,7 +19,6 @@ impl AccountsRoute {
|
|||||||
match self {
|
match self {
|
||||||
Self::Accounts => &["accounts", "show"],
|
Self::Accounts => &["accounts", "show"],
|
||||||
Self::AddAccount => &["accounts", "new"],
|
Self::AddAccount => &["accounts", "new"],
|
||||||
Self::Onboarding => &["accounts", "onboarding"],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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::{selected_has_at_least_n_replies, NoteSeenFlags, ThreadNode, Threads},
|
thread::{
|
||||||
InsertionResponse, ThreadSelection, TimelineCache, TimelineKind,
|
selected_has_at_least_n_replies, InsertionResponse, NoteSeenFlags, ThreadNode, Threads,
|
||||||
|
},
|
||||||
|
ThreadSelection, TimelineCache, TimelineKind,
|
||||||
},
|
},
|
||||||
view_state::ViewState,
|
view_state::ViewState,
|
||||||
};
|
};
|
||||||
@@ -30,9 +30,8 @@ pub enum NotesOpenResult {
|
|||||||
Thread(NewThreadNotes),
|
Thread(NewThreadNotes),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TimelineOpenResult {
|
pub enum TimelineOpenResult {
|
||||||
new_notes: Option<NewNotes>,
|
NewNotes(NewNotes),
|
||||||
new_pks: Option<HashSet<Pubkey>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoteActionResponse {
|
struct NoteActionResponse {
|
||||||
@@ -82,11 +81,7 @@ fn execute_note_action(
|
|||||||
.open(ndb, note_cache, txn, pool, &kind)
|
.open(ndb, note_cache, txn, pool, &kind)
|
||||||
.map(NotesOpenResult::Timeline);
|
.map(NotesOpenResult::Timeline);
|
||||||
}
|
}
|
||||||
NoteAction::Note {
|
NoteAction::Note { note_id, preview } => 'ex: {
|
||||||
note_id,
|
|
||||||
preview,
|
|
||||||
scroll_offset,
|
|
||||||
} => 'ex: {
|
|
||||||
let Ok(thread_selection) = ThreadSelection::from_note_id(ndb, note_cache, txn, note_id)
|
let Ok(thread_selection) = ThreadSelection::from_note_id(ndb, note_cache, txn, note_id)
|
||||||
else {
|
else {
|
||||||
tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes()));
|
tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes()));
|
||||||
@@ -94,15 +89,7 @@ fn execute_note_action(
|
|||||||
};
|
};
|
||||||
|
|
||||||
timeline_res = threads
|
timeline_res = threads
|
||||||
.open(
|
.open(ndb, txn, pool, &thread_selection, preview, col)
|
||||||
ndb,
|
|
||||||
txn,
|
|
||||||
pool,
|
|
||||||
&thread_selection,
|
|
||||||
preview,
|
|
||||||
col,
|
|
||||||
scroll_offset,
|
|
||||||
)
|
|
||||||
.map(NotesOpenResult::Thread);
|
.map(NotesOpenResult::Thread);
|
||||||
|
|
||||||
let route = Route::Thread(thread_selection);
|
let route = Route::Thread(thread_selection);
|
||||||
@@ -271,24 +258,7 @@ 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 {
|
Self::NewNotes(NewNotes::new(notes, id))
|
||||||
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(
|
||||||
@@ -299,17 +269,11 @@ impl TimelineOpenResult {
|
|||||||
storage: &mut TimelineCache,
|
storage: &mut TimelineCache,
|
||||||
unknown_ids: &mut UnknownIds,
|
unknown_ids: &mut UnknownIds,
|
||||||
) {
|
) {
|
||||||
// update the thread for next render if we have new notes
|
match self {
|
||||||
if let Some(new_notes) = &self.new_notes {
|
// update the thread for next render if we have new notes
|
||||||
new_notes.process(storage, ndb, txn, unknown_ids, note_cache);
|
TimelineOpenResult::NewNotes(new_notes) => {
|
||||||
}
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,7 +375,7 @@ pub fn process_thread_notes(
|
|||||||
created_at,
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
if thread.replies.contains_key(¬e_ref.key) {
|
if thread.replies.contains(¬e_ref) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ use crate::{
|
|||||||
decks::{Decks, DecksCache},
|
decks::{Decks, DecksCache},
|
||||||
draft::Drafts,
|
draft::Drafts,
|
||||||
nav::{self, ProcessNavResult},
|
nav::{self, ProcessNavResult},
|
||||||
onboarding::Onboarding,
|
|
||||||
options::AppOptions,
|
options::AppOptions,
|
||||||
route::Route,
|
route::Route,
|
||||||
storage,
|
storage,
|
||||||
subscriptions::{SubKind, Subscriptions},
|
subscriptions::{SubKind, Subscriptions},
|
||||||
support::Support,
|
support::Support,
|
||||||
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
|
timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
|
||||||
toolbar::unseen_notification,
|
ui::{self, DesktopSidePanel, ShowSourceClientOption, SidePanelAction},
|
||||||
ui::{self, toolbar::toolbar, DesktopSidePanel, SidePanelAction},
|
|
||||||
view_state::ViewState,
|
view_state::ViewState,
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
@@ -29,6 +27,7 @@ use notedeck_ui::{
|
|||||||
};
|
};
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
use tracing::{debug, error, info, trace, warn};
|
use tracing::{debug, error, info, trace, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -59,20 +58,18 @@ pub struct Damus {
|
|||||||
pub note_options: NoteOptions,
|
pub note_options: NoteOptions,
|
||||||
|
|
||||||
pub unrecognized_args: BTreeSet<String>,
|
pub unrecognized_args: BTreeSet<String>,
|
||||||
|
|
||||||
/// keep track of follow packs
|
|
||||||
pub onboarding: Onboarding,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_egui_events(input: &egui::InputState, columns: &mut Columns) {
|
fn handle_key_events(input: &egui::InputState, columns: &mut Columns) {
|
||||||
for event in &input.raw.events {
|
for event in &input.raw.events {
|
||||||
match event {
|
if let egui::Event::Key {
|
||||||
egui::Event::Key { key, pressed, .. } if *pressed => match key {
|
key, pressed: true, ..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
match key {
|
||||||
egui::Key::J => {
|
egui::Key::J => {
|
||||||
//columns.select_down();
|
columns.select_down();
|
||||||
{}
|
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
egui::Key::K => {
|
egui::Key::K => {
|
||||||
columns.select_up();
|
columns.select_up();
|
||||||
}
|
}
|
||||||
@@ -82,18 +79,11 @@ fn handle_egui_events(input: &egui::InputState, columns: &mut Columns) {
|
|||||||
egui::Key::L => {
|
egui::Key::L => {
|
||||||
columns.select_left();
|
columns.select_left();
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
egui::Key::BrowserBack | egui::Key::Escape => {
|
egui::Key::BrowserBack | egui::Key::Escape => {
|
||||||
columns.get_selected_router().go_back();
|
columns.get_selected_router().go_back();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
|
||||||
|
|
||||||
egui::Event::InsetsChanged => {
|
|
||||||
tracing::debug!("insets have changed!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +95,7 @@ fn try_process_event(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let current_columns =
|
let current_columns =
|
||||||
get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache);
|
get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache);
|
||||||
ctx.input(|i| handle_egui_events(i, current_columns));
|
ctx.input(|i| handle_key_events(i, current_columns));
|
||||||
|
|
||||||
let ctx2 = ctx.clone();
|
let ctx2 = ctx.clone();
|
||||||
let wakeup = move || {
|
let wakeup = move || {
|
||||||
@@ -148,14 +138,13 @@ fn try_process_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (kind, timeline) in &mut damus.timeline_cache {
|
for (_kind, timeline) in &mut damus.timeline_cache {
|
||||||
let is_ready = timeline::is_timeline_ready(
|
let is_ready = timeline::is_timeline_ready(
|
||||||
app_ctx.ndb,
|
app_ctx.ndb,
|
||||||
app_ctx.pool,
|
app_ctx.pool,
|
||||||
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 {
|
||||||
@@ -174,16 +163,9 @@ fn try_process_event(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: show loading?
|
// TODO: show loading?
|
||||||
if matches!(kind, TimelineKind::List(ListKind::Contact(_))) {
|
|
||||||
timeline::fetch_contact_list(&mut damus.subscriptions, timeline, app_ctx.accounts);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(follow_packs) = damus.onboarding.get_follow_packs_mut() {
|
|
||||||
follow_packs.poll_for_notes(app_ctx.ndb, app_ctx.unknown_ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
if app_ctx.unknown_ids.ready_to_send() {
|
if app_ctx.unknown_ids.ready_to_send() {
|
||||||
unknown_id_send(app_ctx.unknown_ids, app_ctx.pool);
|
unknown_id_send(app_ctx.unknown_ids, app_ctx.pool);
|
||||||
}
|
}
|
||||||
@@ -223,7 +205,6 @@ 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}");
|
||||||
}
|
}
|
||||||
@@ -393,7 +374,7 @@ fn render_damus(
|
|||||||
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
|
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
|
||||||
|
|
||||||
// We use this for keeping timestamps and things up to date
|
// We use this for keeping timestamps and things up to date
|
||||||
//ui.ctx().request_repaint_after(Duration::from_secs(5));
|
ui.ctx().request_repaint_after(Duration::from_secs(5));
|
||||||
|
|
||||||
app_action
|
app_action
|
||||||
}
|
}
|
||||||
@@ -513,10 +494,14 @@ impl Damus {
|
|||||||
// cache.add_deck_default(*pk);
|
// cache.add_deck_default(*pk);
|
||||||
//}
|
//}
|
||||||
};
|
};
|
||||||
|
let settings = &app_context.settings;
|
||||||
|
|
||||||
let support = Support::new(app_context.path);
|
let support = Support::new(app_context.path);
|
||||||
let note_options = get_note_options(parsed_args, app_context.settings);
|
|
||||||
|
let note_options = get_note_options(parsed_args, settings);
|
||||||
|
|
||||||
let jobs = JobsCache::default();
|
let jobs = JobsCache::default();
|
||||||
|
|
||||||
let threads = Threads::default();
|
let threads = Threads::default();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -533,7 +518,6 @@ impl Damus {
|
|||||||
unrecognized_args,
|
unrecognized_args,
|
||||||
jobs,
|
jobs,
|
||||||
threads,
|
threads,
|
||||||
onboarding: Onboarding::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +568,6 @@ impl Damus {
|
|||||||
unrecognized_args: BTreeSet::default(),
|
unrecognized_args: BTreeSet::default(),
|
||||||
jobs: JobsCache::default(),
|
jobs: JobsCache::default(),
|
||||||
threads: Threads::default(),
|
threads: Threads::default(),
|
||||||
onboarding: Onboarding::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,17 +578,9 @@ impl Damus {
|
|||||||
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
pub fn unrecognized_args(&self) -> &BTreeSet<String> {
|
||||||
&self.unrecognized_args
|
&self.unrecognized_args
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toolbar_height() -> f32 {
|
|
||||||
48.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initially_selected_toolbar_index() -> i32 {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_note_options(args: ColumnsArgs, settings_handler: &mut SettingsHandler) -> NoteOptions {
|
fn get_note_options(args: ColumnsArgs, settings_handler: &&mut SettingsHandler) -> NoteOptions {
|
||||||
let mut note_options = NoteOptions::default();
|
let mut note_options = NoteOptions::default();
|
||||||
|
|
||||||
note_options.set(
|
note_options.set(
|
||||||
@@ -620,6 +595,17 @@ fn get_note_options(args: ColumnsArgs, settings_handler: &mut SettingsHandler) -
|
|||||||
NoteOptions::HideMedia,
|
NoteOptions::HideMedia,
|
||||||
args.is_flag_set(ColumnsFlag::NoMedia),
|
args.is_flag_set(ColumnsFlag::NoMedia),
|
||||||
);
|
);
|
||||||
|
note_options.set(
|
||||||
|
NoteOptions::ShowNoteClientTop,
|
||||||
|
ShowSourceClientOption::Top == settings_handler.show_source_client().into()
|
||||||
|
|| args.is_flag_set(ColumnsFlag::ShowNoteClientTop),
|
||||||
|
);
|
||||||
|
note_options.set(
|
||||||
|
NoteOptions::ShowNoteClientBottom,
|
||||||
|
ShowSourceClientOption::Bottom == settings_handler.show_source_client().into()
|
||||||
|
|| args.is_flag_set(ColumnsFlag::ShowNoteClientBottom),
|
||||||
|
);
|
||||||
|
|
||||||
note_options.set(
|
note_options.set(
|
||||||
NoteOptions::RepliesNewestFirst,
|
NoteOptions::RepliesNewestFirst,
|
||||||
settings_handler.show_replies_newest_first(),
|
settings_handler.show_replies_newest_first(),
|
||||||
@@ -644,71 +630,36 @@ fn render_damus_mobile(
|
|||||||
) -> Option<AppAction> {
|
) -> Option<AppAction> {
|
||||||
//let routes = app.timelines[0].routes.clone();
|
//let routes = app.timelines[0].routes.clone();
|
||||||
|
|
||||||
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
|
let rect = ui.available_rect_before_wrap();
|
||||||
let mut app_action: Option<AppAction> = None;
|
let mut app_action: Option<AppAction> = None;
|
||||||
// don't show toolbar if soft keyboard is open
|
|
||||||
let skb_rect = app_ctx.soft_keyboard_rect(
|
|
||||||
ui.ctx().screen_rect(),
|
|
||||||
notedeck::SoftKeyboardContext::platform(ui.ctx()),
|
|
||||||
);
|
|
||||||
let toolbar_height = if skb_rect.is_none() {
|
|
||||||
Damus::toolbar_height()
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
StripBuilder::new(ui)
|
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
|
||||||
.size(Size::remainder()) // top cell
|
|
||||||
.size(Size::exact(toolbar_height)) // bottom cell
|
|
||||||
.vertical(|mut strip| {
|
|
||||||
strip.cell(|ui| {
|
|
||||||
let rect = ui.available_rect_before_wrap();
|
|
||||||
if !app.columns(app_ctx.accounts).columns().is_empty() {
|
|
||||||
let r = nav::render_nav(
|
|
||||||
active_col,
|
|
||||||
ui.available_rect_before_wrap(),
|
|
||||||
app,
|
|
||||||
app_ctx,
|
|
||||||
ui,
|
|
||||||
)
|
|
||||||
.process_render_nav_response(app, app_ctx, ui);
|
|
||||||
if let Some(r) = &r {
|
|
||||||
match r {
|
|
||||||
ProcessNavResult::SwitchOccurred => {
|
|
||||||
if !app.options.contains(AppOptions::TmpColumns) {
|
|
||||||
storage::save_decks_cache(app_ctx.path, &app.decks_cache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessNavResult::PfpClicked => {
|
if !app.columns(app_ctx.accounts).columns().is_empty() {
|
||||||
app_action = Some(AppAction::ToggleChrome);
|
let r = nav::render_nav(
|
||||||
}
|
active_col,
|
||||||
}
|
ui.available_rect_before_wrap(),
|
||||||
|
app,
|
||||||
|
app_ctx,
|
||||||
|
ui,
|
||||||
|
)
|
||||||
|
.process_render_nav_response(app, app_ctx, ui);
|
||||||
|
if let Some(r) = &r {
|
||||||
|
match r {
|
||||||
|
ProcessNavResult::SwitchOccurred => {
|
||||||
|
if !app.options.contains(AppOptions::TmpColumns) {
|
||||||
|
storage::save_decks_cache(app_ctx.path, &app.decks_cache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hovering_post_button(ui, app, app_ctx, rect);
|
ProcessNavResult::PfpClicked => {
|
||||||
});
|
app_action = Some(AppAction::ToggleChrome);
|
||||||
|
|
||||||
strip.cell(|ui| 'brk: {
|
|
||||||
if toolbar_height <= 0.0 {
|
|
||||||
break 'brk;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let unseen_notif = unseen_notification(
|
hovering_post_button(ui, app, app_ctx, rect);
|
||||||
app,
|
|
||||||
app_ctx.ndb,
|
|
||||||
app_ctx.accounts.get_selected_account().key.pubkey,
|
|
||||||
);
|
|
||||||
|
|
||||||
if skb_rect.is_none() {
|
|
||||||
let resp = toolbar(ui, unseen_notif);
|
|
||||||
if let Some(action) = resp {
|
|
||||||
action.process(app, app_ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app_action
|
app_action
|
||||||
}
|
}
|
||||||
@@ -724,10 +675,8 @@ fn hovering_post_button(
|
|||||||
let button_y = ui
|
let button_y = ui
|
||||||
.ctx()
|
.ctx()
|
||||||
.animate_bool_responsive(btn_id, should_show_compose);
|
.animate_bool_responsive(btn_id, should_show_compose);
|
||||||
|
rect.min.x = rect.max.x - if is_narrow(ui.ctx()) { 60.0 } else { 100.0 };
|
||||||
rect.min.x = rect.max.x - (if is_narrow(ui.ctx()) { 60.0 } else { 100.0 } * button_y);
|
rect.min.y = rect.max.y - 100.0 * button_y;
|
||||||
rect.min.y = rect.max.y - 100.0;
|
|
||||||
rect.max.x += 48.0 * (1.0 - button_y);
|
|
||||||
|
|
||||||
let darkmode = ui.ctx().style().visuals.dark_mode;
|
let darkmode = ui.ctx().style().visuals.dark_mode;
|
||||||
|
|
||||||
@@ -904,13 +853,7 @@ fn timelines_view(
|
|||||||
let mut save_cols = false;
|
let mut save_cols = false;
|
||||||
if let Some(action) = side_panel_action {
|
if let Some(action) = side_panel_action {
|
||||||
save_cols = save_cols
|
save_cols = save_cols
|
||||||
|| action.process(
|
|| action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx, ui.ctx());
|
||||||
&mut app.timeline_cache,
|
|
||||||
&mut app.decks_cache,
|
|
||||||
ctx,
|
|
||||||
&mut app.subscriptions,
|
|
||||||
ui.ctx(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut app_action: Option<AppAction> = None;
|
let mut app_action: Option<AppAction> = None;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ pub enum ColumnsFlag {
|
|||||||
Textmode,
|
Textmode,
|
||||||
Scramble,
|
Scramble,
|
||||||
NoMedia,
|
NoMedia,
|
||||||
|
ShowNoteClientTop,
|
||||||
|
ShowNoteClientBottom,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ColumnsArgs {
|
pub struct ColumnsArgs {
|
||||||
@@ -52,6 +54,10 @@ impl ColumnsArgs {
|
|||||||
res.clear_flag(ColumnsFlag::SinceOptimize);
|
res.clear_flag(ColumnsFlag::SinceOptimize);
|
||||||
} else if arg == "--scramble" {
|
} else if arg == "--scramble" {
|
||||||
res.set_flag(ColumnsFlag::Scramble);
|
res.set_flag(ColumnsFlag::Scramble);
|
||||||
|
} else if arg == "--show-note-client=top" {
|
||||||
|
res.set_flag(ColumnsFlag::ShowNoteClientTop);
|
||||||
|
} else if arg == "--show-note-client=bottom" {
|
||||||
|
res.set_flag(ColumnsFlag::ShowNoteClientBottom);
|
||||||
} else if arg == "--no-media" {
|
} else if arg == "--no-media" {
|
||||||
res.set_flag(ColumnsFlag::NoMedia);
|
res.set_flag(ColumnsFlag::NoMedia);
|
||||||
} else if arg == "--filter" {
|
} else if arg == "--filter" {
|
||||||
@@ -140,16 +146,7 @@ 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());
|
||||||
|
|||||||
@@ -75,33 +75,25 @@ impl Columns {
|
|||||||
/// Select the column based on the timeline kind.
|
/// Select the column based on the timeline kind.
|
||||||
///
|
///
|
||||||
/// TODO: add timeline if missing?
|
/// TODO: add timeline if missing?
|
||||||
pub fn select_by_route(&mut self, desired_route: Route) -> SelectionResult {
|
pub fn select_by_kind(&mut self, kind: &TimelineKind) -> SelectionResult {
|
||||||
for (i, col) in self.columns.iter().enumerate() {
|
for (i, col) in self.columns.iter().enumerate() {
|
||||||
for route in col.router().routes() {
|
for route in col.router().routes() {
|
||||||
if *route == desired_route {
|
if let Some(timeline) = route.timeline_id() {
|
||||||
if self.selected as usize == i {
|
if timeline == kind {
|
||||||
return SelectionResult::AlreadySelected(i);
|
tracing::info!("selecting {kind:?} column");
|
||||||
} else {
|
if self.selected as usize == i {
|
||||||
self.select_column(i as i32);
|
return SelectionResult::AlreadySelected(i);
|
||||||
return SelectionResult::NewSelection(i);
|
} else {
|
||||||
|
self.select_column(i as i32);
|
||||||
|
return SelectionResult::NewSelection(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(&desired_route, Route::Timeline(_))
|
tracing::error!("failed to select {kind:?} column");
|
||||||
|| matches!(&desired_route, Route::Thread(_))
|
SelectionResult::Failed
|
||||||
{
|
|
||||||
// these require additional handling to add state
|
|
||||||
tracing::error!("failed to select {desired_route:?} column");
|
|
||||||
return SelectionResult::Failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.add_column(Column::new(vec![desired_route]));
|
|
||||||
|
|
||||||
let selected_index = self.columns.len() - 1;
|
|
||||||
self.select_column(selected_index as i32);
|
|
||||||
SelectionResult::NewSelection(selected_index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_new_timeline_column(
|
pub fn add_new_timeline_column(
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ impl DecksCache {
|
|||||||
&self.fallback_pubkey
|
&self.fallback_pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_decks_mut(&mut self) -> ValuesMut<'_, Pubkey, Decks> {
|
pub fn get_all_decks_mut(&mut self) -> ValuesMut<Pubkey, Decks> {
|
||||||
self.account_to_decks.values_mut()
|
self.account_to_decks.values_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ pub mod login_manager;
|
|||||||
mod media_upload;
|
mod media_upload;
|
||||||
mod multi_subscriber;
|
mod multi_subscriber;
|
||||||
mod nav;
|
mod nav;
|
||||||
mod onboarding;
|
|
||||||
pub mod options;
|
pub mod options;
|
||||||
mod post;
|
mod post;
|
||||||
mod profile;
|
mod profile;
|
||||||
@@ -28,7 +27,6 @@ mod subscriptions;
|
|||||||
mod support;
|
mod support;
|
||||||
mod test_data;
|
mod test_data;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
mod toolbar;
|
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
mod unknowns;
|
mod unknowns;
|
||||||
mod view_state;
|
mod view_state;
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
#![cfg_attr(target_os = "android", allow(dead_code, unused_variables))]
|
#![cfg_attr(target_os = "android", allow(dead_code, unused_variables))]
|
||||||
|
|
||||||
use crate::Error;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
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::{
|
use notedeck::SupportedMimeType;
|
||||||
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";
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ pub fn get_nostr_build_upload_url() -> Promise<Result<String, Error>> {
|
|||||||
get_upload_url_from_provider(NOSTR_BUILD_URL())
|
get_upload_url_from_provider(NOSTR_BUILD_URL())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String) -> Note<'_> {
|
fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String) -> Note {
|
||||||
NoteBuilder::new()
|
NoteBuilder::new()
|
||||||
.kind(27235)
|
.kind(27235)
|
||||||
.start_tag()
|
.start_tag()
|
||||||
@@ -93,15 +94,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,
|
||||||
file_name: &str,
|
media_path: MediaPath,
|
||||||
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!(
|
||||||
"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{file_name}\"\r\nContent-Type: {media_type}\r\n\r\n",
|
"--{}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\nContent-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);
|
||||||
@@ -133,14 +134,25 @@ 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,
|
||||||
selected_media: SelectedMedia,
|
media_path: MediaPath,
|
||||||
) -> Promise<Result<Nip94Event, Error>> {
|
) -> Promise<Result<Nip94Event, Error>> {
|
||||||
internal_nip96_upload(seckey, upload_url, selected_media)
|
let bytes_res = fetch_binary_from_disk(media_path.full_path.clone());
|
||||||
|
|
||||||
|
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],
|
||||||
selected_media: SelectedMedia,
|
media_path: MediaPath,
|
||||||
) -> 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 || {
|
||||||
@@ -154,7 +166,7 @@ pub fn nostrbuild_nip96_upload(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = nip96_upload(seckey, upload_url, selected_media).block_and_take();
|
let res = nip96_upload(seckey, upload_url, media_path).block_and_take();
|
||||||
sender.send(res);
|
sender.send(res);
|
||||||
});
|
});
|
||||||
promise
|
promise
|
||||||
@@ -163,21 +175,9 @@ 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,
|
||||||
selected_media: SelectedMedia,
|
media_path: MediaPath,
|
||||||
|
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,13 +186,7 @@ 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(
|
let request = create_nip96_request(&upload_url, media_path, file_contents, &nip98_base64);
|
||||||
&upload_url,
|
|
||||||
&file_name,
|
|
||||||
mime_type,
|
|
||||||
file_contents,
|
|
||||||
&nip98_base64,
|
|
||||||
);
|
|
||||||
|
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
|
|
||||||
@@ -238,10 +232,33 @@ fn find_nip94_ev_in_json(json: String) -> Result<Nip94Event, Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bytes_from_media(media: MediaFrom) -> Result<Vec<u8>, notedeck::Error> {
|
#[derive(Debug)]
|
||||||
match media {
|
pub struct MediaPath {
|
||||||
MediaFrom::PathBuf(full_path) => fetch_binary_from_disk(full_path.clone()),
|
full_path: PathBuf,
|
||||||
MediaFrom::Memory(bytes) => Ok(bytes),
|
file_name: String,
|
||||||
|
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"
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +349,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, SelectedMedia, NOSTR_BUILD_URL,
|
get_upload_url_from_provider, nostrbuild_nip96_upload, MediaPath, NOSTR_BUILD_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::internal_nip96_upload;
|
use super::internal_nip96_upload;
|
||||||
@@ -351,7 +368,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 selected_media = SelectedMedia::from_path(file_path).unwrap();
|
let media_path = MediaPath::new(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();
|
||||||
@@ -361,7 +378,8 @@ 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(),
|
||||||
selected_media,
|
media_path,
|
||||||
|
img_bytes.to_vec(),
|
||||||
);
|
);
|
||||||
let res = promise.block_until_ready();
|
let res = promise.block_until_ready();
|
||||||
assert!(res.is_ok())
|
assert!(res.is_ok())
|
||||||
@@ -377,11 +395,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 selected_media = SelectedMedia::from_path(file_path).unwrap();
|
let media_path = MediaPath::new(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(), selected_media);
|
let promise = nostrbuild_nip96_upload(kp.secret_key.secret_bytes(), media_path);
|
||||||
|
|
||||||
let out = promise.block_and_take();
|
let out = promise.block_and_take();
|
||||||
assert!(out.is_ok());
|
assert!(out.is_ok());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
accounts::{render_accounts_route, AccountsAction, AccountsResponse},
|
accounts::{render_accounts_route, AccountsAction},
|
||||||
app::{get_active_columns_mut, get_decks_mut},
|
app::{get_active_columns_mut, get_decks_mut},
|
||||||
column::ColumnsAction,
|
column::ColumnsAction,
|
||||||
deck_state::DeckState,
|
deck_state::DeckState,
|
||||||
@@ -8,9 +8,7 @@ use crate::{
|
|||||||
options::AppOptions,
|
options::AppOptions,
|
||||||
profile::{ProfileAction, SaveProfileChanges},
|
profile::{ProfileAction, SaveProfileChanges},
|
||||||
route::{Route, Router, SingletonRouter},
|
route::{Route, Router, SingletonRouter},
|
||||||
subscriptions::Subscriptions,
|
|
||||||
timeline::{
|
timeline::{
|
||||||
kind::ListKind,
|
|
||||||
route::{render_thread_route, render_timeline_route},
|
route::{render_thread_route, render_timeline_route},
|
||||||
TimelineCache, TimelineKind,
|
TimelineCache, TimelineKind,
|
||||||
},
|
},
|
||||||
@@ -21,7 +19,6 @@ use crate::{
|
|||||||
configure_deck::ConfigureDeckView,
|
configure_deck::ConfigureDeckView,
|
||||||
edit_deck::{EditDeckResponse, EditDeckView},
|
edit_deck::{EditDeckResponse, EditDeckView},
|
||||||
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
|
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
|
||||||
onboarding::FollowPackOnboardingView,
|
|
||||||
profile::EditProfileView,
|
profile::EditProfileView,
|
||||||
search::{FocusState, SearchView},
|
search::{FocusState, SearchView},
|
||||||
settings::SettingsAction,
|
settings::SettingsAction,
|
||||||
@@ -40,7 +37,6 @@ use notedeck::{
|
|||||||
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
|
get_current_default_msats, tr, ui::is_narrow, Accounts, AppContext, NoteAction, NoteContext,
|
||||||
RelayAction,
|
RelayAction,
|
||||||
};
|
};
|
||||||
use notedeck_ui::NoteOptions;
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
/// The result of processing a nav response
|
/// The result of processing a nav response
|
||||||
@@ -83,36 +79,19 @@ impl SwitchingAction {
|
|||||||
timeline_cache: &mut TimelineCache,
|
timeline_cache: &mut TimelineCache,
|
||||||
decks_cache: &mut DecksCache,
|
decks_cache: &mut DecksCache,
|
||||||
ctx: &mut AppContext<'_>,
|
ctx: &mut AppContext<'_>,
|
||||||
subs: &mut Subscriptions,
|
|
||||||
ui_ctx: &egui::Context,
|
ui_ctx: &egui::Context,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match &self {
|
match &self {
|
||||||
SwitchingAction::Accounts(account_action) => match account_action {
|
SwitchingAction::Accounts(account_action) => match account_action {
|
||||||
AccountsAction::Switch(switch_action) => {
|
AccountsAction::Switch(switch_action) => {
|
||||||
{
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
ctx.accounts.select_account(
|
||||||
ctx.accounts.select_account(
|
&switch_action.switch_to,
|
||||||
&switch_action.switch_to,
|
ctx.ndb,
|
||||||
ctx.ndb,
|
&txn,
|
||||||
&txn,
|
ctx.pool,
|
||||||
ctx.pool,
|
ui_ctx,
|
||||||
ui_ctx,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
let contacts_sub = ctx.accounts.get_subs().contacts.remote.clone();
|
|
||||||
// this is cringe but we're gonna get a new sub manager soon...
|
|
||||||
subs.subs.insert(
|
|
||||||
contacts_sub,
|
|
||||||
crate::subscriptions::SubKind::FetchingContactList(TimelineKind::List(
|
|
||||||
ListKind::Contact(*ctx.accounts.selected_account_pubkey()),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if switch_action.switching_to_new {
|
|
||||||
decks_cache.add_deck_default(ctx, timeline_cache, switch_action.switch_to);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pop nav after switch
|
// pop nav after switch
|
||||||
get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
|
get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
|
||||||
.column_mut(switch_action.source_column)
|
.column_mut(switch_action.source_column)
|
||||||
@@ -488,7 +467,6 @@ fn process_render_nav_action(
|
|||||||
&mut app.timeline_cache,
|
&mut app.timeline_cache,
|
||||||
&mut app.decks_cache,
|
&mut app.decks_cache,
|
||||||
ctx,
|
ctx,
|
||||||
&mut app.subscriptions,
|
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
) {
|
) {
|
||||||
return Some(ProcessNavResult::SwitchOccurred);
|
return Some(ProcessNavResult::SwitchOccurred);
|
||||||
@@ -585,33 +563,21 @@ fn render_nav_body(
|
|||||||
&mut note_context,
|
&mut note_context,
|
||||||
&mut app.jobs,
|
&mut app.jobs,
|
||||||
),
|
),
|
||||||
Route::Accounts(amr) => 's: {
|
Route::Accounts(amr) => {
|
||||||
let Some(action) = render_accounts_route(
|
let mut action = render_accounts_route(
|
||||||
ui,
|
ui,
|
||||||
ctx,
|
ctx,
|
||||||
&mut app.jobs,
|
col,
|
||||||
|
&mut app.decks_cache,
|
||||||
|
&mut app.timeline_cache,
|
||||||
&mut app.view_state.login,
|
&mut app.view_state.login,
|
||||||
&mut app.onboarding,
|
|
||||||
&mut app.view_state.follow_packs,
|
|
||||||
*amr,
|
*amr,
|
||||||
) else {
|
);
|
||||||
break 's None;
|
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||||
};
|
action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
|
||||||
|
action
|
||||||
match action {
|
.accounts_action
|
||||||
AccountsResponse::ViewProfile(pubkey) => {
|
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
|
||||||
Some(RenderNavAction::NoteAction(NoteAction::Profile(pubkey)))
|
|
||||||
}
|
|
||||||
AccountsResponse::Account(accounts_route_response) => {
|
|
||||||
let mut action = accounts_route_response.process(ctx, app, col);
|
|
||||||
|
|
||||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
|
||||||
action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
|
|
||||||
action
|
|
||||||
.accounts_action
|
|
||||||
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
|
Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
@@ -625,7 +591,6 @@ fn render_nav_body(
|
|||||||
)
|
)
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
.map(RenderNavAction::SettingsAction),
|
.map(RenderNavAction::SettingsAction),
|
||||||
|
|
||||||
Route::Reply(id) => {
|
Route::Reply(id) => {
|
||||||
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
|
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
|
||||||
txn
|
txn
|
||||||
@@ -654,16 +619,13 @@ fn render_nav_body(
|
|||||||
let action = {
|
let action = {
|
||||||
let draft = app.drafts.reply_mut(note.id());
|
let draft = app.drafts.reply_mut(note.id());
|
||||||
|
|
||||||
let mut options = app.note_options;
|
|
||||||
options.set(NoteOptions::Wide, false);
|
|
||||||
|
|
||||||
let response = ui::PostReplyView::new(
|
let response = ui::PostReplyView::new(
|
||||||
&mut note_context,
|
&mut note_context,
|
||||||
poster,
|
poster,
|
||||||
draft,
|
draft,
|
||||||
¬e,
|
¬e,
|
||||||
inner_rect,
|
inner_rect,
|
||||||
options,
|
app.note_options,
|
||||||
&mut app.jobs,
|
&mut app.jobs,
|
||||||
col,
|
col,
|
||||||
)
|
)
|
||||||
@@ -832,7 +794,7 @@ fn render_nav_body(
|
|||||||
return action;
|
return action;
|
||||||
};
|
};
|
||||||
|
|
||||||
if EditProfileView::new(ctx.i18n, state, ctx.img_cache, ctx.clipboard).ui(ui) {
|
if EditProfileView::new(ctx.i18n, state, ctx.img_cache).ui(ui) {
|
||||||
if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) {
|
if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) {
|
||||||
action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
|
action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
|
||||||
SaveProfileChanges::new(kp.to_full(), state.clone()),
|
SaveProfileChanges::new(kp.to_full(), state.clone()),
|
||||||
@@ -960,7 +922,7 @@ pub fn render_nav(
|
|||||||
ctx.ndb,
|
ctx.ndb,
|
||||||
ctx.img_cache,
|
ctx.img_cache,
|
||||||
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
|
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
|
||||||
std::slice::from_ref(route),
|
&[route.clone()],
|
||||||
col,
|
col,
|
||||||
ctx.i18n,
|
ctx.i18n,
|
||||||
)
|
)
|
||||||
@@ -1094,9 +1056,6 @@ fn get_scroll_id(
|
|||||||
Route::Accounts(accounts_route) => match accounts_route {
|
Route::Accounts(accounts_route) => match accounts_route {
|
||||||
crate::accounts::AccountsRoute::Accounts => Some(AccountsView::scroll_id()),
|
crate::accounts::AccountsRoute::Accounts => Some(AccountsView::scroll_id()),
|
||||||
crate::accounts::AccountsRoute::AddAccount => None,
|
crate::accounts::AccountsRoute::AddAccount => None,
|
||||||
crate::accounts::AccountsRoute::Onboarding => {
|
|
||||||
Some(FollowPackOnboardingView::scroll_id())
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Route::Reply(note_id) => Some(PostReplyView::scroll_id(col, note_id.bytes())),
|
Route::Reply(note_id) => Some(PostReplyView::scroll_id(col, note_id.bytes())),
|
||||||
Route::Quote(note_id) => Some(QuoteRepostView::scroll_id(col, note_id.bytes())),
|
Route::Quote(note_id) => Some(QuoteRepostView::scroll_id(col, note_id.bytes())),
|
||||||
@@ -1121,7 +1080,6 @@ fn route_uses_frame(route: &Route) -> bool {
|
|||||||
Route::Accounts(accounts_route) => match accounts_route {
|
Route::Accounts(accounts_route) => match accounts_route {
|
||||||
crate::accounts::AccountsRoute::Accounts => true,
|
crate::accounts::AccountsRoute::Accounts => true,
|
||||||
crate::accounts::AccountsRoute::AddAccount => false,
|
crate::accounts::AccountsRoute::AddAccount => false,
|
||||||
crate::accounts::AccountsRoute::Onboarding => false,
|
|
||||||
},
|
},
|
||||||
Route::Relays => true,
|
Route::Relays => true,
|
||||||
Route::Timeline(_) => false,
|
Route::Timeline(_) => false,
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
|
|
||||||
use egui_virtual_list::VirtualList;
|
|
||||||
use enostr::{Pubkey, RelayPool};
|
|
||||||
use nostrdb::{Filter, Ndb, NoteKey, Transaction};
|
|
||||||
use notedeck::{create_nip51_set, filter::default_limit, Nip51SetCache, UnknownIds};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::subscriptions::Subscriptions;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum OnboardingState {
|
|
||||||
AwaitingTrustedPksList(Vec<Filter>),
|
|
||||||
HaveFollowPacks(Nip51SetCache),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Manages the onboarding process. Responsible for retriving the kind 30000 list of trusted pubkeys
|
|
||||||
/// and then retrieving all follow packs from the trusted pks updating when new ones arrive
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Onboarding {
|
|
||||||
state: Option<Result<OnboardingState, OnboardingError>>,
|
|
||||||
pub list: Rc<RefCell<VirtualList>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Onboarding {
|
|
||||||
pub fn get_follow_packs(&self) -> Option<&Nip51SetCache> {
|
|
||||||
let Some(Ok(OnboardingState::HaveFollowPacks(packs))) = &self.state else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(packs)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_follow_packs_mut(&mut self) -> Option<&mut Nip51SetCache> {
|
|
||||||
let Some(Ok(OnboardingState::HaveFollowPacks(packs))) = &mut self.state else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(packs)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process(
|
|
||||||
&mut self,
|
|
||||||
pool: &mut RelayPool,
|
|
||||||
ndb: &Ndb,
|
|
||||||
subs: &mut Subscriptions,
|
|
||||||
unknown_ids: &mut UnknownIds,
|
|
||||||
) {
|
|
||||||
match &self.state {
|
|
||||||
Some(res) => {
|
|
||||||
let Ok(OnboardingState::AwaitingTrustedPksList(filter)) = res else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let txn = Transaction::new(ndb).expect("txns");
|
|
||||||
let Ok(res) = ndb.query(&txn, filter, 1) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if res.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = res.first().expect("checked empty").note_key;
|
|
||||||
|
|
||||||
let new_state = get_trusted_authors(ndb, &txn, key).and_then(|trusted_pks| {
|
|
||||||
let pks: Vec<&[u8; 32]> = trusted_pks.iter().map(|f| f.bytes()).collect();
|
|
||||||
Nip51SetCache::new(pool, ndb, &txn, unknown_ids, vec![follow_packs_filter(pks)])
|
|
||||||
.map(OnboardingState::HaveFollowPacks)
|
|
||||||
.ok_or(OnboardingError::InvalidNip51Set)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.state = Some(new_state);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let filter = vec![trusted_pks_list_filter()];
|
|
||||||
|
|
||||||
let subid = Uuid::new_v4().to_string();
|
|
||||||
pool.subscribe(subid.clone(), filter.clone());
|
|
||||||
subs.subs
|
|
||||||
.insert(subid, crate::subscriptions::SubKind::OneShot);
|
|
||||||
|
|
||||||
let new_state = Some(Ok(OnboardingState::AwaitingTrustedPksList(filter)));
|
|
||||||
self.state = new_state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unsubscribe and clear state
|
|
||||||
pub fn end_onboarding(&mut self, pool: &mut RelayPool, ndb: &mut Ndb) {
|
|
||||||
let Some(Ok(OnboardingState::HaveFollowPacks(state))) = &mut self.state else {
|
|
||||||
self.state = None;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let unified = &state.sub;
|
|
||||||
|
|
||||||
pool.unsubscribe(unified.remote.clone());
|
|
||||||
let _ = ndb.unsubscribe(unified.local);
|
|
||||||
|
|
||||||
self.state = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum OnboardingError {
|
|
||||||
InvalidNip51Set,
|
|
||||||
InvalidTrustedPksListKind,
|
|
||||||
NdbCouldNotFindNote,
|
|
||||||
}
|
|
||||||
|
|
||||||
// author providing the list of trusted follow pack authors
|
|
||||||
const FOLLOW_PACK_AUTHOR: [u8; 32] = [
|
|
||||||
0x89, 0x5c, 0x2a, 0x90, 0xa8, 0x60, 0xac, 0x18, 0x43, 0x4a, 0xa6, 0x9e, 0x7b, 0x0d, 0xa8, 0x46,
|
|
||||||
0x57, 0x21, 0x21, 0x6f, 0xa3, 0x6e, 0x42, 0xc0, 0x22, 0xe3, 0x93, 0x57, 0x9c, 0x48, 0x6c, 0xba,
|
|
||||||
];
|
|
||||||
|
|
||||||
fn trusted_pks_list_filter() -> Filter {
|
|
||||||
Filter::new()
|
|
||||||
.kinds([30000])
|
|
||||||
.limit(1)
|
|
||||||
.authors(&[FOLLOW_PACK_AUTHOR])
|
|
||||||
.tags(["trusted-follow-pack-authors"], 'd')
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn follow_packs_filter(pks: Vec<&[u8; 32]>) -> Filter {
|
|
||||||
Filter::new()
|
|
||||||
.kinds([39089])
|
|
||||||
.limit(default_limit())
|
|
||||||
.authors(pks)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// gets the pubkeys from a kind 30000 follow set
|
|
||||||
fn get_trusted_authors(
|
|
||||||
ndb: &Ndb,
|
|
||||||
txn: &Transaction,
|
|
||||||
key: NoteKey,
|
|
||||||
) -> Result<Vec<Pubkey>, OnboardingError> {
|
|
||||||
let Ok(note) = ndb.get_note_by_key(txn, key) else {
|
|
||||||
return Result::Err(OnboardingError::NdbCouldNotFindNote);
|
|
||||||
};
|
|
||||||
|
|
||||||
if note.kind() != 30000 {
|
|
||||||
return Result::Err(OnboardingError::InvalidTrustedPksListKind);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(nip51set) = create_nip51_set(note) else {
|
|
||||||
return Result::Err(OnboardingError::InvalidNip51Set);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(nip51set.pks)
|
|
||||||
}
|
|
||||||
@@ -22,23 +22,11 @@ pub struct NewPost {
|
|||||||
pub mentions: Vec<Pubkey>,
|
pub mentions: Vec<Pubkey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_variant() -> &'static str {
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
{
|
|
||||||
"Damus Android"
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
{
|
|
||||||
"Damus Notedeck"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
|
fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
|
||||||
builder
|
builder
|
||||||
.start_tag()
|
.start_tag()
|
||||||
.tag_str("client")
|
.tag_str("client")
|
||||||
.tag_str(client_variant())
|
.tag_str("Damus Notedeck")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewPost {
|
impl NewPost {
|
||||||
@@ -56,7 +44,7 @@ impl NewPost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_note(&self, seckey: &[u8; 32]) -> Note<'_> {
|
pub fn to_note(&self, seckey: &[u8; 32]) -> Note {
|
||||||
let mut content = self.content.clone();
|
let mut content = self.content.clone();
|
||||||
append_urls(&mut content, &self.media);
|
append_urls(&mut content, &self.media);
|
||||||
|
|
||||||
@@ -77,7 +65,7 @@ impl NewPost {
|
|||||||
builder.sign(seckey).build().expect("note should be ok")
|
builder.sign(seckey).build().expect("note should be ok")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_reply(&self, seckey: &[u8; 32], replying_to: &Note) -> Note<'_> {
|
pub fn to_reply(&self, seckey: &[u8; 32], replying_to: &Note) -> Note {
|
||||||
let mut content = self.content.clone();
|
let mut content = self.content.clone();
|
||||||
append_urls(&mut content, &self.media);
|
append_urls(&mut content, &self.media);
|
||||||
|
|
||||||
@@ -157,7 +145,7 @@ impl NewPost {
|
|||||||
.expect("expected build to work")
|
.expect("expected build to work")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note<'_> {
|
pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note {
|
||||||
let mut new_content = format!(
|
let mut new_content = format!(
|
||||||
"{}\nnostr:{}",
|
"{}\nnostr:{}",
|
||||||
self.content,
|
self.content,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ impl SaveProfileChanges {
|
|||||||
pub fn new(kp: FullKeypair, state: ProfileState) -> Self {
|
pub fn new(kp: FullKeypair, state: ProfileState) -> Self {
|
||||||
Self { kp, state }
|
Self { kp, state }
|
||||||
}
|
}
|
||||||
pub fn to_note(&self) -> Note<'_> {
|
pub fn to_note(&self) -> Note {
|
||||||
let sec = &self.kp.secret_key.to_secret_bytes();
|
let sec = &self.kp.secret_key.to_secret_bytes();
|
||||||
add_client_tag(NoteBuilder::new())
|
add_client_tag(NoteBuilder::new())
|
||||||
.kind(0)
|
.kind(0)
|
||||||
@@ -218,30 +218,18 @@ fn send_note_builder(builder: NoteBuilder, ndb: &Ndb, pool: &mut RelayPool, kp:
|
|||||||
pool.send(event);
|
pool.send(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_new_contact_list(
|
pub fn send_new_contact_list(kp: FilledKeypair, ndb: &Ndb, pool: &mut RelayPool) {
|
||||||
kp: FilledKeypair,
|
let builder = construct_new_contact_list(kp.pubkey);
|
||||||
ndb: &Ndb,
|
|
||||||
pool: &mut RelayPool,
|
|
||||||
mut pks_to_follow: Vec<Pubkey>,
|
|
||||||
) {
|
|
||||||
if !pks_to_follow.contains(kp.pubkey) {
|
|
||||||
pks_to_follow.push(*kp.pubkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
let builder = construct_new_contact_list(pks_to_follow);
|
|
||||||
|
|
||||||
send_note_builder(builder, ndb, pool, kp);
|
send_note_builder(builder, ndb, pool, kp);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_new_contact_list<'a>(pks: Vec<Pubkey>) -> NoteBuilder<'a> {
|
fn construct_new_contact_list<'a>(pk: &'a Pubkey) -> NoteBuilder<'a> {
|
||||||
let mut builder = NoteBuilder::new()
|
NoteBuilder::new()
|
||||||
.content("")
|
.content("")
|
||||||
.kind(3)
|
.kind(3)
|
||||||
.options(NoteBuildOptions::default());
|
.options(NoteBuildOptions::default())
|
||||||
|
.start_tag()
|
||||||
for pk in pks {
|
.tag_str("p")
|
||||||
builder = builder.start_tag().tag_str("p").tag_str(&pk.hex());
|
.tag_str(&pk.hex())
|
||||||
}
|
|
||||||
|
|
||||||
builder
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,11 +278,6 @@ impl Route {
|
|||||||
"Add Account",
|
"Add Account",
|
||||||
"Column title for adding new account"
|
"Column title for adding new account"
|
||||||
)),
|
)),
|
||||||
AccountsRoute::Onboarding => ColumnTitle::formatted(tr!(
|
|
||||||
i18n,
|
|
||||||
"Onboarding",
|
|
||||||
"Column title for finding users to follow"
|
|
||||||
)),
|
|
||||||
},
|
},
|
||||||
Route::ComposeNote => ColumnTitle::formatted(tr!(
|
Route::ComposeNote => ColumnTitle::formatted(tr!(
|
||||||
i18n,
|
i18n,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
actionbar::TimelineOpenResult,
|
actionbar::TimelineOpenResult,
|
||||||
error::Error,
|
error::Error,
|
||||||
timeline::{Timeline, TimelineKind, UnknownPksOwned},
|
timeline::{Timeline, TimelineKind},
|
||||||
};
|
};
|
||||||
|
|
||||||
use notedeck::{filter, FilterState, NoteCache, NoteRef};
|
use notedeck::{filter, FilterState, NoteCache, NoteRef};
|
||||||
@@ -90,19 +90,17 @@ 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 None;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// insert initial notes into timeline
|
// insert initial notes into timeline
|
||||||
let res = timeline.insert_new(txn, ndb, note_cache, notes);
|
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) {
|
||||||
@@ -115,22 +113,19 @@ impl TimelineCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get and/or update the notes associated with this timeline
|
/// Get and/or update the notes associated with this timeline
|
||||||
fn notes<'a>(
|
pub 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,
|
||||||
) -> GetNotesResponse<'a> {
|
) -> Vitality<'a, Timeline> {
|
||||||
// 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 GetNotesResponse {
|
return Vitality::Stale(self.get_expected_mut(id));
|
||||||
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) {
|
||||||
@@ -154,12 +149,9 @@ impl TimelineCache {
|
|||||||
info!("found NotesHolder with {} notes", notes.len());
|
info!("found NotesHolder with {} notes", notes.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
let unknown_pks = self.insert_new(id.to_owned(), txn, ndb, ¬es, note_cache);
|
self.insert_new(id.to_owned(), txn, ndb, ¬es, note_cache);
|
||||||
|
|
||||||
GetNotesResponse {
|
Vitality::Fresh(self.get_expected_mut(id))
|
||||||
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
|
||||||
@@ -174,12 +166,11 @@ impl TimelineCache {
|
|||||||
pool: &mut RelayPool,
|
pool: &mut RelayPool,
|
||||||
id: &TimelineKind,
|
id: &TimelineKind,
|
||||||
) -> Option<TimelineOpenResult> {
|
) -> Option<TimelineOpenResult> {
|
||||||
let notes_resp = self.notes(ndb, note_cache, txn, id);
|
let (open_result, timeline) = match 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_entries().latest(),
|
timeline.all_or_any_notes(),
|
||||||
timeline.subscription.get_filter()?.local(),
|
timeline.subscription.get_filter()?.local(),
|
||||||
txn,
|
txn,
|
||||||
ndb,
|
ndb,
|
||||||
@@ -216,13 +207,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,22 +231,18 @@ 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(
|
||||||
latest: Option<&NoteRef>,
|
notes: &[NoteRef],
|
||||||
filters: &[Filter],
|
filters: &[Filter],
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
ndb: &Ndb,
|
ndb: &Ndb,
|
||||||
) -> Vec<NoteRef> {
|
) -> Vec<NoteRef> {
|
||||||
let Some(last_note) = latest else {
|
if notes.is_empty() {
|
||||||
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) {
|
||||||
|
|||||||
@@ -532,7 +532,7 @@ impl TimelineKind {
|
|||||||
let contact_filter = contacts_filter(pk.bytes());
|
let contact_filter = contacts_filter(pk.bytes());
|
||||||
|
|
||||||
let results = ndb
|
let results = ndb
|
||||||
.query(txn, std::slice::from_ref(&contact_filter), 1)
|
.query(txn, &[contact_filter.clone()], 1)
|
||||||
.expect("contact query failed?");
|
.expect("contact query failed?");
|
||||||
|
|
||||||
let kind_fn = TimelineKind::last_per_pubkey;
|
let kind_fn = TimelineKind::last_per_pubkey;
|
||||||
@@ -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, 7])
|
.kinds([1])
|
||||||
.limit(default_limit())
|
.limit(default_limit())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -681,7 +681,7 @@ fn contact_filter_state(txn: &Transaction, ndb: &Ndb, pk: &Pubkey) -> FilterStat
|
|||||||
let contact_filter = contacts_filter(pk);
|
let contact_filter = contacts_filter(pk);
|
||||||
|
|
||||||
let results = ndb
|
let results = ndb
|
||||||
.query(txn, std::slice::from_ref(&contact_filter), 1)
|
.query(txn, &[contact_filter.clone()], 1)
|
||||||
.expect("contact query failed?");
|
.expect("contact query failed?");
|
||||||
|
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
@@ -706,7 +706,7 @@ fn last_per_pubkey_filter_state(ndb: &Ndb, pk: &Pubkey) -> FilterState {
|
|||||||
|
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
let txn = Transaction::new(ndb).expect("txn");
|
||||||
let results = ndb
|
let results = ndb
|
||||||
.query(&txn, std::slice::from_ref(&contact_filter), 1)
|
.query(&txn, &[contact_filter.clone()], 1)
|
||||||
.expect("contact query failed?");
|
.expect("contact query failed?");
|
||||||
|
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
|
|||||||
@@ -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, note_units::InsertManyResponse, timeline_units::NotePayload},
|
timeline::kind::ListKind,
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ 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};
|
||||||
@@ -28,17 +27,37 @@ 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};
|
|
||||||
pub use timeline_units::{TimelineUnits, UnknownPks};
|
//#[derive(Debug, Hash, Clone, Eq, PartialEq)]
|
||||||
pub use unit::{CompositeUnit, NoteUnit, ReactionUnit};
|
//pub type TimelineId = TimelineKind;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -84,7 +103,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 units: TimelineUnits,
|
pub notes: Vec<NoteRef>,
|
||||||
pub selection: i32,
|
pub selection: i32,
|
||||||
pub filter: ViewFilter,
|
pub filter: ViewFilter,
|
||||||
pub list: Rc<RefCell<VirtualList>>,
|
pub list: Rc<RefCell<VirtualList>>,
|
||||||
@@ -117,9 +136,10 @@ 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 {
|
||||||
units: TimelineUnits::with_capacity(cap),
|
notes,
|
||||||
selection,
|
selection,
|
||||||
filter,
|
filter,
|
||||||
list,
|
list,
|
||||||
@@ -127,54 +147,45 @@ impl TimelineTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert<'a>(
|
fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) {
|
||||||
&mut self,
|
if new_refs.is_empty() {
|
||||||
payloads: Vec<&'a NotePayload>,
|
return;
|
||||||
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);
|
||||||
|
|
||||||
let num_refs = payloads.len();
|
self.notes = notes;
|
||||||
|
let new_items = self.notes.len() - num_prev_items;
|
||||||
|
|
||||||
let resp = self.units.merge_new_notes(payloads, ndb, txn);
|
// TODO: technically items could have been added inbetween
|
||||||
|
if new_items > 0 {
|
||||||
|
let mut list = self.list.borrow_mut();
|
||||||
|
|
||||||
let InsertManyResponse::Some {
|
match merge_kind {
|
||||||
entries_merged,
|
// TODO: update egui_virtual_list to support spliced inserts
|
||||||
merge_kind,
|
MergeKind::Spliced => {
|
||||||
} = resp.insertion_response
|
debug!(
|
||||||
else {
|
"spliced when inserting {} new notes, resetting virtual list",
|
||||||
return resp.tl_response;
|
new_refs.len()
|
||||||
};
|
);
|
||||||
|
list.reset();
|
||||||
let mut list = self.list.borrow_mut();
|
}
|
||||||
|
MergeKind::FrontInsert => {
|
||||||
match merge_kind {
|
// only run this logic if we're reverse-chronological
|
||||||
// TODO: update egui_virtual_list to support spliced inserts
|
// reversed in this case means chronological, since the
|
||||||
MergeKind::Spliced => {
|
// default is reverse-chronological. yeah it's confusing.
|
||||||
debug!("spliced when inserting {num_refs} new notes, resetting virtual list",);
|
if !reversed {
|
||||||
list.reset();
|
debug!("inserting {} new notes at start", new_refs.len());
|
||||||
}
|
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.units.len() as i32 {
|
if self.selection + 1 > self.notes.len() as i32 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,14 +202,6 @@ 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 {
|
||||||
@@ -290,20 +293,15 @@ 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_entries(&self) -> &TimelineUnits {
|
pub fn all_or_any_notes(&self) -> &[NoteRef] {
|
||||||
self.entries(ViewFilter::NotesAndReplies)
|
self.notes(ViewFilter::NotesAndReplies).unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| {
|
self.notes(ViewFilter::Notes)
|
||||||
self.entries(ViewFilter::Notes)
|
.expect("should have at least notes")
|
||||||
.expect("should have at least notes")
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entries(&self, view: ViewFilter) -> Option<&TimelineUnits> {
|
pub fn notes(&self, view: ViewFilter) -> Option<&[NoteRef]> {
|
||||||
self.view(view).map(|v| &v.units)
|
self.view(view).map(|v| &*v.notes)
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
||||||
@@ -322,7 +320,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> =
|
||||||
@@ -330,7 +328,6 @@ 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) {
|
||||||
@@ -338,32 +335,11 @@ 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,
|
||||||
) {
|
) {
|
||||||
if let Some(resp) = self.views[view]
|
self.views[view].notes.push(*note_ref)
|
||||||
.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
|
||||||
@@ -378,7 +354,7 @@ impl Timeline {
|
|||||||
note_cache: &mut NoteCache,
|
note_cache: &mut NoteCache,
|
||||||
reversed: bool,
|
reversed: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut payloads: Vec<NotePayload> = Vec::with_capacity(new_note_ids.len());
|
let mut new_refs: Vec<(Note, NoteRef)> = 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) {
|
||||||
@@ -395,32 +371,35 @@ 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);
|
||||||
|
|
||||||
payloads.push(NotePayload { note, key: *key });
|
let created_at = note.created_at();
|
||||||
|
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 res: Vec<&NotePayload<'_>> = payloads.iter().collect();
|
let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
|
||||||
if let Some(res) = view.insert(res, ndb, txn, reversed) {
|
|
||||||
res.process(unknown_ids, ndb, txn);
|
view.insert(&refs, reversed);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewFilter::Notes => {
|
ViewFilter::Notes => {
|
||||||
let mut filtered_payloads = Vec::with_capacity(payloads.len());
|
let mut filtered_refs = Vec::with_capacity(new_refs.len());
|
||||||
for payload in &payloads {
|
for (note, nr) in &new_refs {
|
||||||
let cached_note =
|
let cached_note = note_cache.cached_note_or_insert(nr.key, note);
|
||||||
note_cache.cached_note_or_insert(payload.key, &payload.note);
|
|
||||||
|
|
||||||
if ViewFilter::filter_notes(cached_note, &payload.note) {
|
if ViewFilter::filter_notes(cached_note, note) {
|
||||||
filtered_payloads.push(payload);
|
filtered_refs.push(*nr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(res) = view.insert(filtered_payloads, ndb, txn, reversed) {
|
view.insert(&filtered_refs, reversed);
|
||||||
res.process(unknown_ids, ndb, txn);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,18 +436,6 @@ 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,
|
||||||
@@ -525,11 +492,10 @@ 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, unknown_ids) {
|
if is_timeline_ready(ndb, pool, note_cache, timeline, accounts) {
|
||||||
if let Err(err) = setup_timeline_nostrdb_sub(ndb, txn, note_cache, timeline, unknown_ids) {
|
if let Err(err) = setup_timeline_nostrdb_sub(ndb, txn, note_cache, timeline) {
|
||||||
error!("setup_new_timeline: {err}");
|
error!("setup_new_timeline: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,7 +564,7 @@ pub fn send_initial_timeline_filter(
|
|||||||
filter = filter.limit_mut(lim);
|
filter = filter.limit_mut(lim);
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries = timeline.all_or_any_entries();
|
let notes = timeline.all_or_any_notes();
|
||||||
|
|
||||||
// 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
|
||||||
@@ -606,8 +572,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, entries.len()) {
|
if can_since_optimize && filter::should_since_optimize(lim, notes.len()) {
|
||||||
filter = filter::since_optimize_filter(filter, entries.latest());
|
filter = filter::since_optimize_filter(filter, notes);
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
@@ -627,14 +593,18 @@ pub fn send_initial_timeline_filter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we need some data first
|
// we need some data first
|
||||||
FilterState::NeedsRemote => fetch_contact_list(subs, timeline, accounts),
|
FilterState::NeedsRemote => fetch_contact_list(subs, relay, timeline, accounts),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_contact_list(subs: &mut Subscriptions, timeline: &mut Timeline, accounts: &Accounts) {
|
pub fn fetch_contact_list(
|
||||||
if timeline.filter.get_any_ready().is_some() {
|
subs: &mut Subscriptions,
|
||||||
return;
|
relay: &mut PoolRelay,
|
||||||
}
|
timeline: &mut Timeline,
|
||||||
|
accounts: &Accounts,
|
||||||
|
) {
|
||||||
|
let sub_kind = SubKind::FetchingContactList(timeline.kind.clone());
|
||||||
|
let sub = &accounts.get_subs().contacts;
|
||||||
|
|
||||||
let new_filter_state = match accounts.get_selected_account().data.contacts.get_state() {
|
let new_filter_state = match accounts.get_selected_account().data.contacts.get_state() {
|
||||||
ContactState::Unreceived => {
|
ContactState::Unreceived => {
|
||||||
@@ -647,14 +617,10 @@ pub fn fetch_contact_list(subs: &mut Subscriptions, timeline: &mut Timeline, acc
|
|||||||
} => FilterState::GotRemote(filter::GotRemoteType::Contact),
|
} => FilterState::GotRemote(filter::GotRemoteType::Contact),
|
||||||
};
|
};
|
||||||
|
|
||||||
timeline.filter.set_all_states(new_filter_state);
|
timeline
|
||||||
|
.filter
|
||||||
|
.set_relay_state(relay.url().to_string(), new_filter_state);
|
||||||
|
|
||||||
let sub = &accounts.get_subs().contacts;
|
|
||||||
if subs.subs.contains_key(&sub.remote) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sub_kind = SubKind::FetchingContactList(timeline.kind.clone());
|
|
||||||
subs.subs.insert(sub.remote.clone(), sub_kind);
|
subs.subs.insert(sub.remote.clone(), sub_kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,7 +629,6 @@ 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
|
||||||
@@ -689,9 +654,7 @@ fn setup_initial_timeline(
|
|||||||
.map(NoteRef::from_query_result)
|
.map(NoteRef::from_query_result)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if let Some(pks) = timeline.insert_new(txn, ndb, note_cache, ¬es) {
|
timeline.insert_new(txn, ndb, note_cache, ¬es);
|
||||||
pks.process(ndb, txn, unknown_ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -700,11 +663,10 @@ 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, unknown_ids) {
|
if let Err(err) = setup_timeline_nostrdb_sub(ndb, &txn, note_cache, timeline) {
|
||||||
error!("setup_initial_nostrdb_subs: {err}");
|
error!("setup_initial_nostrdb_subs: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -717,7 +679,6 @@ 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
|
||||||
@@ -725,7 +686,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, unknown_ids, &filter_state)?;
|
setup_initial_timeline(ndb, txn, timeline, note_cache, &filter_state)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -740,7 +701,6 @@ 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
|
||||||
@@ -814,8 +774,7 @@ 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, unknown_ids, &filter)
|
setup_initial_timeline(ndb, &txn, timeline, note_cache, &filter).expect("setup init");
|
||||||
.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()));
|
||||||
|
|||||||
@@ -1,559 +0,0 @@
|
|||||||
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,3 +1,8 @@
|
|||||||
|
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};
|
||||||
@@ -8,17 +13,16 @@ 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::{note_units::NoteUnits, unit::NoteUnit, InsertionResponse},
|
timeline::MergeKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::ThreadSelection;
|
use super::ThreadSelection;
|
||||||
|
|
||||||
pub struct ThreadNode {
|
pub struct ThreadNode {
|
||||||
pub replies: SingleNoteUnits,
|
pub replies: HybridSet<NoteRef>,
|
||||||
pub prev: ParentState,
|
pub prev: ParentState,
|
||||||
pub have_all_ancestors: bool,
|
pub have_all_ancestors: bool,
|
||||||
pub list: VirtualList,
|
pub list: VirtualList,
|
||||||
pub set_scroll_offset: Option<f32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -28,20 +32,107 @@ pub enum ParentState {
|
|||||||
Parent(NoteId),
|
Parent(NoteId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThreadNode {
|
/// Affords:
|
||||||
pub fn new(parent: ParentState) -> Self {
|
/// - 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 {
|
Self {
|
||||||
replies: SingleNoteUnits::new(true),
|
reversed: Default::default(),
|
||||||
prev: parent,
|
lookup: Default::default(),
|
||||||
have_all_ancestors: false,
|
ordered: Default::default(),
|
||||||
list: VirtualList::new(),
|
}
|
||||||
set_scroll_offset: None,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 with_offset(mut self, offset: f32) -> Self {
|
pub fn new(reversed: bool) -> Self {
|
||||||
self.set_scroll_offset = Some(offset);
|
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 {
|
||||||
|
pub fn new(parent: ParentState) -> Self {
|
||||||
|
Self {
|
||||||
|
replies: HybridSet::new(true),
|
||||||
|
prev: parent,
|
||||||
|
have_all_ancestors: false,
|
||||||
|
list: VirtualList::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +147,6 @@ pub struct Threads {
|
|||||||
impl Threads {
|
impl Threads {
|
||||||
/// Opening a thread.
|
/// Opening a thread.
|
||||||
/// Similar to [[super::cache::TimelineCache::open]]
|
/// Similar to [[super::cache::TimelineCache::open]]
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn open(
|
pub fn open(
|
||||||
&mut self,
|
&mut self,
|
||||||
ndb: &mut Ndb,
|
ndb: &mut Ndb,
|
||||||
@@ -65,7 +155,6 @@ impl Threads {
|
|||||||
thread: &ThreadSelection,
|
thread: &ThreadSelection,
|
||||||
new_scope: bool,
|
new_scope: bool,
|
||||||
col: usize,
|
col: usize,
|
||||||
scroll_offset: f32,
|
|
||||||
) -> Option<NewThreadNotes> {
|
) -> Option<NewThreadNotes> {
|
||||||
tracing::info!("Opening thread: {:?}", thread);
|
tracing::info!("Opening thread: {:?}", thread);
|
||||||
let local_sub_filter = if let Some(selected) = &thread.selected_note {
|
let local_sub_filter = if let Some(selected) = &thread.selected_note {
|
||||||
@@ -95,7 +184,7 @@ impl Threads {
|
|||||||
RawEntryMut::Vacant(entry) => {
|
RawEntryMut::Vacant(entry) => {
|
||||||
let id = NoteId::new(*selected_note_id);
|
let id = NoteId::new(*selected_note_id);
|
||||||
|
|
||||||
let node = ThreadNode::new(ParentState::Unknown).with_offset(scroll_offset);
|
let node = ThreadNode::new(ParentState::Unknown);
|
||||||
entry.insert(id, node);
|
entry.insert(id, node);
|
||||||
|
|
||||||
&local_sub_filter
|
&local_sub_filter
|
||||||
@@ -389,34 +478,3 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
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],
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
use nostrdb::Transaction;
|
|
||||||
use notedeck::AppContext;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
timeline::{kind::ListKind, TimelineKind},
|
|
||||||
Damus, Route,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO(kernelkind): should account for mutes
|
|
||||||
pub fn unseen_notification(
|
|
||||||
columns: &mut Damus,
|
|
||||||
ndb: &nostrdb::Ndb,
|
|
||||||
current_pk: notedeck::enostr::Pubkey,
|
|
||||||
) -> bool {
|
|
||||||
let Some(tl) = columns
|
|
||||||
.timeline_cache
|
|
||||||
.get_mut(&TimelineKind::Notifications(current_pk))
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let freshness = &mut tl.current_view_mut().freshness;
|
|
||||||
freshness.update(|timestamp_last_viewed| {
|
|
||||||
let filter = crate::timeline::kind::notifications_filter(¤t_pk)
|
|
||||||
.since_mut(timestamp_last_viewed);
|
|
||||||
let txn = Transaction::new(ndb).expect("txn");
|
|
||||||
|
|
||||||
let Some(res) = ndb.query(&txn, &[filter], 1).ok() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
!res.is_empty()
|
|
||||||
});
|
|
||||||
|
|
||||||
freshness.has_unseen()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When you click the toolbar button, these actions
|
|
||||||
/// are returned
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
pub enum ToolbarAction {
|
|
||||||
Notifications,
|
|
||||||
Search,
|
|
||||||
Home,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToolbarAction {
|
|
||||||
pub fn process(&self, app: &mut Damus, ctx: &mut AppContext) {
|
|
||||||
let cur_acc_pk = ctx.accounts.get_selected_account().key.pubkey;
|
|
||||||
let route = match &self {
|
|
||||||
ToolbarAction::Notifications => {
|
|
||||||
Route::timeline(TimelineKind::Notifications(cur_acc_pk))
|
|
||||||
}
|
|
||||||
ToolbarAction::Search => Route::Search,
|
|
||||||
ToolbarAction::Home => {
|
|
||||||
Route::timeline(TimelineKind::List(ListKind::Contact(cur_acc_pk)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(cols) = app.decks_cache.active_columns_mut(ctx.i18n, ctx.accounts) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
match cols.select_by_route(route) {
|
|
||||||
crate::column::SelectionResult::AlreadySelected(_) => {} // great! no need to go to top yet
|
|
||||||
crate::column::SelectionResult::NewSelection(_) => {
|
|
||||||
// we already selected this, so scroll to top
|
|
||||||
app.scroll_to_top();
|
|
||||||
}
|
|
||||||
crate::column::SelectionResult::Failed => {
|
|
||||||
// oh no, something went wrong
|
|
||||||
// TODO(jb55): handle tab selection failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::login_manager::AcquireKeyState;
|
use crate::login_manager::AcquireKeyState;
|
||||||
use crate::ui::onboarding::FollowPacksResponse;
|
|
||||||
use crate::ui::{Preview, PreviewConfig};
|
use crate::ui::{Preview, PreviewConfig};
|
||||||
use egui::{
|
use egui::{
|
||||||
Align, Button, Color32, Frame, InnerResponse, Layout, Margin, RichText, TextEdit, Vec2,
|
Align, Button, Color32, Frame, InnerResponse, Layout, Margin, RichText, TextEdit, Vec2,
|
||||||
@@ -19,8 +18,7 @@ pub struct AccountLoginView<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum AccountLoginResponse {
|
pub enum AccountLoginResponse {
|
||||||
CreatingNew,
|
CreateNew,
|
||||||
Onboarding(FollowPacksResponse),
|
|
||||||
LoginWith(Keypair),
|
LoginWith(Keypair),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +57,7 @@ impl<'a> AccountLoginView<'a> {
|
|||||||
let text_edit_width = available_width - button_width;
|
let text_edit_width = available_width - button_width;
|
||||||
|
|
||||||
let textedit_resp = ui.add_sized([text_edit_width, 40.0], login_textedit(self.manager, self.i18n));
|
let textedit_resp = ui.add_sized([text_edit_width, 40.0], login_textedit(self.manager, self.i18n));
|
||||||
input_context(ui, &textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear);
|
input_context(&textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear);
|
||||||
|
|
||||||
if eye_button(ui, self.manager.password_visible()).clicked() {
|
if eye_button(ui, self.manager.password_visible()).clicked() {
|
||||||
self.manager.toggle_password_visibility();
|
self.manager.toggle_password_visibility();
|
||||||
@@ -98,7 +96,7 @@ impl<'a> AccountLoginView<'a> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if self.manager.check_for_create_new() {
|
if self.manager.check_for_create_new() {
|
||||||
return Some(AccountLoginResponse::CreatingNew);
|
return Some(AccountLoginResponse::CreateNew);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(keypair) = self.manager.get_login_keypair() {
|
if let Some(keypair) = self.manager.get_login_keypair() {
|
||||||
|
|||||||
@@ -709,7 +709,6 @@ 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)
|
||||||
@@ -750,7 +749,6 @@ 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)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ pub mod edit_deck;
|
|||||||
pub mod images;
|
pub mod images;
|
||||||
pub mod mentions_picker;
|
pub mod mentions_picker;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
pub mod onboarding;
|
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
@@ -18,7 +17,6 @@ pub mod side_panel;
|
|||||||
pub mod support;
|
pub mod support;
|
||||||
pub mod thread;
|
pub mod thread;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
pub mod toolbar;
|
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
@@ -28,6 +26,7 @@ pub use preview::{Preview, PreviewApp, PreviewConfig};
|
|||||||
pub use profile::ProfileView;
|
pub use profile::ProfileView;
|
||||||
pub use relay::RelayView;
|
pub use relay::RelayView;
|
||||||
pub use settings::SettingsView;
|
pub use settings::SettingsView;
|
||||||
|
pub use settings::ShowSourceClientOption;
|
||||||
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
||||||
pub use thread::ThreadView;
|
pub use thread::ThreadView;
|
||||||
pub use timeline::TimelineView;
|
pub use timeline::TimelineView;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use crate::draft::{Draft, Drafts, MentionHint};
|
use crate::draft::{Draft, Drafts, MentionHint};
|
||||||
use crate::media_upload::nostrbuild_nip96_upload;
|
#[cfg(not(target_os = "android"))]
|
||||||
|
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,
|
||||||
@@ -13,23 +15,19 @@ use egui::{
|
|||||||
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
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;
|
|
||||||
#[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>,
|
||||||
@@ -39,7 +37,6 @@ pub struct PostView<'a, 'd> {
|
|||||||
inner_rect: egui::Rect,
|
inner_rect: egui::Rect,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
jobs: &'a mut JobsCache,
|
jobs: &'a mut JobsCache,
|
||||||
animation_mode: AnimationMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -113,11 +110,6 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
jobs: &'a mut JobsCache,
|
jobs: &'a mut JobsCache,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let animation_mode = if note_options.contains(NoteOptions::NoAnimations) {
|
|
||||||
AnimationMode::NoAnimation
|
|
||||||
} else {
|
|
||||||
AnimationMode::Continuous { fps: None }
|
|
||||||
};
|
|
||||||
PostView {
|
PostView {
|
||||||
note_context,
|
note_context,
|
||||||
draft,
|
draft,
|
||||||
@@ -125,7 +117,6 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
post_type,
|
post_type,
|
||||||
inner_rect,
|
inner_rect,
|
||||||
note_options,
|
note_options,
|
||||||
animation_mode,
|
|
||||||
jobs,
|
jobs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,11 +129,6 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
PostView::id().with("scroll")
|
PostView::id().with("scroll")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn animation_mode(mut self, animation_mode: AnimationMode) -> Self {
|
|
||||||
self.animation_mode = animation_mode;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
|
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
|
||||||
ui.spacing_mut().item_spacing.x = 12.0;
|
ui.spacing_mut().item_spacing.x = 12.0;
|
||||||
|
|
||||||
@@ -210,7 +196,6 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
let out = textedit.show(ui);
|
let out = textedit.show(ui);
|
||||||
|
|
||||||
input_context(
|
input_context(
|
||||||
ui,
|
|
||||||
&out.response,
|
&out.response,
|
||||||
self.note_context.clipboard,
|
self.note_context.clipboard,
|
||||||
&mut self.draft.buffer.text_buffer,
|
&mut self.draft.buffer.text_buffer,
|
||||||
@@ -342,22 +327,6 @@ 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))
|
||||||
@@ -523,7 +492,6 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
height,
|
height,
|
||||||
cur_state,
|
cur_state,
|
||||||
url,
|
url,
|
||||||
self.animation_mode,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
to_remove.reverse();
|
to_remove.reverse();
|
||||||
@@ -538,14 +506,22 @@ 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 {
|
||||||
emit_selected_file(SelectedMedia::from_path(file));
|
match MediaPath::new(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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,7 +582,6 @@ fn render_post_view_media(
|
|||||||
height: u32,
|
height: u32,
|
||||||
render_state: RenderState,
|
render_state: RenderState,
|
||||||
url: &str,
|
url: &str,
|
||||||
animation_mode: AnimationMode,
|
|
||||||
) {
|
) {
|
||||||
match render_state.texture_state {
|
match render_state.texture_state {
|
||||||
notedeck::TextureState::Pending => {
|
notedeck::TextureState::Pending => {
|
||||||
@@ -630,7 +605,7 @@ fn render_post_view_media(
|
|||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
let texture_handle =
|
let texture_handle =
|
||||||
ensure_latest_texture(ui, url, render_state.gifs, renderable_media, animation_mode);
|
ensure_latest_texture(ui, url, render_state.gifs, renderable_media);
|
||||||
let img_resp = ui.add(
|
let img_resp = ui.add(
|
||||||
egui::Image::new(&texture_handle)
|
egui::Image::new(&texture_handle)
|
||||||
.max_size(size)
|
.max_size(size)
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
|||||||
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
|
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt(self.scroll_id)
|
.id_salt(self.scroll_id)
|
||||||
.stick_to_bottom(true)
|
|
||||||
.show(ui, |ui| self.show_internal(ui))
|
.show(ui, |ui| self.show_internal(ui))
|
||||||
.inner
|
.inner
|
||||||
}
|
}
|
||||||
@@ -122,7 +121,7 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
|||||||
// large and things start breaking. I think this is an ok
|
// large and things start breaking. I think this is an ok
|
||||||
// solution but there could be a better one.
|
// solution but there could be a better one.
|
||||||
//
|
//
|
||||||
//ui.add_space(500.0);
|
ui.add_space(500.0);
|
||||||
|
|
||||||
post_response
|
post_response
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
use std::mem;
|
|
||||||
|
|
||||||
use egui::{Layout, ScrollArea};
|
|
||||||
use nostrdb::Ndb;
|
|
||||||
use notedeck::{Images, JobPool, JobsCache, Localization};
|
|
||||||
use notedeck_ui::{
|
|
||||||
colors,
|
|
||||||
nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetAction, Nip51SetWidgetFlags},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{onboarding::Onboarding, ui::widgets::styled_button};
|
|
||||||
|
|
||||||
/// Display Follow Packs for the user to choose from authors trusted by the Damus team
|
|
||||||
pub struct FollowPackOnboardingView<'a> {
|
|
||||||
onboarding: &'a mut Onboarding,
|
|
||||||
ui_state: &'a mut Nip51SetUiCache,
|
|
||||||
ndb: &'a Ndb,
|
|
||||||
images: &'a mut Images,
|
|
||||||
loc: &'a mut Localization,
|
|
||||||
job_pool: &'a mut JobPool,
|
|
||||||
jobs: &'a mut JobsCache,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum OnboardingResponse {
|
|
||||||
FollowPacks(FollowPacksResponse),
|
|
||||||
ViewProfile(enostr::Pubkey),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum FollowPacksResponse {
|
|
||||||
NoFollowPacks,
|
|
||||||
UserSelectedPacks(Nip51SetUiCache),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> FollowPackOnboardingView<'a> {
|
|
||||||
pub fn new(
|
|
||||||
onboarding: &'a mut Onboarding,
|
|
||||||
ui_state: &'a mut Nip51SetUiCache,
|
|
||||||
ndb: &'a Ndb,
|
|
||||||
images: &'a mut Images,
|
|
||||||
loc: &'a mut Localization,
|
|
||||||
job_pool: &'a mut JobPool,
|
|
||||||
jobs: &'a mut JobsCache,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
onboarding,
|
|
||||||
ui_state,
|
|
||||||
ndb,
|
|
||||||
images,
|
|
||||||
loc,
|
|
||||||
job_pool,
|
|
||||||
jobs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scroll_id() -> egui::Id {
|
|
||||||
egui::Id::new("follow_pack_onboarding")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<OnboardingResponse> {
|
|
||||||
let Some(follow_pack_state) = self.onboarding.get_follow_packs() else {
|
|
||||||
return Some(OnboardingResponse::FollowPacks(
|
|
||||||
FollowPacksResponse::NoFollowPacks,
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
let max_height = ui.available_height() - 48.0;
|
|
||||||
|
|
||||||
let mut action = None;
|
|
||||||
ScrollArea::vertical()
|
|
||||||
.id_salt(Self::scroll_id())
|
|
||||||
.max_height(max_height)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
egui::Frame::new().inner_margin(8.0).show(ui, |ui| {
|
|
||||||
self.onboarding.list.borrow_mut().ui_custom_layout(
|
|
||||||
ui,
|
|
||||||
follow_pack_state.len(),
|
|
||||||
|ui, index| {
|
|
||||||
let resp = Nip51SetWidget::new(
|
|
||||||
follow_pack_state,
|
|
||||||
self.ui_state,
|
|
||||||
self.ndb,
|
|
||||||
self.loc,
|
|
||||||
self.images,
|
|
||||||
self.job_pool,
|
|
||||||
self.jobs,
|
|
||||||
)
|
|
||||||
.with_flags(Nip51SetWidgetFlags::TRUST_IMAGES)
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
|
|
||||||
ui.add_space(4.0);
|
|
||||||
if ui.add(styled_button("Done", colors::PINK)).clicked() {
|
|
||||||
action = Some(OnboardingResponse::FollowPacks(
|
|
||||||
FollowPacksResponse::UserSelectedPacks(mem::take(self.ui_state)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
use core::f32;
|
use core::f32;
|
||||||
|
|
||||||
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
|
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
|
||||||
use egui_winit::clipboard::Clipboard;
|
|
||||||
use enostr::ProfileState;
|
use enostr::ProfileState;
|
||||||
use notedeck::{profile::unwrap_profile_url, tr, Images, Localization, NotedeckTextStyle};
|
use notedeck::{profile::unwrap_profile_url, tr, Images, Localization, NotedeckTextStyle};
|
||||||
use notedeck_ui::context_menu::{input_context, PasteBehavior};
|
|
||||||
use notedeck_ui::{profile::banner, ProfilePic};
|
use notedeck_ui::{profile::banner, ProfilePic};
|
||||||
|
|
||||||
pub struct EditProfileView<'a> {
|
pub struct EditProfileView<'a> {
|
||||||
state: &'a mut ProfileState,
|
state: &'a mut ProfileState,
|
||||||
clipboard: &'a mut Clipboard,
|
|
||||||
img_cache: &'a mut Images,
|
img_cache: &'a mut Images,
|
||||||
i18n: &'a mut Localization,
|
i18n: &'a mut Localization,
|
||||||
}
|
}
|
||||||
@@ -19,13 +16,11 @@ impl<'a> EditProfileView<'a> {
|
|||||||
i18n: &'a mut Localization,
|
i18n: &'a mut Localization,
|
||||||
state: &'a mut ProfileState,
|
state: &'a mut ProfileState,
|
||||||
img_cache: &'a mut Images,
|
img_cache: &'a mut Images,
|
||||||
clipboard: &'a mut Clipboard,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
i18n,
|
i18n,
|
||||||
state,
|
state,
|
||||||
img_cache,
|
img_cache,
|
||||||
clipboard,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +32,6 @@ impl<'a> EditProfileView<'a> {
|
|||||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
|
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
.id_salt(EditProfileView::scroll_id())
|
.id_salt(EditProfileView::scroll_id())
|
||||||
.stick_to_bottom(true)
|
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
banner(ui, self.state.banner(), 188.0);
|
banner(ui, self.state.banner(), 188.0);
|
||||||
|
|
||||||
@@ -101,14 +95,14 @@ impl<'a> EditProfileView<'a> {
|
|||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
));
|
));
|
||||||
singleline_textedit(ui, self.state.str_mut("display_name"), self.clipboard);
|
ui.add(singleline_textedit(self.state.str_mut("display_name")));
|
||||||
});
|
});
|
||||||
|
|
||||||
in_frame(ui, |ui| {
|
in_frame(ui, |ui| {
|
||||||
ui.add(label(
|
ui.add(label(
|
||||||
tr!(self.i18n, "Username", "Profile username field label").as_str(),
|
tr!(self.i18n, "Username", "Profile username field label").as_str(),
|
||||||
));
|
));
|
||||||
singleline_textedit(ui, self.state.str_mut("name"), self.clipboard);
|
ui.add(singleline_textedit(self.state.str_mut("name")));
|
||||||
});
|
});
|
||||||
|
|
||||||
in_frame(ui, |ui| {
|
in_frame(ui, |ui| {
|
||||||
@@ -120,28 +114,28 @@ impl<'a> EditProfileView<'a> {
|
|||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
));
|
));
|
||||||
multiline_textedit(ui, self.state.str_mut("picture"), self.clipboard);
|
ui.add(multiline_textedit(self.state.str_mut("picture")));
|
||||||
});
|
});
|
||||||
|
|
||||||
in_frame(ui, |ui| {
|
in_frame(ui, |ui| {
|
||||||
ui.add(label(
|
ui.add(label(
|
||||||
tr!(self.i18n, "Banner", "Profile banner URL field label").as_str(),
|
tr!(self.i18n, "Banner", "Profile banner URL field label").as_str(),
|
||||||
));
|
));
|
||||||
multiline_textedit(ui, self.state.str_mut("banner"), self.clipboard);
|
ui.add(multiline_textedit(self.state.str_mut("banner")));
|
||||||
});
|
});
|
||||||
|
|
||||||
in_frame(ui, |ui| {
|
in_frame(ui, |ui| {
|
||||||
ui.add(label(
|
ui.add(label(
|
||||||
tr!(self.i18n, "About", "Profile about/bio field label").as_str(),
|
tr!(self.i18n, "About", "Profile about/bio field label").as_str(),
|
||||||
));
|
));
|
||||||
multiline_textedit(ui, self.state.str_mut("about"), self.clipboard);
|
ui.add(multiline_textedit(self.state.str_mut("about")));
|
||||||
});
|
});
|
||||||
|
|
||||||
in_frame(ui, |ui| {
|
in_frame(ui, |ui| {
|
||||||
ui.add(label(
|
ui.add(label(
|
||||||
tr!(self.i18n, "Website", "Profile website field label").as_str(),
|
tr!(self.i18n, "Website", "Profile website field label").as_str(),
|
||||||
));
|
));
|
||||||
singleline_textedit(ui, self.state.str_mut("website"), self.clipboard);
|
ui.add(singleline_textedit(self.state.str_mut("website")));
|
||||||
});
|
});
|
||||||
|
|
||||||
in_frame(ui, |ui| {
|
in_frame(ui, |ui| {
|
||||||
@@ -153,7 +147,7 @@ impl<'a> EditProfileView<'a> {
|
|||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
));
|
));
|
||||||
multiline_textedit(ui, self.state.str_mut("lud16"), self.clipboard);
|
ui.add(multiline_textedit(self.state.str_mut("lud16")));
|
||||||
});
|
});
|
||||||
|
|
||||||
in_frame(ui, |ui| {
|
in_frame(ui, |ui| {
|
||||||
@@ -165,8 +159,7 @@ impl<'a> EditProfileView<'a> {
|
|||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
));
|
));
|
||||||
|
ui.add(singleline_textedit(self.state.str_mut("nip05")));
|
||||||
singleline_textedit(ui, self.state.str_mut("nip05"), self.clipboard);
|
|
||||||
|
|
||||||
let Some(nip05) = self.state.nip05() else {
|
let Some(nip05) = self.state.nip05() else {
|
||||||
return;
|
return;
|
||||||
@@ -215,29 +208,21 @@ fn label(text: &str) -> impl egui::Widget + '_ {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn singleline_textedit(ui: &mut egui::Ui, data: &mut String, clipboard: &mut Clipboard) {
|
fn singleline_textedit(data: &mut String) -> impl egui::Widget + '_ {
|
||||||
let r = ui.add(
|
TextEdit::singleline(data)
|
||||||
TextEdit::singleline(data)
|
.min_size(vec2(0.0, 40.0))
|
||||||
.min_size(vec2(0.0, 40.0))
|
.vertical_align(egui::Align::Center)
|
||||||
.vertical_align(egui::Align::Center)
|
.margin(Margin::symmetric(12, 10))
|
||||||
.margin(Margin::symmetric(12, 10))
|
.desired_width(f32::INFINITY)
|
||||||
.desired_width(f32::INFINITY),
|
|
||||||
);
|
|
||||||
|
|
||||||
input_context(ui, &r, clipboard, data, PasteBehavior::Clear);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn multiline_textedit(ui: &mut egui::Ui, data: &mut String, clipboard: &mut Clipboard) {
|
fn multiline_textedit(data: &mut String) -> impl egui::Widget + '_ {
|
||||||
let r = ui.add(
|
TextEdit::multiline(data)
|
||||||
TextEdit::multiline(data)
|
// .min_size(vec2(0.0, 40.0))
|
||||||
// .min_size(vec2(0.0, 40.0))
|
.vertical_align(egui::Align::TOP)
|
||||||
.vertical_align(egui::Align::TOP)
|
.margin(Margin::symmetric(12, 10))
|
||||||
.margin(Margin::symmetric(12, 10))
|
.desired_width(f32::INFINITY)
|
||||||
.desired_width(f32::INFINITY)
|
.desired_rows(1)
|
||||||
.desired_rows(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
input_context(ui, &r, clipboard, data, PasteBehavior::Clear);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn in_frame(ui: &mut egui::Ui, contents: impl FnOnce(&mut egui::Ui)) {
|
fn in_frame(ui: &mut egui::Ui, contents: impl FnOnce(&mut egui::Ui)) {
|
||||||
|
|||||||
@@ -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| 's: {
|
let output = scroll_area.show(ui, |ui| {
|
||||||
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,13 +85,15 @@ 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
|
||||||
.get_mut(&TimelineKind::Profile(*self.pubkey))
|
.notes(
|
||||||
else {
|
self.note_context.ndb,
|
||||||
break 's action;
|
self.note_context.note_cache,
|
||||||
};
|
&txn,
|
||||||
|
&TimelineKind::Profile(*self.pubkey),
|
||||||
|
)
|
||||||
|
.get_ptr();
|
||||||
|
|
||||||
profile_timeline.selected_view = tabs_ui(
|
profile_timeline.selected_view = tabs_ui(
|
||||||
ui,
|
ui,
|
||||||
@@ -114,6 +116,7 @@ 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,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user