diff --git a/.envrc b/.envrc index ec7378d..df6cf65 100644 --- a/.envrc +++ b/.envrc @@ -12,3 +12,4 @@ export JB55=32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245 export JACK=npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m export VROD=npub1h50pnxqw9jg7dhr906fvy4mze2yzawf895jhnc3p7qmljdugm6gsrurqev +export JEFFG=npub1zuuajd7u3sx8xu92yav9jwxpr839cs0kc3q6t56vd5u9q033xmhsk6c2uc diff --git a/Cargo.lock b/Cargo.lock index b8c3ec2..e7dbcd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ab_glyph" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c3a1cbc201cc13ed06cf875efb781f2249b3677f5c74571b67d817877f9d697" +checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] @@ -33,6 +33,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -162,9 +168,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -177,33 +183,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -211,9 +217,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" [[package]] name = "arbitrary" @@ -244,20 +250,20 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "as-raw-xcb-connection" @@ -293,7 +299,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -333,17 +339,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.0", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -400,7 +406,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -408,9 +414,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", - "syn 2.0.68", + "syn 2.0.77", "which", ] @@ -501,9 +507,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitstream-io" -version = "2.4.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415f8399438eb5e4b2f73ed3152a3448b98149dda642a957ee704e1daa5cf1d8" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" [[package]] name = "block" @@ -559,9 +565,9 @@ dependencies = [ [[package]] name = "built" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a6c0b39c38fd754ac338b00a88066436389c0f029da5d37d1e01091d9b7c17" +checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" [[package]] name = "bumpalo" @@ -571,22 +577,22 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b" +checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -603,9 +609,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "calloop" @@ -621,13 +627,39 @@ dependencies = [ "thiserror", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + [[package]] name = "calloop-wayland-source" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" dependencies = [ - "calloop", + "calloop 0.12.4", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", "rustix", "wayland-backend", "wayland-client", @@ -644,13 +676,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.104" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -733,14 +765,14 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.4", + "libloading 0.8.5", ] [[package]] name = "clipboard-win" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -763,9 +795,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "com" @@ -839,9 +871,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" @@ -869,9 +901,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -971,14 +1003,14 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.4", + "libloading 0.8.5", ] [[package]] name = "document-features" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" dependencies = [ "litrs", ] @@ -1116,7 +1148,7 @@ dependencies = [ [[package]] name = "egui_nav" version = "0.1.0" -source = "git+https://github.com/damus-io/egui-nav?branch=egui-0.28#a8dd95d2ae2a9a5c5251d47407320ad0eb074953" +source = "git+https://github.com/damus-io/egui-nav?rev=b19742503329a13df660ac8c5a3ada4a25b7cc53#b19742503329a13df660ac8c5a3ada4a25b7cc53" dependencies = [ "egui", "egui_extras", @@ -1185,7 +1217,7 @@ dependencies = [ name = "enostr" version = "0.1.0" dependencies = [ - "env_logger 0.11.3", + "env_logger 0.11.5", "ewebsock", "hex", "nostr", @@ -1214,14 +1246,14 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] name = "env_filter" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", "regex", @@ -1242,9 +1274,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", @@ -1287,9 +1319,9 @@ dependencies = [ [[package]] name = "error-code" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "ewebsock" @@ -1320,7 +1352,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -1347,12 +1379,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -1394,7 +1426,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -1468,7 +1500,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -1546,9 +1578,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "gl_generator" @@ -1669,7 +1701,7 @@ dependencies = [ "bitflags 2.6.0", "com", "libc", - "libloading 0.8.4", + "libloading 0.8.5", "thiserror", "widestring", "winapi", @@ -1759,9 +1791,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -1794,9 +1826,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -1813,28 +1845,28 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", "hyper", "hyper-util", - "rustls 0.23.10", - "rustls-native-certs", + "rustls 0.23.13", + "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", "tower-service", - "webpki-roots 0.26.3", + "webpki-roots 0.26.5", ] [[package]] name = "hyper-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", @@ -1873,12 +1905,12 @@ dependencies = [ [[package]] name = "image" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "color_quant", "exr", "gif", @@ -1896,12 +1928,12 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" dependencies = [ "byteorder-lite", - "thiserror", + "quick-error", ] [[package]] @@ -1918,9 +1950,9 @@ checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" [[package]] name = "indexmap" -version = "2.2.6" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -1957,31 +1989,40 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] [[package]] name = "itertools" @@ -2022,9 +2063,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] @@ -2037,9 +2078,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -2051,7 +2092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.4", + "libloading 0.8.5", "pkg-config", ] @@ -2090,9 +2131,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libfuzzer-sys" @@ -2117,12 +2158,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2195,7 +2236,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", - "rayon", ] [[package]] @@ -2269,15 +2309,25 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2294,7 +2344,7 @@ dependencies = [ "indexmap", "log", "num-traits", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "termcolor", "thiserror", @@ -2513,7 +2563,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -2545,16 +2595,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - [[package]] name = "num_enum" version = "0.5.11" @@ -2588,7 +2628,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 1.0.109", @@ -2600,10 +2640,10 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -2612,10 +2652,10 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -2744,9 +2784,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -2786,9 +2826,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owned_ttf_parser" -version = "0.21.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b41438d2fc63c46c74a2203bf5ccd82c41ba04347b2fcf5754f230b167067d5" +checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" dependencies = [ "ttf-parser", ] @@ -2811,9 +2851,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.2", + "redox_syscall 0.5.3", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2872,7 +2912,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -2903,7 +2943,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", ] [[package]] @@ -2919,9 +2959,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.2" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", @@ -2929,7 +2969,7 @@ dependencies = [ "pin-project-lite", "rustix", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2957,9 +2997,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "presser" @@ -2969,12 +3012,12 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "prettyplease" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -2987,6 +3030,15 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.20", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -3012,19 +3064,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] name = "puffin" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f76ad4bb049fded4e572df72cbb6381ff5d1f41f85c3a04b56e4eca287a02f" +checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f" dependencies = [ "anyhow", "bincode", "byteorder", "cfg-if", + "itertools 0.10.5", "lz4_flex", "once_cell", "parking_lot", @@ -3065,25 +3118,26 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" dependencies = [ "memchr", ] [[package]] name = "quinn" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" dependencies = [ "bytes", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", - "rustls 0.23.10", + "rustc-hash 2.0.0", + "rustls 0.23.13", + "socket2", "thiserror", "tokio", "tracing", @@ -3091,15 +3145,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.3" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" dependencies = [ "bytes", "rand", "ring 0.17.8", - "rustc-hash", - "rustls 0.23.10", + "rustc-hash 2.0.0", + "rustls 0.23.13", "slab", "thiserror", "tinyvec", @@ -3108,22 +3162,22 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" dependencies = [ "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -3172,7 +3226,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.12.1", "libc", "libfuzzer-sys", "log", @@ -3195,16 +3249,15 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.7" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67376f469e7e7840d0040bbf4b9b3334005bb167f814621326e4c7ab8cd6e944" +checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" dependencies = [ "avif-serialize", "imgref", "loop9", "quick-error", "rav1e", - "rayon", "rgb", ] @@ -3266,18 +3319,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -3310,9 +3363,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64 0.22.1", "bytes", @@ -3332,8 +3385,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.10", - "rustls-native-certs", + "rustls 0.23.13", + "rustls-native-certs 0.7.3", "rustls-pemfile", "rustls-pki-types", "serde", @@ -3348,8 +3401,8 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.26.3", - "winreg", + "webpki-roots 0.26.5", + "windows-registry", ] [[package]] @@ -3368,9 +3421,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.40" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -3423,20 +3476,26 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" dependencies = [ "bitflags 2.6.0", "errno", @@ -3459,11 +3518,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.4" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ "log", + "once_cell", "ring 0.17.8", "rustls-pki-types", "rustls-webpki", @@ -3472,24 +3532,23 @@ dependencies = [ ] [[package]] -name = "rustls" -version = "0.23.10" +name = "rustls-native-certs" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "once_cell", - "ring 0.17.8", + "openssl-probe", + "rustls-pemfile", "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "schannel", + "security-framework", ] [[package]] name = "rustls-native-certs" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" dependencies = [ "openssl-probe", "rustls-pemfile", @@ -3500,9 +3559,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -3510,15 +3569,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -3557,11 +3616,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3607,7 +3666,7 @@ dependencies = [ "ab_glyph", "log", "memmap2", - "smithay-client-toolkit", + "smithay-client-toolkit 0.18.1", "tiny-skia", ] @@ -3634,9 +3693,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -3647,9 +3706,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -3663,41 +3722,42 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "indexmap", "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -3812,8 +3872,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" dependencies = [ "bitflags 2.6.0", - "calloop", - "calloop-wayland-source", + "calloop 0.12.4", + "calloop-wayland-source 0.2.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.31.2", + "wayland-protocols-wlr 0.2.0", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", "cursor-icon", "libc", "log", @@ -3824,20 +3909,20 @@ dependencies = [ "wayland-client", "wayland-csd-frame", "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", + "wayland-protocols 0.32.4", + "wayland-protocols-wlr 0.3.4", "wayland-scanner", "xkeysym", ] [[package]] name = "smithay-clipboard" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" dependencies = [ "libc", - "smithay-client-toolkit", + "smithay-client-toolkit 0.19.2", "wayland-backend", ] @@ -3915,7 +4000,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -3947,9 +4032,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -3961,6 +4046,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "system-deps" @@ -3977,9 +4065,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "termcolor" @@ -3992,22 +4080,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -4090,9 +4178,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -4105,30 +4193,29 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -4148,16 +4235,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.10", + "rustls 0.23.13", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-socks" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", @@ -4183,21 +4270,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit 0.22.20", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -4215,15 +4302,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow 0.6.18", ] [[package]] @@ -4243,15 +4330,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -4272,7 +4359,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -4329,9 +4416,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttf-parser" -version = "0.21.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" +checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" [[package]] name = "tungstenite" @@ -4360,7 +4447,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" dependencies = [ - "rustc-hash", + "rustc-hash 1.1.0", ] [[package]] @@ -4413,9 +4500,9 @@ checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" [[package]] name = "universal-hash" @@ -4441,19 +4528,18 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.7" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" +checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" dependencies = [ "base64 0.22.1", "flate2", "log", "once_cell", - "rustls 0.22.4", + "rustls 0.23.13", "rustls-pki-types", - "rustls-webpki", "url", - "webpki-roots 0.26.3", + "webpki-roots 0.26.5", ] [[package]] @@ -4564,9 +4650,9 @@ checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -4595,34 +4681,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -4632,9 +4719,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4642,28 +4729,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wayland-backend" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", @@ -4675,9 +4762,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d" dependencies = [ "bitflags 2.6.0", "rustix", @@ -4698,9 +4785,9 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.3" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a206e8b2b53b1d3fcb9428fec72bc278ce539e2fa81fe2bfc1ab27703d5187b9" +checksum = "3a94697e66e76c85923b0d28a0c251e8f0666f58fc47d316c0f4da6da75d37cb" dependencies = [ "rustix", "wayland-client", @@ -4719,6 +4806,18 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5755d77ae9040bb872a25026555ce4cb0ae75fd923e90d25fba07d81057de0" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + [[package]] name = "wayland-protocols-plasma" version = "0.2.0" @@ -4728,7 +4827,7 @@ dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", "wayland-scanner", ] @@ -4741,15 +4840,28 @@ dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad87b5fd1b1d3ca2f792df8f686a2a11e3fe1077b71096f7a175ab699f89109" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.4", "wayland-scanner", ] [[package]] name = "wayland-scanner" -version = "0.31.2" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", "quick-xml", @@ -4758,9 +4870,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.2" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", @@ -4770,9 +4882,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -4837,9 +4949,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" dependencies = [ "rustls-pki-types", ] @@ -4895,7 +5007,7 @@ dependencies = [ "parking_lot", "profiling", "raw-window-handle 0.6.2", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "web-sys", @@ -4925,7 +5037,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.4", + "libloading 0.8.5", "log", "metal", "naga", @@ -4936,7 +5048,7 @@ dependencies = [ "profiling", "raw-window-handle 0.6.2", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "wasm-bindgen", @@ -4992,11 +5104,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5012,7 +5124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -5021,7 +5133,37 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -5048,7 +5190,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -5083,18 +5234,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -5111,9 +5262,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -5129,9 +5280,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -5147,15 +5298,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -5171,9 +5322,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -5189,9 +5340,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -5207,9 +5358,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -5225,9 +5376,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" @@ -5240,7 +5391,7 @@ dependencies = [ "atomic-waker", "bitflags 2.6.0", "bytemuck", - "calloop", + "calloop 0.12.4", "cfg_aliases", "core-foundation", "core-graphics", @@ -5260,14 +5411,14 @@ dependencies = [ "redox_syscall 0.3.5", "rustix", "sctk-adwaita", - "smithay-client-toolkit", + "smithay-client-toolkit 0.18.1", "smol_str", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", "wayland-protocols-plasma", "web-sys", "web-time 0.2.4", @@ -5288,23 +5439,13 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "x11-dl" version = "2.21.0" @@ -5325,7 +5466,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading 0.8.4", + "libloading 0.8.5", "once_cell", "rustix", "x11rb-protocol", @@ -5339,9 +5480,9 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] name = "xkbcommon-dl" @@ -5364,9 +5505,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.20" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" [[package]] name = "xmlwriter" @@ -5380,6 +5521,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -5391,7 +5533,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.77", ] [[package]] @@ -5417,9 +5559,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 14af4b7..d861e5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] } ehttp = "0.2.0" egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" } -egui_nav = { git = "https://github.com/damus-io/egui-nav", branch = "egui-0.28" } +egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "b19742503329a13df660ac8c5a3ada4a25b7cc53" } egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" } reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] } image = { version = "0.25", features = ["jpeg", "png", "webp"] } diff --git a/enostr/src/keypair.rs b/enostr/src/keypair.rs index 187a57a..1181877 100644 --- a/enostr/src/keypair.rs +++ b/enostr/src/keypair.rs @@ -32,11 +32,11 @@ impl Keypair { } } - pub fn to_full(self) -> Option { - if let Some(secret_key) = self.secret_key { - Some(FullKeypair { - pubkey: self.pubkey, - secret_key, + pub fn to_full<'a>(&'a self) -> Option> { + if let Some(secret_key) = &self.secret_key { + Some(FilledKeypair { + pubkey: &self.pubkey, + secret_key: secret_key, }) } else { None @@ -44,17 +44,40 @@ impl Keypair { } } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct FullKeypair { pub pubkey: Pubkey, pub secret_key: SecretKey, } +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub struct FilledKeypair<'a> { + pub pubkey: &'a Pubkey, + pub secret_key: &'a SecretKey, +} + +impl<'a> FilledKeypair<'a> { + pub fn new(pubkey: &'a Pubkey, secret_key: &'a SecretKey) -> Self { + FilledKeypair { pubkey, secret_key } + } + + pub fn to_full(&self) -> FullKeypair { + FullKeypair { + pubkey: self.pubkey.to_owned(), + secret_key: self.secret_key.to_owned(), + } + } +} + impl FullKeypair { pub fn new(pubkey: Pubkey, secret_key: SecretKey) -> Self { FullKeypair { pubkey, secret_key } } + pub fn to_filled<'a>(&'a self) -> FilledKeypair<'a> { + FilledKeypair::new(&self.pubkey, &self.secret_key) + } + pub fn generate() -> Self { let mut rng = nostr::secp256k1::rand::rngs::OsRng; let (secret_key, _) = &nostr::SECP256K1.generate_keypair(&mut rng); diff --git a/enostr/src/lib.rs b/enostr/src/lib.rs index d35885b..e33e563 100644 --- a/enostr/src/lib.rs +++ b/enostr/src/lib.rs @@ -11,7 +11,7 @@ pub use client::ClientMessage; pub use error::Error; pub use ewebsock; pub use filter::Filter; -pub use keypair::{FullKeypair, Keypair, SerializableKeypair}; +pub use keypair::{FilledKeypair, FullKeypair, Keypair, SerializableKeypair}; pub use nostr::SecretKey; pub use note::{Note, NoteId}; pub use profile::Profile; diff --git a/src/account_manager.rs b/src/account_manager.rs index a083912..17d8706 100644 --- a/src/account_manager.rs +++ b/src/account_manager.rs @@ -1,10 +1,22 @@ use std::cmp::Ordering; -use enostr::Keypair; +use enostr::{FilledKeypair, FullKeypair, Keypair}; +use nostrdb::Ndb; + +use crate::{ + column::Columns, + imgcache::ImageCache, + key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType}, + login_manager::LoginState, + route::{Route, Router}, + ui::{ + account_login_view::{AccountLoginResponse, AccountLoginView}, + account_management::{AccountsView, AccountsViewResponse}, + }, +}; +use tracing::info; -use crate::key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType}; pub use crate::user_account::UserAccount; -use tracing::info; /// The interface for managing the user's accounts. /// Represents all user-facing operations related to account management. @@ -14,6 +26,75 @@ pub struct AccountManager { key_store: KeyStorageType, } +// TODO(jb55): move to accounts/route.rs +pub enum AccountsRouteResponse { + Accounts(AccountsViewResponse), + AddAccount(AccountLoginResponse), +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum AccountsRoute { + Accounts, + AddAccount, +} + +/// Render account management views from a route +#[allow(clippy::too_many_arguments)] +pub fn render_accounts_route( + ui: &mut egui::Ui, + ndb: &Ndb, + col: usize, + columns: &mut Columns, + img_cache: &mut ImageCache, + accounts: &mut AccountManager, + login_state: &mut LoginState, + route: AccountsRoute, +) { + let router = columns.column_mut(col).router_mut(); + let resp = match route { + AccountsRoute::Accounts => AccountsView::new(ndb, accounts, img_cache) + .ui(ui) + .inner + .map(AccountsRouteResponse::Accounts), + + AccountsRoute::AddAccount => AccountLoginView::new(login_state) + .ui(ui) + .inner + .map(AccountsRouteResponse::AddAccount), + }; + + if let Some(resp) = resp { + match resp { + AccountsRouteResponse::Accounts(response) => { + process_accounts_view_response(accounts, response, router); + } + AccountsRouteResponse::AddAccount(response) => { + process_login_view_response(accounts, response); + *login_state = Default::default(); + router.go_back(); + } + } + } +} + +pub fn process_accounts_view_response( + manager: &mut AccountManager, + response: AccountsViewResponse, + router: &mut Router, +) { + match response { + AccountsViewResponse::RemoveAccount(index) => { + manager.remove_account(index); + } + AccountsViewResponse::SelectAccount(index) => { + manager.select_account(index); + } + AccountsViewResponse::RouteToLogin => { + router.route_to(Route::add_account()); + } + } +} + impl AccountManager { pub fn new(currently_selected_account: Option, key_store: KeyStorageType) -> Self { let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { @@ -88,6 +169,12 @@ impl AccountManager { self.currently_selected_account } + pub fn selected_or_first_nsec(&self) -> Option> { + self.get_selected_account() + .and_then(|kp| kp.to_full()) + .or_else(|| self.accounts.iter().find_map(|a| a.to_full())) + } + pub fn get_selected_account(&self) -> Option<&UserAccount> { if let Some(account_index) = self.currently_selected_account { if let Some(account) = self.get_account(account_index) { @@ -110,3 +197,14 @@ impl AccountManager { self.currently_selected_account = None } } + +pub fn process_login_view_response(manager: &mut AccountManager, response: AccountLoginResponse) { + match response { + AccountLoginResponse::CreateNew => { + manager.add_account(FullKeypair::generate().to_keypair()); + } + AccountLoginResponse::LoginWith(keypair) => { + manager.add_account(keypair); + } + } +} diff --git a/src/actionbar.rs b/src/actionbar.rs index e3f1b06..128909b 100644 --- a/src/actionbar.rs +++ b/src/actionbar.rs @@ -1,18 +1,18 @@ use crate::{ note::NoteRef, - route::Route, - thread::{Thread, ThreadResult}, - Damus, + notecache::NoteCache, + route::{Route, Router}, + thread::{Thread, ThreadResult, Threads}, }; -use enostr::NoteId; -use nostrdb::Transaction; +use enostr::{NoteId, RelayPool}; +use nostrdb::{Ndb, Transaction}; use tracing::{error, info}; use uuid::Uuid; #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum BarAction { - Reply, - OpenThread, + Reply(NoteId), + OpenThread(NoteId), } pub struct NewThreadNotes { @@ -30,26 +30,26 @@ pub enum BarResult { /// the thread view. We don't have a concept of model/view/controller etc /// in egui, but this is the closest thing to that. fn open_thread( - app: &mut Damus, + ndb: &Ndb, txn: &Transaction, - timeline: usize, + router: &mut Router, + note_cache: &mut NoteCache, + pool: &mut RelayPool, + threads: &mut Threads, selected_note: &[u8; 32], ) -> Option { { - let timeline = &mut app.timelines[timeline]; - timeline - .routes - .push(Route::Thread(NoteId::new(selected_note.to_owned()))); - timeline.navigating = true; + router.route_to(Route::thread(NoteId::new(selected_note.to_owned()))); + router.navigating = true; } - let root_id = crate::note::root_note_id_from_selected_id(app, txn, selected_note); - let thread_res = app.threads.thread_mut(&app.ndb, txn, root_id); + let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note); + let thread_res = threads.thread_mut(ndb, txn, root_id); let (thread, result) = match thread_res { ThreadResult::Stale(thread) => { // The thread is stale, let's update it - let notes = Thread::new_notes(&thread.view.notes, root_id, txn, &app.ndb); + let notes = Thread::new_notes(&thread.view().notes, root_id, txn, ndb); let bar_result = if notes.is_empty() { None } else { @@ -76,14 +76,14 @@ fn open_thread( // an active subscription for this thread. if thread.subscription().is_none() { let filters = Thread::filters(root_id); - *thread.subscription_mut() = app.ndb.subscribe(&filters).ok(); + *thread.subscription_mut() = ndb.subscribe(&filters).ok(); if thread.remote_subscription().is_some() { error!("Found active remote subscription when it was not expected"); } else { let subid = Uuid::new_v4().to_string(); *thread.remote_subscription_mut() = Some(subid.clone()); - app.pool.subscribe(subid, filters); + pool.subscribe(subid, filters); } match thread.subscription() { @@ -91,7 +91,7 @@ fn open_thread( thread.subscribers += 1; info!( "Locally/remotely subscribing to thread. {} total active subscriptions, {} on this thread", - app.ndb.subscription_count(), + ndb.subscription_count(), thread.subscribers, ); } @@ -104,7 +104,7 @@ fn open_thread( thread.subscribers += 1; info!( "Re-using existing thread subscription. {} total active subscriptions, {} on this thread", - app.ndb.subscription_count(), + ndb.subscription_count(), thread.subscribers, ) } @@ -113,24 +113,41 @@ fn open_thread( } impl BarAction { + #[allow(clippy::too_many_arguments)] pub fn execute( self, - app: &mut Damus, - timeline: usize, - replying_to: &[u8; 32], + ndb: &Ndb, + router: &mut Router, + threads: &mut Threads, + note_cache: &mut NoteCache, + pool: &mut RelayPool, txn: &Transaction, ) -> Option { match self { - BarAction::Reply => { - let timeline = &mut app.timelines[timeline]; - timeline - .routes - .push(Route::Reply(NoteId::new(replying_to.to_owned()))); - timeline.navigating = true; + BarAction::Reply(note_id) => { + router.route_to(Route::reply(note_id)); + router.navigating = true; None } - BarAction::OpenThread => open_thread(app, txn, timeline, replying_to), + BarAction::OpenThread(note_id) => { + open_thread(ndb, txn, router, note_cache, pool, threads, note_id.bytes()) + } + } + } + + /// Execute the BarAction and process the BarResult + pub fn execute_and_process_result( + self, + ndb: &Ndb, + router: &mut Router, + threads: &mut Threads, + note_cache: &mut NoteCache, + pool: &mut RelayPool, + txn: &Transaction, + ) { + if let Some(br) = self.execute(ndb, router, threads, note_cache, pool, txn) { + br.process(ndb, txn, threads); } } } @@ -139,6 +156,18 @@ impl BarResult { pub fn new_thread_notes(notes: Vec, root_id: NoteId) -> Self { BarResult::NewThreadNotes(NewThreadNotes::new(notes, root_id)) } + + pub fn process(&self, ndb: &Ndb, txn: &Transaction, threads: &mut Threads) { + match self { + // update the thread for next render if we have new notes + BarResult::NewThreadNotes(new_notes) => { + let thread = threads + .thread_mut(ndb, txn, new_notes.root_id.bytes()) + .get_ptr(); + new_notes.process(thread); + } + } + } } impl NewThreadNotes { @@ -151,6 +180,6 @@ impl NewThreadNotes { pub fn process(&self, thread: &mut Thread) { // threads are chronological, ie reversed from reverse-chronological, the default. let reversed = true; - thread.view.insert(&self.notes, reversed); + thread.view_mut().insert(&self.notes, reversed); } } diff --git a/src/app.rs b/src/app.rs index 05e7ba9..9fe6a2e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,31 +1,29 @@ -use crate::account_manager::AccountManager; -use crate::actionbar::BarResult; -use crate::app_creation::setup_cc; -use crate::app_style::user_requested_visuals_change; -use crate::args::Args; -use crate::column::ColumnKind; -use crate::draft::Drafts; -use crate::error::{Error, FilterError}; -use crate::filter::FilterState; -use crate::frame_history::FrameHistory; -use crate::imgcache::ImageCache; -use crate::key_storage::KeyStorageType; -use crate::note::NoteRef; -use crate::notecache::{CachedNote, NoteCache}; -use crate::relay_pool_manager::RelayPoolManager; -use crate::route::Route; -use crate::subscriptions::{SubKind, Subscriptions}; -use crate::thread::{DecrementResult, Threads}; -use crate::timeline::{Timeline, TimelineSource, ViewFilter}; -use crate::ui::note::PostAction; -use crate::ui::{self, AccountSelectionWidget, DesktopGlobalPopup}; -use crate::ui::{DesktopSidePanel, RelayView, View}; -use crate::unknowns::UnknownIds; -use crate::{filter, Result}; -use egui_nav::{Nav, NavAction}; +use crate::{ + account_manager::AccountManager, + app_creation::setup_cc, + app_style::user_requested_visuals_change, + args::Args, + column::Columns, + draft::Drafts, + error::{Error, FilterError}, + filter, + filter::FilterState, + frame_history::FrameHistory, + imgcache::ImageCache, + key_storage::KeyStorageType, + nav, + note::NoteRef, + notecache::{CachedNote, NoteCache}, + subscriptions::{SubKind, Subscriptions}, + thread::Threads, + timeline::{Timeline, TimelineKind, ViewFilter}, + ui::{self, AccountSelectionWidget, DesktopSidePanel}, + unknowns::UnknownIds, + view_state::ViewState, + Result, +}; + use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool}; -use std::cell::RefCell; -use std::rc::Rc; use uuid::Uuid; use egui::{Context, Frame, Style}; @@ -47,21 +45,17 @@ pub enum DamusState { /// We derive Deserialize/Serialize so we can persist app state on shutdown. pub struct Damus { state: DamusState, - note_cache: NoteCache, + pub note_cache: NoteCache, pub pool: RelayPool, - /// global navigation for account management popups, etc. - pub global_nav: Vec, - - pub timelines: Vec, - pub selected_timeline: i32, - + pub columns: Columns, pub ndb: Ndb, + pub view_state: ViewState, pub unknown_ids: UnknownIds, pub drafts: Drafts, pub threads: Threads, pub img_cache: ImageCache, - pub account_manager: AccountManager, + pub accounts: AccountManager, pub subscriptions: Subscriptions, frame_history: crate::frame_history::FrameHistory, @@ -71,7 +65,6 @@ pub struct Damus { pub since_optimize: bool, pub textmode: bool, pub show_account_switcher: bool, - pub show_global_popup: bool, } fn relay_setup(pool: &mut RelayPool, ctx: &egui::Context) { @@ -99,10 +92,15 @@ fn relay_setup(pool: &mut RelayPool, ctx: &egui::Context) { } } -fn send_initial_timeline_filter(damus: &mut Damus, timeline: usize, to: &str) { - let can_since_optimize = damus.since_optimize; - - let filter_state = damus.timelines[timeline].filter.clone(); +fn send_initial_timeline_filter( + ndb: &Ndb, + can_since_optimize: bool, + subs: &mut Subscriptions, + pool: &mut RelayPool, + timeline: &mut Timeline, + to: &str, +) { + let filter_state = timeline.filter.clone(); match filter_state { FilterState::Broken(err) => { @@ -131,7 +129,7 @@ fn send_initial_timeline_filter(damus: &mut Damus, timeline: usize, to: &str) { filter = filter.limit_mut(lim); } - let notes = damus.timelines[timeline].notes(ViewFilter::NotesAndReplies); + let notes = timeline.notes(ViewFilter::NotesAndReplies); // Should we since optimize? Not always. For example // if we only have a few notes locally. One way to @@ -148,38 +146,41 @@ fn send_initial_timeline_filter(damus: &mut Damus, timeline: usize, to: &str) { filter }).collect(); - let sub_id = damus.gen_subid(&SubKind::Initial); - damus - .subscriptions() - .insert(sub_id.clone(), SubKind::Initial); + //let sub_id = damus.gen_subid(&SubKind::Initial); + let sub_id = Uuid::new_v4().to_string(); + subs.subs.insert(sub_id.clone(), SubKind::Initial); let cmd = ClientMessage::req(sub_id, new_filters); - damus.pool.send_to(&cmd, to); + pool.send_to(&cmd, to); } // we need some data first FilterState::NeedsRemote(filter) => { - let uid = damus.timelines[timeline].uid; - let sub_kind = SubKind::FetchingContactList(uid); - let sub_id = damus.gen_subid(&sub_kind); - let local_sub = damus.ndb.subscribe(&filter).expect("sub"); + let sub_kind = SubKind::FetchingContactList(timeline.id); + //let sub_id = damus.gen_subid(&sub_kind); + let sub_id = Uuid::new_v4().to_string(); + let local_sub = ndb.subscribe(&filter).expect("sub"); - damus.timelines[timeline].filter = - FilterState::fetching_remote(sub_id.clone(), local_sub); + timeline.filter = FilterState::fetching_remote(sub_id.clone(), local_sub); - damus.subscriptions().insert(sub_id.clone(), sub_kind); + subs.subs.insert(sub_id.clone(), sub_kind); - damus.pool.subscribe(sub_id, filter.to_owned()); + pool.subscribe(sub_id, filter.to_owned()); } } } fn send_initial_filters(damus: &mut Damus, relay_url: &str) { info!("Sending initial filters to {}", relay_url); - let timelines = damus.timelines.len(); - - for i in 0..timelines { - send_initial_timeline_filter(damus, i, relay_url); + for timeline in damus.columns.timelines_mut() { + send_initial_timeline_filter( + &damus.ndb, + damus.since_optimize, + &mut damus.subscriptions, + &mut damus.pool, + timeline, + relay_url, + ); } } @@ -190,7 +191,7 @@ enum ContextAction { fn handle_key_events( input: &egui::InputState, pixels_per_point: f32, - damus: &mut Damus, + columns: &mut Columns, ) -> Option { let amount = 0.2; @@ -213,16 +214,16 @@ fn handle_key_events( Some(ContextAction::SetPixelsPerPoint(pixels_per_point - amount)); } egui::Key::J => { - damus.select_down(); + columns.select_down(); } egui::Key::K => { - damus.select_up(); + columns.select_up(); } egui::Key::H => { - damus.select_left(); + columns.select_left(); } egui::Key::L => { - damus.select_left(); + columns.select_left(); } _ => {} } @@ -234,7 +235,7 @@ fn handle_key_events( fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { let ppp = ctx.pixels_per_point(); - let res = ctx.input(|i| handle_key_events(i, ppp, damus)); + let res = ctx.input(|i| handle_key_events(i, ppp, &mut damus.columns)); if let Some(action) = res { match action { ContextAction::SetPixelsPerPoint(amt) => { @@ -263,12 +264,27 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { } } - for timeline in 0..damus.timelines.len() { - let src = TimelineSource::column(timeline); + let n_timelines = damus.columns.timelines().len(); + for timeline_ind in 0..n_timelines { + let is_ready = { + let timeline = &mut damus.columns.timelines[timeline_ind]; + matches!( + is_timeline_ready(&damus.ndb, &mut damus.pool, &mut damus.note_cache, timeline), + Ok(true) + ) + }; - if let Ok(true) = is_timeline_ready(damus, timeline) { + if is_ready { let txn = Transaction::new(&damus.ndb).expect("txn"); - if let Err(err) = src.poll_notes_into_view(&txn, damus) { + + if let Err(err) = Timeline::poll_notes_into_view( + timeline_ind, + &mut damus.columns.timelines, + &damus.ndb, + &txn, + &mut damus.unknown_ids, + &mut damus.note_cache, + ) { error!("poll_notes_into_view: {err}"); } } else { @@ -298,8 +314,13 @@ fn unknown_id_send(damus: &mut Damus) { /// Our timelines may require additional data before it is functional. For /// example, when we have to fetch a contact list before we do the actual /// following list query. -fn is_timeline_ready(damus: &mut Damus, timeline: usize) -> Result { - let sub = match &damus.timelines[timeline].filter { +fn is_timeline_ready( + ndb: &Ndb, + pool: &mut RelayPool, + note_cache: &mut NoteCache, + timeline: &mut Timeline, +) -> Result { + let sub = match &timeline.filter { FilterState::GotRemote(sub) => *sub, FilterState::Ready(_f) => return Ok(true), _ => return Ok(false), @@ -307,9 +328,12 @@ fn is_timeline_ready(damus: &mut Damus, timeline: usize) -> Result { // We got at least one eose for our filter request. Let's see // if nostrdb is done processing it yet. - let res = damus.ndb.poll_for_notes(sub, 1); + let res = ndb.poll_for_notes(sub, 1); if res.is_empty() { - debug!("check_timeline_filter_state: no notes found (yet?) for timeline {timeline}"); + debug!( + "check_timeline_filter_state: no notes found (yet?) for timeline {:?}", + timeline + ); return Ok(false); } @@ -318,8 +342,8 @@ fn is_timeline_ready(damus: &mut Damus, timeline: usize) -> Result { let note_key = res[0]; let filter = { - let txn = Transaction::new(&damus.ndb).expect("txn"); - let note = damus.ndb.get_note_by_key(&txn, note_key).expect("note"); + let txn = Transaction::new(ndb).expect("txn"); + let note = ndb.get_note_by_key(&txn, note_key).expect("note"); filter::filter_from_tags(¬e).map(|f| f.into_follow_filter()) }; @@ -327,23 +351,24 @@ fn is_timeline_ready(damus: &mut Damus, timeline: usize) -> Result { match filter { Err(Error::Filter(e)) => { error!("got broken when building filter {e}"); - damus.timelines[timeline].filter = FilterState::broken(e); + timeline.filter = FilterState::broken(e); } Err(err) => { error!("got broken when building filter {err}"); - damus.timelines[timeline].filter = FilterState::broken(FilterError::EmptyContactList); + timeline.filter = FilterState::broken(FilterError::EmptyContactList); return Err(err); } Ok(filter) => { // we just switched to the ready state, we should send initial // queries and setup the local subscription info!("Found contact list! Setting up local and remote contact list query"); - setup_initial_timeline(damus, timeline, &filter).expect("setup init"); - damus.timelines[timeline].filter = FilterState::ready(filter.clone()); + setup_initial_timeline(ndb, timeline, note_cache, &filter).expect("setup init"); + timeline.filter = FilterState::ready(filter.clone()); - let ck = &damus.timelines[timeline].kind; - let subid = damus.gen_subid(&SubKind::Column(ck.clone())); - damus.pool.subscribe(subid, filter) + //let ck = &timeline.kind; + //let subid = damus.gen_subid(&SubKind::Column(ck.clone())); + let subid = Uuid::new_v4().to_string(); + pool.subscribe(subid, filter) } } @@ -355,18 +380,23 @@ fn setup_profiling() { puffin::set_scopes_on(true); // tell puffin to collect data } -fn setup_initial_timeline(damus: &mut Damus, timeline: usize, filters: &[Filter]) -> Result<()> { - damus.timelines[timeline].subscription = Some(damus.ndb.subscribe(filters)?); - let txn = Transaction::new(&damus.ndb)?; +fn setup_initial_timeline( + ndb: &Ndb, + timeline: &mut Timeline, + note_cache: &mut NoteCache, + filters: &[Filter], +) -> Result<()> { + timeline.subscription = Some(ndb.subscribe(filters)?); + let txn = Transaction::new(ndb)?; debug!( "querying nostrdb sub {:?} {:?}", - damus.timelines[timeline].subscription, damus.timelines[timeline].filter + timeline.subscription, timeline.filter ); let lim = filters[0].limit().unwrap_or(crate::filter::default_limit()) as i32; - let results = damus.ndb.query(&txn, filters, lim)?; + let results = ndb.query(&txn, filters, lim)?; let filters = { - let views = &damus.timelines[timeline].views; + let views = &timeline.views; let filters: Vec bool> = views.iter().map(|v| v.filter.filter()).collect(); filters @@ -375,12 +405,10 @@ fn setup_initial_timeline(damus: &mut Damus, timeline: usize, filters: &[Filter] for result in results { for (view, filter) in filters.iter().enumerate() { if filter( - damus - .note_cache_mut() - .cached_note_or_insert_mut(result.note_key, &result.note), + note_cache.cached_note_or_insert_mut(result.note_key, &result.note), &result.note, ) { - damus.timelines[timeline].views[view].notes.push(NoteRef { + timeline.views[view].notes.push(NoteRef { key: result.note_key, created_at: result.note.created_at(), }) @@ -391,12 +419,16 @@ fn setup_initial_timeline(damus: &mut Damus, timeline: usize, filters: &[Filter] Ok(()) } -fn setup_initial_nostrdb_subs(damus: &mut Damus) -> Result<()> { - let timelines = damus.timelines.len(); - for i in 0..timelines { - let filter = damus.timelines[i].filter.clone(); - match filter { - FilterState::Ready(filters) => setup_initial_timeline(damus, i, &filters)?, +fn setup_initial_nostrdb_subs( + ndb: &Ndb, + note_cache: &mut NoteCache, + columns: &mut Columns, +) -> Result<()> { + for timeline in columns.timelines_mut() { + match &timeline.filter { + FilterState::Ready(filters) => { + { setup_initial_timeline(ndb, timeline, note_cache, &filters.clone()) }? + } FilterState::Broken(err) => { error!("FetchingRemote state broken in setup_initial_nostr_subs: {err}") @@ -427,7 +459,8 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) { damus .subscriptions() .insert("unknownids".to_string(), SubKind::OneShot); - setup_initial_nostrdb_subs(damus).expect("home subscription failed"); + setup_initial_nostrdb_subs(&damus.ndb, &mut damus.note_cache, &mut damus.columns) + .expect("home subscription failed"); } if let Err(err) = try_process_event(damus, ctx) { @@ -458,12 +491,18 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> { }; match *sub_kind { - SubKind::Column(_) => { - // eose on column? whatevs + SubKind::Timeline(_) => { + // eose on timeline? whatevs } SubKind::Initial => { let txn = Transaction::new(&damus.ndb)?; - UnknownIds::update(&txn, damus); + UnknownIds::update( + &txn, + &mut damus.unknown_ids, + &damus.columns, + &damus.ndb, + &mut damus.note_cache, + ); // this is possible if this is the first time if damus.unknown_ids.ready_to_send() { unknown_id_send(damus); @@ -477,8 +516,8 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> { } SubKind::FetchingContactList(timeline_uid) => { - let timeline_ind = if let Some(i) = damus.find_timeline(timeline_uid) { - i + let timeline = if let Some(tl) = damus.columns.find_timeline_mut(timeline_uid) { + tl } else { error!( "timeline uid:{} not found for FetchingContactList", @@ -487,35 +526,25 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> { return Ok(()); }; - let local_sub = if let FilterState::FetchingRemote(unisub) = - &damus.timelines[timeline_ind].filter - { + // If this request was fetching a contact list, our filter + // state should be "FetchingRemote". We look at the local + // subscription for that filter state and get the subscription id + let local_sub = if let FilterState::FetchingRemote(unisub) = &timeline.filter { unisub.local } else { // TODO: we could have multiple contact list results, we need // to check to see if this one is newer and use that instead warn!( "Expected timeline to have FetchingRemote state but was {:?}", - damus.timelines[timeline_ind].filter + timeline.filter ); return Ok(()); }; - damus.timelines[timeline_ind].filter = FilterState::got_remote(local_sub); - - /* - // see if we're fast enough to catch a processed contact list - let note_keys = damus.ndb.poll_for_notes(local_sub, 1); - if !note_keys.is_empty() { - debug!("fast! caught contact list from {relay_url} right away"); - let txn = Transaction::new(&damus.ndb)?; - let note_key = note_keys[0]; - let nr = damus.ndb.get_note_by_key(&txn, note_key)?; - let filter = filter::filter_from_tags(&nr)?.into_follow_filter(); - setup_initial_timeline(damus, timeline, &filter) - damus.timelines[timeline_ind].filter = FilterState::ready(filter); - } - */ + // We take the subscription id and pass it to the new state of + // "GotRemote". This will let future frames know that it can try + // to look for the contact list in nostrdb. + timeline.filter = FilterState::got_remote(local_sub); } } @@ -593,7 +622,7 @@ impl Damus { let mut config = Config::new(); config.set_ingester_threads(4); - let mut account_manager = AccountManager::new( + let mut accounts = AccountManager::new( // TODO: should pull this from settings None, // TODO: use correct KeyStorage mechanism for current OS arch @@ -602,12 +631,12 @@ impl Damus { for key in parsed_args.keys { info!("adding account: {}", key.pubkey); - account_manager.add_account(key); + accounts.add_account(key); } // TODO: pull currently selected account from settings - if account_manager.num_accounts() > 0 { - account_manager.select_account(0); + if accounts.num_accounts() > 0 { + accounts.select_account(0); } // setup relays if we have them @@ -629,27 +658,27 @@ impl Damus { pool }; - let account = account_manager + let account = accounts .get_selected_account() .as_ref() .map(|a| a.pubkey.bytes()); let ndb = Ndb::new(&dbpath, &config).expect("ndb"); - let mut timelines: Vec = Vec::with_capacity(parsed_args.columns.len()); + let mut columns: Columns = Columns::new(); for col in parsed_args.columns { if let Some(timeline) = col.into_timeline(&ndb, account) { - timelines.push(timeline); + columns.add_timeline(timeline); } } let debug = parsed_args.debug; - if timelines.is_empty() { + if columns.columns().is_empty() { let filter = Filter::from_json(include_str!("../queries/timeline.json")).unwrap(); - timelines.push(Timeline::new( - ColumnKind::Generic, + columns.add_timeline(Timeline::new( + TimelineKind::Generic, FilterState::ready(vec![filter]), - )); + )) } Self { @@ -663,18 +692,52 @@ impl Damus { state: DamusState::Initializing, img_cache: ImageCache::new(imgcache_dir.into()), note_cache: NoteCache::default(), - selected_timeline: 0, - timelines, + columns, textmode: parsed_args.textmode, ndb, - account_manager, + accounts, frame_history: FrameHistory::default(), show_account_switcher: false, - show_global_popup: false, - global_nav: Vec::new(), + view_state: ViewState::default(), } } + pub fn pool_mut(&mut self) -> &mut RelayPool { + &mut self.pool + } + + pub fn ndb(&self) -> &Ndb { + &self.ndb + } + + pub fn drafts_mut(&mut self) -> &mut Drafts { + &mut self.drafts + } + + pub fn img_cache_mut(&mut self) -> &mut ImageCache { + &mut self.img_cache + } + + pub fn accounts(&self) -> &AccountManager { + &self.accounts + } + + pub fn accounts_mut(&mut self) -> &mut AccountManager { + &mut self.accounts + } + + pub fn view_state_mut(&mut self) -> &mut ViewState { + &mut self.view_state + } + + pub fn columns_mut(&mut self) -> &mut Columns { + &mut self.columns + } + + pub fn columns(&self) -> &Columns { + &self.columns + } + pub fn gen_subid(&self, kind: &SubKind) -> String { if self.debug { format!("{:?}", kind) @@ -684,12 +747,12 @@ impl Damus { } pub fn mock>(data_path: P) -> Self { - let mut timelines: Vec = vec![]; + let mut columns = Columns::new(); let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap(); - timelines.push(Timeline::new( - ColumnKind::Universe, - FilterState::ready(vec![filter]), - )); + + let timeline = Timeline::new(TimelineKind::Universe, FilterState::ready(vec![filter])); + + columns.add_timeline(timeline); let imgcache_dir = data_path.as_ref().join(ImageCache::rel_datadir()); let _ = std::fs::create_dir_all(imgcache_dir.clone()); @@ -708,28 +771,16 @@ impl Damus { pool: RelayPool::new(), img_cache: ImageCache::new(imgcache_dir), note_cache: NoteCache::default(), - selected_timeline: 0, - timelines, + columns, textmode: false, ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"), - account_manager: AccountManager::new(None, KeyStorageType::None), + accounts: AccountManager::new(None, KeyStorageType::None), frame_history: FrameHistory::default(), show_account_switcher: false, - show_global_popup: true, - global_nav: Vec::new(), + view_state: ViewState::default(), } } - pub fn find_timeline(&self, uid: u32) -> Option { - for (i, timeline) in self.timelines.iter().enumerate() { - if timeline.uid == uid { - return Some(i); - } - } - - None - } - pub fn subscriptions(&mut self) -> &mut HashMap { &mut self.subscriptions.subs } @@ -738,34 +789,20 @@ impl Damus { &mut self.note_cache } - pub fn note_cache(&self) -> &NoteCache { - &self.note_cache + pub fn unknown_ids_mut(&mut self) -> &mut UnknownIds { + &mut self.unknown_ids } - pub fn selected_timeline(&mut self) -> &mut Timeline { - &mut self.timelines[self.selected_timeline as usize] + pub fn threads(&self) -> &Threads { + &self.threads } - pub fn select_down(&mut self) { - self.selected_timeline().current_view_mut().select_down(); + pub fn threads_mut(&mut self) -> &mut Threads { + &mut self.threads } - pub fn select_up(&mut self) { - self.selected_timeline().current_view_mut().select_up(); - } - - pub fn select_left(&mut self) { - if self.selected_timeline - 1 < 0 { - return; - } - self.selected_timeline -= 1; - } - - pub fn select_right(&mut self) { - if self.selected_timeline + 1 >= self.timelines.len() as i32 { - return; - } - self.selected_timeline += 1; + pub fn note_cache(&self) -> &NoteCache { + &self.note_cache } } @@ -797,7 +834,7 @@ fn top_panel(ctx: &egui::Context) -> egui::TopBottomPanel { .show_separator_line(false) } -fn render_panel(ctx: &egui::Context, app: &mut Damus, timeline_ind: usize) { +fn render_panel(ctx: &egui::Context, app: &mut Damus) { top_panel(ctx).show(ctx, |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { ui.visuals_mut().button_frame = false; @@ -843,168 +880,21 @@ fn render_panel(ctx: &egui::Context, app: &mut Damus, timeline_ind: usize) { app.frame_history.mean_frame_time() * 1e3 )); - if !app.timelines.is_empty() { + /* + if !app.timelines().count().is_empty() { ui.weak(format!( "{} notes", - &app.timelines[timeline_ind] + &app.timelines() .notes(ViewFilter::NotesAndReplies) .len() )); } + */ } }); }); } -/// Local thread unsubscribe -fn thread_unsubscribe(app: &mut Damus, id: &[u8; 32]) { - let (unsubscribe, remote_subid) = { - let txn = Transaction::new(&app.ndb).expect("txn"); - let root_id = crate::note::root_note_id_from_selected_id(app, &txn, id); - - let thread = app.threads.thread_mut(&app.ndb, &txn, root_id).get_ptr(); - let unsub = thread.decrement_sub(); - - let mut remote_subid: Option = None; - if let Ok(DecrementResult::LastSubscriber(_subid)) = unsub { - *thread.subscription_mut() = None; - remote_subid = thread.remote_subscription().to_owned(); - *thread.remote_subscription_mut() = None; - } - - (unsub, remote_subid) - }; - - match unsubscribe { - Ok(DecrementResult::LastSubscriber(sub)) => { - if let Err(e) = app.ndb.unsubscribe(sub) { - error!( - "failed to unsubscribe from thread: {e}, subid:{}, {} active subscriptions", - sub.id(), - app.ndb.subscription_count() - ); - } else { - info!( - "Unsubscribed from thread subid:{}. {} active subscriptions", - sub.id(), - app.ndb.subscription_count() - ); - } - - // unsub from remote - if let Some(subid) = remote_subid { - app.pool.unsubscribe(subid); - } - } - - Ok(DecrementResult::ActiveSubscribers) => { - info!( - "Keeping thread subscription. {} active subscriptions.", - app.ndb.subscription_count() - ); - // do nothing - } - - Err(e) => { - // something is wrong! - error!( - "Thread unsubscribe error: {e}. {} active subsciptions.", - app.ndb.subscription_count() - ); - } - } -} - -fn render_nav(routes: Vec, timeline_ind: usize, app: &mut Damus, ui: &mut egui::Ui) { - let navigating = app.timelines[timeline_ind].navigating; - let returning = app.timelines[timeline_ind].returning; - let app_ctx = Rc::new(RefCell::new(app)); - - let nav_response = Nav::new(routes) - .navigating(navigating) - .returning(returning) - .title(false) - .show(ui, |ui, nav| match nav.top() { - Route::Timeline(_n) => { - let app = &mut app_ctx.borrow_mut(); - ui::TimelineView::new(app, timeline_ind).ui(ui); - None - } - - Route::ManageAccount => { - ui.label("account management view"); - None - } - - Route::Relays => { - let pool = &mut app_ctx.borrow_mut().pool; - let manager = RelayPoolManager::new(pool); - RelayView::new(manager).ui(ui); - None - } - - Route::Thread(id) => { - let app = &mut app_ctx.borrow_mut(); - let result = ui::ThreadView::new(app, timeline_ind, id.bytes()).ui(ui); - - if let Some(bar_result) = result { - match bar_result { - BarResult::NewThreadNotes(new_notes) => { - let thread = app.threads.thread_expected_mut(new_notes.root_id.bytes()); - new_notes.process(thread); - } - } - } - - None - } - - Route::Reply(id) => { - let mut app = app_ctx.borrow_mut(); - - let txn = if let Ok(txn) = Transaction::new(&app.ndb) { - txn - } else { - ui.label("Reply to unknown note"); - return None; - }; - - let note = if let Ok(note) = app.ndb.get_note_by_id(&txn, id.bytes()) { - note - } else { - ui.label("Reply to unknown note"); - return None; - }; - - let id = egui::Id::new(("post", timeline_ind, note.key().unwrap())); - let response = egui::ScrollArea::vertical().show(ui, |ui| { - ui::PostReplyView::new(&mut app, ¬e) - .id_source(id) - .show(ui) - }); - - Some(response) - } - }); - - let mut app = app_ctx.borrow_mut(); - if let Some(reply_response) = nav_response.inner { - if let Some(PostAction::Post(_np)) = reply_response.inner.action { - app.timelines[timeline_ind].returning = true; - } - } - - if let Some(NavAction::Returned) = nav_response.action { - let popped = app.timelines[timeline_ind].routes.pop(); - if let Some(Route::Thread(id)) = popped { - thread_unsubscribe(&mut app, id.bytes()); - } - app.timelines[timeline_ind].returning = false; - } else if let Some(NavAction::Navigated) = nav_response.action { - app.timelines[timeline_ind].navigating = false; - } -} - fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { //render_panel(ctx, app, 0); @@ -1014,7 +904,9 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { //let routes = app.timelines[0].routes.clone(); main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { - render_nav(app.timelines[0].routes.clone(), 0, app, ui); + if !app.columns.columns().is_empty() { + nav::render_nav(false, 0, app, ui); + } }); } @@ -1033,12 +925,12 @@ fn main_panel(style: &Style, narrow: bool) -> egui::CentralPanel { } fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { - render_panel(ctx, app, 0); + render_panel(ctx, app); #[cfg(feature = "profiling")] puffin::profile_function!(); let screen_size = ctx.screen_rect().width(); - let calc_panel_width = (screen_size / app.timelines.len() as f32) - 30.0; + let calc_panel_width = (screen_size / app.columns.columns().len() as f32) - 30.0; let min_width = 320.0; let need_scroll = calc_panel_width < min_width; let panel_sizes = if need_scroll { @@ -1050,21 +942,20 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { ui.spacing_mut().item_spacing.x = 0.0; AccountSelectionWidget::ui(app, ui); - DesktopGlobalPopup::show(app.global_nav.clone(), app, ui); if need_scroll { egui::ScrollArea::horizontal().show(ui, |ui| { - timelines_view(ui, panel_sizes, app, app.timelines.len()); + timelines_view(ui, panel_sizes, app, app.columns.columns().len()); }); } else { - timelines_view(ui, panel_sizes, app, app.timelines.len()); + timelines_view(ui, panel_sizes, app, app.columns.columns().len()); } }); } -fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, timelines: usize) { +fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usize) { StripBuilder::new(ui) .size(Size::exact(40.0)) - .sizes(sizes, timelines) + .sizes(sizes, columns) .clip(true) .horizontal(|mut strip| { strip.cell(|ui| { @@ -1085,15 +976,25 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, timelines: us ); }); - for timeline_ind in 0..timelines { + let n_cols = app.columns.columns().len(); + let mut first = true; + for column_ind in 0..n_cols { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - render_nav( - app.timelines[timeline_ind].routes.clone(), - timeline_ind, - app, - ui, - ); + let show_postbox = first + && app + .columns + .column(column_ind) + .router() + .routes() + .iter() + .find_map(|r| r.timeline_id()) + .is_some(); + if show_postbox { + first = false + } + + nav::render_nav(show_postbox, column_ind, app, ui); // vertical line ui.painter().vline( diff --git a/src/args.rs b/src/args.rs index 59e9066..3d7d8ca 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,6 +1,5 @@ -use crate::column::{ColumnKind, PubkeySource}; use crate::filter::FilterState; -use crate::timeline::Timeline; +use crate::timeline::{PubkeySource, Timeline, TimelineKind}; use enostr::{Filter, Keypair, Pubkey, SecretKey}; use nostrdb::Ndb; use tracing::{debug, error, info}; @@ -137,22 +136,24 @@ impl Args { if let Some(rest) = column_name.strip_prefix("contacts:") { if let Ok(pubkey) = Pubkey::parse(rest) { info!("contact column for user {}", pubkey.hex()); - res.columns.push(ArgColumn::Column(ColumnKind::contact_list( - PubkeySource::Explicit(pubkey), - ))) + res.columns + .push(ArgColumn::Timeline(TimelineKind::contact_list( + PubkeySource::Explicit(pubkey), + ))) } else { error!("error parsing contacts pubkey {}", rest); continue; } } else if column_name == "contacts" { - res.columns.push(ArgColumn::Column(ColumnKind::contact_list( - PubkeySource::DeckAuthor, - ))) + res.columns + .push(ArgColumn::Timeline(TimelineKind::contact_list( + PubkeySource::DeckAuthor, + ))) } else if let Some(notif_pk_str) = column_name.strip_prefix("notifications:") { if let Ok(pubkey) = Pubkey::parse(notif_pk_str) { info!("got notifications column for user {}", pubkey.hex()); res.columns - .push(ArgColumn::Column(ColumnKind::notifications( + .push(ArgColumn::Timeline(TimelineKind::notifications( PubkeySource::Explicit(pubkey), ))) } else { @@ -162,21 +163,22 @@ impl Args { } else if column_name == "notifications" { debug!("got notification column for default user"); res.columns - .push(ArgColumn::Column(ColumnKind::notifications( + .push(ArgColumn::Timeline(TimelineKind::notifications( PubkeySource::DeckAuthor, ))) } else if column_name == "profile" { debug!("got profile column for default user"); - res.columns.push(ArgColumn::Column(ColumnKind::profile( + res.columns.push(ArgColumn::Timeline(TimelineKind::profile( PubkeySource::DeckAuthor, ))) } else if column_name == "universe" { debug!("got universe column"); - res.columns.push(ArgColumn::Column(ColumnKind::Universe)) + res.columns + .push(ArgColumn::Timeline(TimelineKind::Universe)) } else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") { if let Ok(pubkey) = Pubkey::parse(profile_pk_str) { info!("got profile column for user {}", pubkey.hex()); - res.columns.push(ArgColumn::Column(ColumnKind::profile( + res.columns.push(ArgColumn::Timeline(TimelineKind::profile( PubkeySource::Explicit(pubkey), ))) } else { @@ -214,9 +216,9 @@ impl Args { } if res.columns.is_empty() { - let ck = ColumnKind::contact_list(PubkeySource::DeckAuthor); + let ck = TimelineKind::contact_list(PubkeySource::DeckAuthor); info!("No columns set, setting up defaults: {:?}", ck); - res.columns.push(ArgColumn::Column(ck)); + res.columns.push(ArgColumn::Timeline(ck)); } res @@ -226,7 +228,7 @@ impl Args { /// A way to define columns from the commandline. Can be column kinds or /// generic queries pub enum ArgColumn { - Column(ColumnKind), + Timeline(TimelineKind), Generic(Vec), } @@ -234,10 +236,10 @@ impl ArgColumn { pub fn into_timeline(self, ndb: &Ndb, user: Option<&[u8; 32]>) -> Option { match self { ArgColumn::Generic(filters) => Some(Timeline::new( - ColumnKind::Generic, + TimelineKind::Generic, FilterState::ready(filters), )), - ArgColumn::Column(ck) => ck.into_timeline(ndb, user), + ArgColumn::Timeline(tk) => tk.into_timeline(ndb, user), } } } diff --git a/src/column.rs b/src/column.rs index fa1ab9b..16e4d5b 100644 --- a/src/column.rs +++ b/src/column.rs @@ -1,152 +1,111 @@ -use crate::error::FilterError; -use crate::filter; -use crate::filter::FilterState; -use crate::{timeline::Timeline, Error}; -use enostr::Pubkey; -use nostrdb::{Filter, Ndb, Transaction}; -use std::fmt::Display; -use tracing::{error, warn}; - -#[derive(Clone, Debug)] -pub enum PubkeySource { - Explicit(Pubkey), - DeckAuthor, -} +use crate::route::{Route, Router}; +use crate::timeline::{Timeline, TimelineId}; +use std::iter::Iterator; +use tracing::warn; -#[derive(Debug, Clone)] -pub enum ListKind { - Contact(PubkeySource), +pub struct Column { + router: Router, } -/// -/// What kind of column is it? -/// - Follow List -/// - Notifications -/// - DM -/// - filter -/// - ... etc -#[derive(Debug, Clone)] -pub enum ColumnKind { - List(ListKind), +impl Column { + pub fn new(routes: Vec) -> Self { + let router = Router::new(routes); + Column { router } + } - Notifications(PubkeySource), + pub fn router(&self) -> &Router { + &self.router + } - Profile(PubkeySource), + pub fn router_mut(&mut self) -> &mut Router { + &mut self.router + } +} - Universe, +#[derive(Default)] +pub struct Columns { + /// Columns are simply routers into settings, timelines, etc + columns: Vec, - /// Generic filter - Generic, + /// Timeline state is not tied to routing logic separately, so that + /// different columns can navigate to and from settings to timelines, + /// etc. + pub timelines: Vec, + + /// The selected column for key navigation + selected: i32, } -impl Display for ColumnKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ColumnKind::List(ListKind::Contact(_src)) => f.write_str("Contacts"), - ColumnKind::Generic => f.write_str("Timeline"), - ColumnKind::Notifications(_) => f.write_str("Notifications"), - ColumnKind::Profile(_) => f.write_str("Profile"), - ColumnKind::Universe => f.write_str("Universe"), +impl Columns { + pub fn new() -> Self { + Columns::default() + } + + pub fn add_timeline(&mut self, timeline: Timeline) { + let routes = vec![Route::timeline(timeline.id)]; + self.timelines.push(timeline); + self.columns.push(Column::new(routes)) + } + + pub fn columns_mut(&mut self) -> &mut Vec { + &mut self.columns + } + + pub fn timeline_mut(&mut self, timeline_ind: usize) -> &mut Timeline { + &mut self.timelines[timeline_ind] + } + + pub fn column(&self, ind: usize) -> &Column { + &self.columns()[ind] + } + + pub fn columns(&self) -> &Vec { + &self.columns + } + + pub fn selected(&mut self) -> &mut Column { + &mut self.columns[self.selected as usize] + } + + pub fn timelines_mut(&mut self) -> &mut Vec { + &mut self.timelines + } + + pub fn timelines(&self) -> &Vec { + &self.timelines + } + + pub fn find_timeline_mut(&mut self, id: TimelineId) -> Option<&mut Timeline> { + self.timelines_mut().iter_mut().find(|tl| tl.id == id) + } + + pub fn find_timeline(&self, id: TimelineId) -> Option<&Timeline> { + self.timelines().iter().find(|tl| tl.id == id) + } + + pub fn column_mut(&mut self, ind: usize) -> &mut Column { + &mut self.columns[ind] + } + + pub fn select_down(&mut self) { + warn!("todo: implement select_down"); + } + + pub fn select_up(&mut self) { + warn!("todo: implement select_up"); + } + + pub fn select_left(&mut self) { + if self.selected - 1 < 0 { + return; } + self.selected -= 1; } -} -impl ColumnKind { - pub fn contact_list(pk: PubkeySource) -> Self { - ColumnKind::List(ListKind::Contact(pk)) - } - - pub fn profile(pk: PubkeySource) -> Self { - ColumnKind::Profile(pk) - } - - pub fn notifications(pk: PubkeySource) -> Self { - ColumnKind::Notifications(pk) - } - - pub fn into_timeline(self, ndb: &Ndb, default_user: Option<&[u8; 32]>) -> Option { - match self { - ColumnKind::Universe => Some(Timeline::new( - ColumnKind::Universe, - FilterState::ready(vec![Filter::new() - .kinds([1]) - .limit(filter::default_limit()) - .build()]), - )), - - ColumnKind::Generic => { - warn!("you can't convert a ColumnKind::Generic to a Timeline"); - None - } - - ColumnKind::Profile(pk_src) => { - let pk = match &pk_src { - PubkeySource::DeckAuthor => default_user?, - PubkeySource::Explicit(pk) => pk.bytes(), - }; - - let filter = Filter::new() - .authors([pk]) - .kinds([1]) - .limit(filter::default_limit()) - .build(); - - Some(Timeline::new( - ColumnKind::profile(pk_src), - FilterState::ready(vec![filter]), - )) - } - - ColumnKind::Notifications(pk_src) => { - let pk = match &pk_src { - PubkeySource::DeckAuthor => default_user?, - PubkeySource::Explicit(pk) => pk.bytes(), - }; - - let notifications_filter = Filter::new() - .pubkeys([pk]) - .kinds([1]) - .limit(filter::default_limit()) - .build(); - - Some(Timeline::new( - ColumnKind::notifications(pk_src), - FilterState::ready(vec![notifications_filter]), - )) - } - - ColumnKind::List(ListKind::Contact(pk_src)) => { - let pk = match &pk_src { - PubkeySource::DeckAuthor => default_user?, - PubkeySource::Explicit(pk) => pk.bytes(), - }; - - let contact_filter = Filter::new().authors([pk]).kinds([3]).limit(1).build(); - - let txn = Transaction::new(ndb).expect("txn"); - let results = ndb - .query(&txn, &[contact_filter.clone()], 1) - .expect("contact query failed?"); - - if results.is_empty() { - return Some(Timeline::new( - ColumnKind::contact_list(pk_src), - FilterState::needs_remote(vec![contact_filter.clone()]), - )); - } - - match Timeline::contact_list(&results[0].note) { - Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new( - ColumnKind::contact_list(pk_src), - FilterState::needs_remote(vec![contact_filter]), - )), - Err(e) => { - error!("Unexpected error: {e}"); - None - } - Ok(tl) => Some(tl), - } - } + pub fn select_right(&mut self) { + if self.selected + 1 >= self.columns.len() as i32 { + return; } + self.selected += 1; } } diff --git a/src/draft.rs b/src/draft.rs index f8b63a0..3f62b71 100644 --- a/src/draft.rs +++ b/src/draft.rs @@ -7,16 +7,21 @@ pub struct Draft { #[derive(Default)] pub struct Drafts { - pub replies: HashMap<[u8; 32], Draft>, - pub compose: Draft, + replies: HashMap<[u8; 32], Draft>, + compose: Draft, } impl Drafts { - pub fn clear(&mut self, source: DraftSource) { - source.draft(self).buffer = "".to_string(); + pub fn compose_mut(&mut self) -> &mut Draft { + &mut self.compose + } + + pub fn reply_mut(&mut self, id: &[u8; 32]) -> &mut Draft { + self.replies.entry(*id).or_default() } } +/* pub enum DraftSource<'a> { Compose, Reply(&'a [u8; 32]), // note id @@ -25,14 +30,19 @@ pub enum DraftSource<'a> { impl<'a> DraftSource<'a> { pub fn draft(&self, drafts: &'a mut Drafts) -> &'a mut Draft { match self { - DraftSource::Compose => &mut drafts.compose, - DraftSource::Reply(id) => drafts.replies.entry(**id).or_default(), + DraftSource::Compose => drafts.compose_mut(), + DraftSource::Reply(id) => drafts.reply_mut(id), } } } +*/ impl Draft { pub fn new() -> Self { Draft::default() } + + pub fn clear(&mut self) { + self.buffer = "".to_string(); + } } diff --git a/src/error.rs b/src/error.rs index 5d6d0d6..371aa04 100644 --- a/src/error.rs +++ b/src/error.rs @@ -40,9 +40,10 @@ impl fmt::Display for SubscriptionError { #[derive(Debug)] pub enum Error { + TimelineNotFound, + LoadFailed, SubscriptionError(SubscriptionError), Filter(FilterError), - LoadFailed, Io(io::Error), Nostr(enostr::Error), Ndb(nostrdb::Error), @@ -72,6 +73,7 @@ impl fmt::Display for Error { Self::SubscriptionError(e) => { write!(f, "{e}") } + Self::TimelineNotFound => write!(f, "Timeline not found"), Self::LoadFailed => { write!(f, "load failed") } diff --git a/src/lib.rs b/src/lib.rs index e115153..3337676 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ mod key_parsing; mod key_storage; pub mod login_manager; mod macos_key_storage; +mod nav; mod note; mod notecache; mod post; @@ -37,6 +38,7 @@ mod timeline; pub mod ui; mod unknowns; mod user_account; +mod view_state; #[cfg(test)] #[macro_use] diff --git a/src/login_manager.rs b/src/login_manager.rs index fb9b3ef..2971286 100644 --- a/src/login_manager.rs +++ b/src/login_manager.rs @@ -6,21 +6,17 @@ use poll_promise::Promise; /// The UI view interface to log in to a nostr account. #[derive(Default)] -pub struct LoginManager { +pub struct LoginState { login_key: String, promise_query: Option<(String, Promise>)>, error: Option, key_on_error: Option, + should_create_new: bool, } -impl<'a> LoginManager { +impl<'a> LoginState { pub fn new() -> Self { - LoginManager { - login_key: String::new(), - promise_query: None, - error: None, - key_on_error: None, - } + LoginState::default() } /// Get the textedit for the login UI without exposing the key variable @@ -85,6 +81,14 @@ impl<'a> LoginManager { } None } + + pub fn should_create_new(&mut self) { + self.should_create_new = true; + } + + pub fn check_for_create_new(&self) -> bool { + self.should_create_new + } } #[cfg(test)] @@ -96,7 +100,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_retrieve_key() { - let mut manager = LoginManager::new(); + let mut manager = LoginState::new(); let expected_str = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"; let expected_key = Keypair::only_pubkey(Pubkey::from_hex(expected_str).unwrap()); diff --git a/src/nav.rs b/src/nav.rs new file mode 100644 index 0000000..11536f0 --- /dev/null +++ b/src/nav.rs @@ -0,0 +1,84 @@ +use crate::{ + account_manager::render_accounts_route, + relay_pool_manager::RelayPoolManager, + route::Route, + thread::thread_unsubscribe, + timeline::route::{render_timeline_route, TimelineRoute, TimelineRouteResponse}, + ui::{note::PostAction, RelayView, View}, + Damus, +}; + +use egui_nav::{Nav, NavAction}; + +pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui::Ui) { + let nav_response = Nav::new(app.columns().column(col).router().routes().clone()) + .navigating(app.columns_mut().column_mut(col).router_mut().navigating) + .returning(app.columns_mut().column_mut(col).router_mut().returning) + .title(false) + .show_mut(ui, |ui, nav| match nav.top() { + Route::Timeline(tlr) => render_timeline_route( + &app.ndb, + &mut app.columns, + &mut app.pool, + &mut app.drafts, + &mut app.img_cache, + &mut app.note_cache, + &mut app.threads, + &mut app.accounts, + *tlr, + col, + show_postbox, + app.textmode, + ui, + ), + Route::Accounts(amr) => { + render_accounts_route( + ui, + &app.ndb, + col, + &mut app.columns, + &mut app.img_cache, + &mut app.accounts, + &mut app.view_state.login, + *amr, + ); + None + } + Route::Relays => { + let manager = RelayPoolManager::new(app.pool_mut()); + RelayView::new(manager).ui(ui); + None + } + }); + + if let Some(reply_response) = nav_response.inner { + // start returning when we're finished posting + match reply_response { + TimelineRouteResponse::Post(resp) => { + if let Some(action) = resp.action { + match action { + PostAction::Post(_) => { + app.columns_mut().column_mut(col).router_mut().returning = true; + } + } + } + } + } + } + + if let Some(NavAction::Returned) = nav_response.action { + let r = app.columns_mut().column_mut(col).router_mut().go_back(); + if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { + thread_unsubscribe( + &app.ndb, + &mut app.threads, + &mut app.pool, + &mut app.note_cache, + id.bytes(), + ); + } + app.columns_mut().column_mut(col).router_mut().returning = false; + } else if let Some(NavAction::Navigated) = nav_response.action { + app.columns_mut().column_mut(col).router_mut().navigating = false; + } +} diff --git a/src/note.rs b/src/note.rs index 496b320..a462a7b 100644 --- a/src/note.rs +++ b/src/note.rs @@ -1,5 +1,5 @@ -use crate::Damus; -use nostrdb::{NoteKey, QueryResult, Transaction}; +use crate::notecache::NoteCache; +use nostrdb::{Ndb, Note, NoteKey, QueryResult, Transaction}; use std::cmp::Ordering; #[derive(Debug, Eq, PartialEq, Copy, Clone)] @@ -13,6 +13,12 @@ impl NoteRef { NoteRef { key, created_at } } + pub fn from_note(note: &Note<'_>) -> Self { + let created_at = note.created_at(); + let key = note.key().expect("todo: implement NoteBuf"); + NoteRef::new(key, created_at) + } + pub fn from_query_result(qr: QueryResult<'_>) -> Self { NoteRef { key: qr.note_key, @@ -38,12 +44,12 @@ impl PartialOrd for NoteRef { } pub fn root_note_id_from_selected_id<'a>( - app: &mut Damus, + ndb: &Ndb, + note_cache: &mut NoteCache, txn: &'a Transaction, selected_note_id: &'a [u8; 32], ) -> &'a [u8; 32] { - let selected_note_key = if let Ok(key) = app - .ndb + let selected_note_key = if let Ok(key) = ndb .get_notekey_by_id(txn, selected_note_id) .map(NoteKey::new) { @@ -52,13 +58,13 @@ pub fn root_note_id_from_selected_id<'a>( return selected_note_id; }; - let note = if let Ok(note) = app.ndb.get_note_by_key(txn, selected_note_key) { + let note = if let Ok(note) = ndb.get_note_by_key(txn, selected_note_key) { note } else { return selected_note_id; }; - app.note_cache_mut() + note_cache .cached_note_or_insert(selected_note_key, ¬e) .reply .borrow(note.tags()) diff --git a/src/post.rs b/src/post.rs index 2b86ccd..3433dbb 100644 --- a/src/post.rs +++ b/src/post.rs @@ -1,12 +1,17 @@ +use enostr::FullKeypair; use nostrdb::{Note, NoteBuilder, NoteReply}; use std::collections::HashSet; pub struct NewPost { pub content: String, - pub account: usize, + pub account: FullKeypair, } impl NewPost { + pub fn new(content: String, account: FullKeypair) -> Self { + NewPost { content, account } + } + pub fn to_note(&self, seckey: &[u8; 32]) -> Note { NoteBuilder::new() .kind(1) diff --git a/src/route.rs b/src/route.rs index 5518452..259132c 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,46 +1,107 @@ -use egui::{Response, RichText}; use enostr::NoteId; use std::fmt::{self}; -use crate::{ui::AccountManagementView, Damus}; +use crate::{ + account_manager::AccountsRoute, + timeline::{TimelineId, TimelineRoute}, +}; /// App routing. These describe different places you can go inside Notedeck. -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum Route { - Timeline(String), - ManageAccount, - Thread(NoteId), - Reply(NoteId), + Timeline(TimelineRoute), + Accounts(AccountsRoute), Relays, } -impl fmt::Display for Route { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Route::ManageAccount => write!(f, "Manage Account"), - Route::Timeline(name) => write!(f, "{}", name), - Route::Thread(_id) => write!(f, "Thread"), - Route::Reply(_id) => write!(f, "Reply"), - Route::Relays => write!(f, "Relays"), +impl Route { + pub fn timeline(timeline_id: TimelineId) -> Self { + Route::Timeline(TimelineRoute::Timeline(timeline_id)) + } + + pub fn timeline_id(&self) -> Option<&TimelineId> { + if let Route::Timeline(TimelineRoute::Timeline(tid)) = self { + Some(tid) + } else { + None } } + + pub fn thread(thread_root: NoteId) -> Self { + Route::Timeline(TimelineRoute::Thread(thread_root)) + } + + pub fn reply(replying_to: NoteId) -> Self { + Route::Timeline(TimelineRoute::Reply(replying_to)) + } + + pub fn accounts() -> Self { + Route::Accounts(AccountsRoute::Accounts) + } + + pub fn add_account() -> Self { + Route::Accounts(AccountsRoute::AddAccount) + } } -impl Route { - pub fn show_global_popup(&self, app: &mut Damus, ui: &mut egui::Ui) -> Option { - match self { - Route::ManageAccount => AccountManagementView::ui(app, ui), - _ => None, +// TODO: add this to egui-nav so we don't have to deal with returning +// and navigating headaches +#[derive(Clone)] +pub struct Router { + routes: Vec, + pub returning: bool, + pub navigating: bool, +} + +impl Router { + pub fn new(routes: Vec) -> Self { + if routes.is_empty() { + panic!("routes can't be empty") + } + let returning = false; + let navigating = false; + Router { + routes, + returning, + navigating, } } - pub fn title(&self) -> RichText { + pub fn route_to(&mut self, route: R) { + self.routes.push(route); + } + + pub fn go_back(&mut self) -> Option { + if self.routes.len() == 1 { + return None; + } + self.routes.pop() + } + + pub fn top(&self) -> &R { + self.routes.last().expect("routes can't be empty") + } + + pub fn routes(&self) -> &Vec { + &self.routes + } +} + +impl fmt::Display for Route { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Route::ManageAccount => RichText::new("Manage Account").size(24.0), - Route::Thread(_) => RichText::new("Thread"), - Route::Reply(_) => RichText::new("Reply"), - Route::Relays => RichText::new("Relays"), - Route::Timeline(_) => RichText::new("Timeline"), + Route::Timeline(tlr) => match tlr { + TimelineRoute::Timeline(name) => write!(f, "{}", name), + TimelineRoute::Thread(_id) => write!(f, "Thread"), + TimelineRoute::Reply(_id) => write!(f, "Reply"), + }, + + Route::Relays => write!(f, "Relays"), + + Route::Accounts(amr) => match amr { + AccountsRoute::Accounts => write!(f, "Accounts"), + AccountsRoute::AddAccount => write!(f, "Add Account"), + }, } } } diff --git a/src/subscriptions.rs b/src/subscriptions.rs index 104e9f4..d006bf4 100644 --- a/src/subscriptions.rs +++ b/src/subscriptions.rs @@ -1,4 +1,4 @@ -use crate::column::ColumnKind; +use crate::timeline::{TimelineId, TimelineKind}; use std::collections::HashMap; #[derive(Debug, Clone)] @@ -10,12 +10,12 @@ pub enum SubKind { /// One shot requests, we can just close after we receive EOSE OneShot, - Column(ColumnKind), + Timeline(TimelineKind), /// We are fetching a contact list so that we can use it for our follows /// Filter. // TODO: generalize this to any list? - FetchingContactList(u32), + FetchingContactList(TimelineId), } /// Subscriptions that need to be tracked at various stages. Sometimes we diff --git a/src/test_data.rs b/src/test_data.rs index ddf5dc6..b271dad 100644 --- a/src/test_data.rs +++ b/src/test_data.rs @@ -3,7 +3,7 @@ use std::path::Path; use enostr::{FullKeypair, Pubkey, RelayPool}; use nostrdb::ProfileRecord; -use crate::{account_manager::UserAccount, Damus}; +use crate::{user_account::UserAccount, Damus}; #[allow(unused_must_use)] pub fn sample_pool() -> RelayPool { @@ -55,6 +55,7 @@ const TEST_PROFILE_DATA: [u8; 448] = [ 0x0c, 0x00, 0x24, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x1c, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, ]; +/* const TEST_PUBKEY: [u8; 32] = [ 0x32, 0xe1, 0x82, 0x76, 0x35, 0x45, 0x0e, 0xbb, 0x3c, 0x5a, 0x7d, 0x12, 0xc1, 0xf8, 0xe7, 0xb2, 0xb5, 0x14, 0x43, 0x9a, 0xc1, 0x0a, 0x67, 0xee, 0xf3, 0xd9, 0xfd, 0x9c, 0x5c, 0x68, 0xe2, 0x45, @@ -63,6 +64,7 @@ const TEST_PUBKEY: [u8; 32] = [ pub fn test_pubkey() -> &'static [u8; 32] { &TEST_PUBKEY } +*/ pub fn test_profile_record() -> ProfileRecord<'static> { ProfileRecord::new_owned(&TEST_PROFILE_DATA).unwrap() @@ -99,7 +101,7 @@ pub fn test_app() -> Damus { let accounts = get_test_accounts(); for account in accounts { - app.account_manager.add_account(account); + app.accounts_mut().add_account(account); } app diff --git a/src/thread.rs b/src/thread.rs index 0dcdf71..baa747a 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -1,14 +1,18 @@ -use crate::note::NoteRef; -use crate::timeline::{TimelineTab, ViewFilter}; -use crate::Error; -use nostrdb::{Filter, FilterBuilder, Ndb, Subscription, Transaction}; +use crate::{ + note::NoteRef, + notecache::NoteCache, + timeline::{TimelineTab, ViewFilter}, + Error, Result, +}; +use enostr::RelayPool; +use nostrdb::{Filter, FilterBuilder, Ndb, Note, Subscription, Transaction}; use std::cmp::Ordering; use std::collections::HashMap; -use tracing::{debug, warn}; +use tracing::{debug, error, info, warn}; #[derive(Default)] pub struct Thread { - pub view: TimelineTab, + view: TimelineTab, sub: Option, remote_sub: Option, pub subscribers: i32, @@ -40,6 +44,48 @@ impl Thread { } } + pub fn view(&self) -> &TimelineTab { + &self.view + } + + pub fn view_mut(&mut self) -> &mut TimelineTab { + &mut self.view + } + + #[must_use = "UnknownIds::update_from_note_refs should be used on this result"] + pub fn poll_notes_into_view<'a>( + &mut self, + txn: &'a Transaction, + ndb: &Ndb, + ) -> Result>> { + let sub = self.subscription().expect("thread subscription"); + let new_note_keys = ndb.poll_for_notes(sub, 500); + if new_note_keys.is_empty() { + return Ok(vec![]); + } else { + debug!("{} new notes! {:?}", new_note_keys.len(), new_note_keys); + } + + let mut notes: Vec> = Vec::with_capacity(new_note_keys.len()); + for key in new_note_keys { + let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { + note + } else { + continue; + }; + + notes.push(note); + } + + { + let reversed = true; + let note_refs: Vec = notes.iter().map(|n| NoteRef::from_note(n)).collect(); + self.view.insert(¬e_refs, reversed); + } + + Ok(notes) + } + /// Look for new thread notes since our last fetch pub fn new_notes( notes: &[NoteRef], @@ -66,7 +112,7 @@ impl Thread { } } - pub fn decrement_sub(&mut self) -> Result { + pub fn decrement_sub(&mut self) -> Result { self.subscribers -= 1; match self.subscribers.cmp(&0) { @@ -165,7 +211,7 @@ impl Threads { // also use hashbrown? if self.root_id_to_thread.contains_key(root_id) { - return ThreadResult::Stale(self.root_id_to_thread.get_mut(root_id).unwrap()); + return ThreadResult::Stale(self.thread_expected_mut(root_id)); } // we don't have the thread, query for it! @@ -198,3 +244,68 @@ impl Threads { //fn thread_by_id(&self, ndb: &Ndb, id: &[u8; 32]) -> &mut Thread { //} } + +/// Local thread unsubscribe +pub fn thread_unsubscribe( + ndb: &Ndb, + threads: &mut Threads, + pool: &mut RelayPool, + note_cache: &mut NoteCache, + id: &[u8; 32], +) { + let (unsubscribe, remote_subid) = { + let txn = Transaction::new(ndb).expect("txn"); + let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, &txn, id); + + let thread = threads.thread_mut(ndb, &txn, root_id).get_ptr(); + let unsub = thread.decrement_sub(); + + let mut remote_subid: Option = None; + if let Ok(DecrementResult::LastSubscriber(_subid)) = unsub { + *thread.subscription_mut() = None; + remote_subid = thread.remote_subscription().to_owned(); + *thread.remote_subscription_mut() = None; + } + + (unsub, remote_subid) + }; + + match unsubscribe { + Ok(DecrementResult::LastSubscriber(sub)) => { + if let Err(e) = ndb.unsubscribe(sub) { + error!( + "failed to unsubscribe from thread: {e}, subid:{}, {} active subscriptions", + sub.id(), + ndb.subscription_count() + ); + } else { + info!( + "Unsubscribed from thread subid:{}. {} active subscriptions", + sub.id(), + ndb.subscription_count() + ); + } + + // unsub from remote + if let Some(subid) = remote_subid { + pool.unsubscribe(subid); + } + } + + Ok(DecrementResult::ActiveSubscribers) => { + info!( + "Keeping thread subscription. {} active subscriptions.", + ndb.subscription_count() + ); + // do nothing + } + + Err(e) => { + // something is wrong! + error!( + "Thread unsubscribe error: {e}. {} active subsciptions.", + ndb.subscription_count() + ); + } + } +} diff --git a/src/timeline/kind.rs b/src/timeline/kind.rs new file mode 100644 index 0000000..016706e --- /dev/null +++ b/src/timeline/kind.rs @@ -0,0 +1,153 @@ +use crate::error::{Error, FilterError}; +use crate::filter; +use crate::filter::FilterState; +use crate::timeline::Timeline; +use enostr::{Filter, Pubkey}; +use nostrdb::{Ndb, Transaction}; +use std::fmt::Display; +use tracing::{error, warn}; + +#[derive(Clone, Debug)] +pub enum PubkeySource { + Explicit(Pubkey), + DeckAuthor, +} + +#[derive(Debug, Clone)] +pub enum ListKind { + Contact(PubkeySource), +} + +/// +/// What kind of timeline is it? +/// - Follow List +/// - Notifications +/// - DM +/// - filter +/// - ... etc +/// +#[derive(Debug, Clone)] +pub enum TimelineKind { + List(ListKind), + + Notifications(PubkeySource), + + Profile(PubkeySource), + + Universe, + + /// Generic filter + Generic, +} + +impl Display for TimelineKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TimelineKind::List(ListKind::Contact(_src)) => f.write_str("Contacts"), + TimelineKind::Generic => f.write_str("Timeline"), + TimelineKind::Notifications(_) => f.write_str("Notifications"), + TimelineKind::Profile(_) => f.write_str("Profile"), + TimelineKind::Universe => f.write_str("Universe"), + } + } +} + +impl TimelineKind { + pub fn contact_list(pk: PubkeySource) -> Self { + TimelineKind::List(ListKind::Contact(pk)) + } + + pub fn profile(pk: PubkeySource) -> Self { + TimelineKind::Profile(pk) + } + + pub fn notifications(pk: PubkeySource) -> Self { + TimelineKind::Notifications(pk) + } + + pub fn into_timeline(self, ndb: &Ndb, default_user: Option<&[u8; 32]>) -> Option { + match self { + TimelineKind::Universe => Some(Timeline::new( + TimelineKind::Universe, + FilterState::ready(vec![Filter::new() + .kinds([1]) + .limit(filter::default_limit()) + .build()]), + )), + + TimelineKind::Generic => { + warn!("you can't convert a TimelineKind::Generic to a Timeline"); + None + } + + TimelineKind::Profile(pk_src) => { + let pk = match &pk_src { + PubkeySource::DeckAuthor => default_user?, + PubkeySource::Explicit(pk) => pk.bytes(), + }; + + let filter = Filter::new() + .authors([pk]) + .kinds([1]) + .limit(filter::default_limit()) + .build(); + + Some(Timeline::new( + TimelineKind::profile(pk_src), + FilterState::ready(vec![filter]), + )) + } + + TimelineKind::Notifications(pk_src) => { + let pk = match &pk_src { + PubkeySource::DeckAuthor => default_user?, + PubkeySource::Explicit(pk) => pk.bytes(), + }; + + let notifications_filter = Filter::new() + .pubkeys([pk]) + .kinds([1]) + .limit(crate::filter::default_limit()) + .build(); + + Some(Timeline::new( + TimelineKind::notifications(pk_src), + FilterState::ready(vec![notifications_filter]), + )) + } + + TimelineKind::List(ListKind::Contact(pk_src)) => { + let pk = match &pk_src { + PubkeySource::DeckAuthor => default_user?, + PubkeySource::Explicit(pk) => pk.bytes(), + }; + + let contact_filter = Filter::new().authors([pk]).kinds([3]).limit(1).build(); + + let txn = Transaction::new(ndb).expect("txn"); + let results = ndb + .query(&txn, &[contact_filter.clone()], 1) + .expect("contact query failed?"); + + if results.is_empty() { + return Some(Timeline::new( + TimelineKind::contact_list(pk_src), + FilterState::needs_remote(vec![contact_filter.clone()]), + )); + } + + match Timeline::contact_list(&results[0].note) { + Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new( + TimelineKind::contact_list(pk_src), + FilterState::needs_remote(vec![contact_filter]), + )), + Err(e) => { + error!("Unexpected error: {e}"); + None + } + Ok(tl) => Some(tl), + } + } + } + } +} diff --git a/src/timeline.rs b/src/timeline/mod.rs similarity index 74% rename from src/timeline.rs rename to src/timeline/mod.rs index 6c2d1b5..f5365c8 100644 --- a/src/timeline.rs +++ b/src/timeline/mod.rs @@ -1,140 +1,39 @@ -use crate::column::{ColumnKind, PubkeySource}; use crate::error::Error; use crate::note::NoteRef; -use crate::notecache::CachedNote; +use crate::notecache::{CachedNote, NoteCache}; use crate::unknowns::UnknownIds; +use crate::Result; use crate::{filter, filter::FilterState}; -use crate::{Damus, Result}; +use std::fmt; use std::sync::atomic::{AtomicU32, Ordering}; -use crate::route::Route; - use egui_virtual_list::VirtualList; use enostr::Pubkey; -use nostrdb::{Note, Subscription, Transaction}; +use nostrdb::{Ndb, Note, Subscription, Transaction}; use std::cell::RefCell; +use std::hash::Hash; use std::rc::Rc; use tracing::{debug, error}; -#[derive(Debug, Copy, Clone)] -pub enum TimelineSource<'a> { - Column { ind: usize }, - Thread(&'a [u8; 32]), -} +pub mod kind; +pub mod route; -impl<'a> TimelineSource<'a> { - pub fn column(ind: usize) -> Self { - TimelineSource::Column { ind } - } +pub use kind::{PubkeySource, TimelineKind}; +pub use route::TimelineRoute; - pub fn view<'b>( - self, - app: &'b mut Damus, - txn: &Transaction, - filter: ViewFilter, - ) -> &'b mut TimelineTab { - match self { - TimelineSource::Column { ind, .. } => app.timelines[ind].view_mut(filter), - TimelineSource::Thread(root_id) => { - // TODO: replace all this with the raw entry api eventually +#[derive(Debug, Hash, Copy, Clone, Eq, PartialEq)] +pub struct TimelineId(u32); - let thread = if app.threads.root_id_to_thread.contains_key(root_id) { - app.threads.thread_expected_mut(root_id) - } else { - app.threads.thread_mut(&app.ndb, txn, root_id).get_ptr() - }; - - &mut thread.view - } - } +impl TimelineId { + pub fn new(id: u32) -> Self { + TimelineId(id) } +} - pub fn sub(self, app: &mut Damus, txn: &Transaction) -> Option { - match self { - TimelineSource::Column { ind, .. } => app.timelines[ind].subscription, - TimelineSource::Thread(root_id) => { - // TODO: replace all this with the raw entry api eventually - - let thread = if app.threads.root_id_to_thread.contains_key(root_id) { - app.threads.thread_expected_mut(root_id) - } else { - app.threads.thread_mut(&app.ndb, txn, root_id).get_ptr() - }; - - thread.subscription() - } - } - } - - /// Check local subscriptions for new notes and insert them into - /// timelines (threads, columns) - pub fn poll_notes_into_view(&self, txn: &Transaction, app: &mut Damus) -> Result<()> { - let sub = if let Some(sub) = self.sub(app, txn) { - sub - } else { - return Err(Error::no_active_sub()); - }; - - let new_note_ids = app.ndb.poll_for_notes(sub, 100); - if new_note_ids.is_empty() { - return Ok(()); - } else { - debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids); - } - - let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len()); - - for key in new_note_ids { - let note = if let Ok(note) = app.ndb.get_note_by_key(txn, key) { - note - } else { - error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key); - continue; - }; - - UnknownIds::update_from_note(txn, app, ¬e); - - let created_at = note.created_at(); - new_refs.push((note, NoteRef { key, created_at })); - } - - // We're assuming reverse-chronological here (timelines). This - // flag ensures we trigger the items_inserted_at_start - // optimization in VirtualList. We need this flag because we can - // insert notes into chronological order sometimes, and this - // optimization doesn't make sense in those situations. - let reversed = false; - - // ViewFilter::NotesAndReplies - { - let refs: Vec = new_refs.iter().map(|(_note, nr)| *nr).collect(); - - let reversed = false; - self.view(app, txn, ViewFilter::NotesAndReplies) - .insert(&refs, reversed); - } - - // - // handle the filtered case (ViewFilter::Notes, no replies) - // - // TODO(jb55): this is mostly just copied from above, let's just use a loop - // I initially tried this but ran into borrow checker issues - { - let mut filtered_refs = Vec::with_capacity(new_refs.len()); - for (note, nr) in &new_refs { - let cached_note = app.note_cache_mut().cached_note_or_insert(nr.key, note); - - if ViewFilter::filter_notes(cached_note, note) { - filtered_refs.push(*nr); - } - } - - self.view(app, txn, ViewFilter::Notes) - .insert(&filtered_refs, reversed); - } - - Ok(()) +impl fmt::Display for TimelineId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "TimelineId({})", self.0) } } @@ -181,7 +80,7 @@ impl ViewFilter { /// are "Notes" and "Notes & Replies". A timeline is associated with a Filter, /// but a TimelineTab is a further filtered view of this Filter that can't /// be captured by a Filter itself. -#[derive(Default)] +#[derive(Default, Debug)] pub struct TimelineTab { pub notes: Vec, pub selection: i32, @@ -265,17 +164,15 @@ impl TimelineTab { } /// A column in a deck. Holds navigation state, loaded notes, column kind, etc. +#[derive(Debug)] pub struct Timeline { - pub uid: u32, - pub kind: ColumnKind, + pub id: TimelineId, + pub kind: TimelineKind, // We may not have the filter loaded yet, so let's make it an option so // that codepaths have to explicitly handle it pub filter: FilterState, pub views: Vec, pub selected_view: i32, - pub routes: Vec, - pub navigating: bool, - pub returning: bool, /// Our nostrdb subscription pub subscription: Option, @@ -288,12 +185,20 @@ impl Timeline { let pk_src = PubkeySource::Explicit(Pubkey::new(*contact_list.pubkey())); Ok(Timeline::new( - ColumnKind::contact_list(pk_src), + TimelineKind::contact_list(pk_src), FilterState::ready(filter), )) } - pub fn new(kind: ColumnKind, filter: FilterState) -> Self { + pub fn make_view_id(id: TimelineId, selected_view: i32) -> egui::Id { + egui::Id::new((id, selected_view)) + } + + pub fn view_id(&self) -> egui::Id { + Timeline::make_view_id(self.id, self.selected_view) + } + + pub fn new(kind: TimelineKind, filter: FilterState) -> Self { // global unique id for all new timelines static UIDS: AtomicU32 = AtomicU32::new(0); @@ -302,21 +207,15 @@ impl Timeline { let replies = TimelineTab::new(ViewFilter::NotesAndReplies); let views = vec![notes, replies]; let selected_view = 0; - let routes = vec![Route::Timeline(format!("{}", kind))]; - let navigating = false; - let returning = false; - let uid = UIDS.fetch_add(1, Ordering::Relaxed); + let id = TimelineId::new(UIDS.fetch_add(1, Ordering::Relaxed)); Timeline { - uid, + id, kind, - navigating, - returning, filter, views, subscription, selected_view, - routes, } } @@ -339,6 +238,80 @@ impl Timeline { pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab { &mut self.views[view.index()] } + + pub fn poll_notes_into_view( + timeline_idx: usize, + timelines: &mut [Timeline], + ndb: &Ndb, + txn: &Transaction, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + ) -> Result<()> { + let timeline = &mut timelines[timeline_idx]; + let sub = timeline.subscription.ok_or(Error::no_active_sub())?; + + let new_note_ids = ndb.poll_for_notes(sub, 500); + if new_note_ids.is_empty() { + return Ok(()); + } else { + debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids); + } + + let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len()); + + for key in new_note_ids { + let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { + note + } else { + error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key); + continue; + }; + + UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e); + + let created_at = note.created_at(); + new_refs.push((note, NoteRef { key, created_at })); + } + + // We're assuming reverse-chronological here (timelines). This + // flag ensures we trigger the items_inserted_at_start + // optimization in VirtualList. We need this flag because we can + // insert notes into chronological order sometimes, and this + // optimization doesn't make sense in those situations. + let reversed = false; + + // ViewFilter::NotesAndReplies + { + let refs: Vec = new_refs.iter().map(|(_note, nr)| *nr).collect(); + + let reversed = false; + timeline + .view_mut(ViewFilter::NotesAndReplies) + .insert(&refs, reversed); + } + + // + // handle the filtered case (ViewFilter::Notes, no replies) + // + // TODO(jb55): this is mostly just copied from above, let's just use a loop + // I initially tried this but ran into borrow checker issues + { + let mut filtered_refs = Vec::with_capacity(new_refs.len()); + for (note, nr) in &new_refs { + let cached_note = note_cache.cached_note_or_insert(nr.key, note); + + if ViewFilter::filter_notes(cached_note, note) { + filtered_refs.push(*nr); + } + } + + timeline + .view_mut(ViewFilter::Notes) + .insert(&filtered_refs, reversed); + } + + Ok(()) + } } pub enum MergeKind { diff --git a/src/timeline/route.rs b/src/timeline/route.rs new file mode 100644 index 0000000..d506004 --- /dev/null +++ b/src/timeline/route.rs @@ -0,0 +1,113 @@ +use crate::{ + account_manager::AccountManager, + column::Columns, + draft::Drafts, + imgcache::ImageCache, + notecache::NoteCache, + thread::Threads, + timeline::TimelineId, + ui::{self, note::post::PostResponse}, +}; + +use enostr::{NoteId, RelayPool}; +use nostrdb::{Ndb, Transaction}; + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum TimelineRoute { + Timeline(TimelineId), + Thread(NoteId), + Reply(NoteId), +} + +pub enum TimelineRouteResponse { + Post(PostResponse), +} + +impl TimelineRouteResponse { + pub fn post(post: PostResponse) -> Self { + TimelineRouteResponse::Post(post) + } +} + +#[allow(clippy::too_many_arguments)] +pub fn render_timeline_route( + ndb: &Ndb, + columns: &mut Columns, + pool: &mut RelayPool, + drafts: &mut Drafts, + img_cache: &mut ImageCache, + note_cache: &mut NoteCache, + threads: &mut Threads, + accounts: &mut AccountManager, + route: TimelineRoute, + col: usize, + show_postbox: bool, + textmode: bool, + ui: &mut egui::Ui, +) -> Option { + match route { + TimelineRoute::Timeline(timeline_id) => { + if show_postbox { + if let Some(kp) = accounts.selected_or_first_nsec() { + ui::timeline::postbox_view(ndb, kp, pool, drafts, img_cache, ui); + } + } + + if let Some(bar_action) = + ui::TimelineView::new(timeline_id, columns, ndb, note_cache, img_cache, textmode) + .ui(ui) + { + let txn = Transaction::new(ndb).expect("txn"); + let router = columns.columns_mut()[col].router_mut(); + + bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); + } + + None + } + + TimelineRoute::Thread(id) => { + if let Some(bar_action) = + ui::ThreadView::new(threads, ndb, note_cache, img_cache, id.bytes(), textmode) + .id_source(egui::Id::new(("threadscroll", col))) + .ui(ui) + { + let txn = Transaction::new(ndb).expect("txn"); + let router = columns.columns_mut()[col].router_mut(); + bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); + } + + None + } + + TimelineRoute::Reply(id) => { + let txn = if let Ok(txn) = Transaction::new(ndb) { + txn + } else { + ui.label("Reply to unknown note"); + return None; + }; + + let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) { + note + } else { + ui.label("Reply to unknown note"); + return None; + }; + + let id = egui::Id::new(("post", col, note.key().unwrap())); + + if let Some(poster) = accounts.selected_or_first_nsec() { + let response = egui::ScrollArea::vertical().show(ui, |ui| { + ui::PostReplyView::new(ndb, poster, pool, drafts, note_cache, img_cache, ¬e) + .id_source(id) + .show(ui) + }); + + Some(TimelineRouteResponse::post(response.inner)) + } else { + None + } + } + } +} diff --git a/src/ui/account_login_view.rs b/src/ui/account_login_view.rs index 79a2d05..8e1b7ab 100644 --- a/src/ui/account_login_view.rs +++ b/src/ui/account_login_view.rs @@ -1,99 +1,34 @@ use crate::app_style::NotedeckTextStyle; use crate::key_parsing::LoginError; -use crate::login_manager::LoginManager; +use crate::login_manager::LoginState; use crate::ui::{Preview, PreviewConfig, View}; -use egui::{ - Align, Align2, Button, Color32, Frame, Id, LayerId, Margin, Pos2, Rect, RichText, Rounding, Ui, - Vec2, Window, -}; -use egui::{Image, TextEdit}; +use egui::TextEdit; +use egui::{Align, Button, Color32, Frame, InnerResponse, Margin, RichText, Vec2}; +use enostr::Keypair; pub struct AccountLoginView<'a> { - is_mobile: bool, - manager: &'a mut LoginManager, - generate_y_intercept: Option, + manager: &'a mut LoginState, } -impl<'a> View for AccountLoginView<'a> { - fn ui(&mut self, ui: &mut egui::Ui) { - if let Some(_key) = self.manager.check_for_successful_login() { - // TODO: route to "home" - /* - return if self.mobile { - // route to "home" on mobile - } else { - // route to "home" on desktop - }; - */ - } - if self.is_mobile { - self.show_mobile(ui); - } else { - self.ui(ui); - } - } +pub enum AccountLoginResponse { + CreateNew, + LoginWith(Keypair), } impl<'a> AccountLoginView<'a> { - pub fn new(manager: &'a mut LoginManager, is_mobile: bool) -> Self { - AccountLoginView { - is_mobile, - manager, - generate_y_intercept: None, - } + pub fn new(state: &'a mut LoginState) -> Self { + AccountLoginView { manager: state } } - fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { - let screen_width = ui.ctx().screen_rect().max.x; - let screen_height = ui.ctx().screen_rect().max.y; - - let title_layer = LayerId::new(egui::Order::Background, Id::new("Title layer")); - - let mut top_panel_height: Option = None; - ui.with_layer_id(title_layer, |ui| { - egui::TopBottomPanel::top("Top") - .resizable(false) - .default_height(340.0) - .frame(Frame::none()) - .show_separator_line(false) - .show_inside(ui, |ui| { - top_panel_height = Some(ui.available_rect_before_wrap().bottom()); - self.top_title_area(ui); - }); - }); - - egui::TopBottomPanel::bottom("Bottom") - .resizable(false) - .frame(Frame::none()) - .show_separator_line(false) - .show_inside(ui, |ui| { - self.window(ui, top_panel_height.unwrap_or(0.0)); - }); - - let top_rect = Rect { - min: Pos2::ZERO, - max: Pos2::new( - screen_width, - self.generate_y_intercept.unwrap_or(screen_height * 0.5), - ), - }; - - let top_background_color = ui.visuals().noninteractive().bg_fill; - ui.painter_at(top_rect) - .with_layer_id(LayerId::background()) - .rect_filled(top_rect, Rounding::ZERO, top_background_color); - - egui::CentralPanel::default() - .show(ui.ctx(), |_ui: &mut egui::Ui| {}) - .response + pub fn ui(&mut self, ui: &mut egui::Ui) -> InnerResponse> { + Frame::none() + .outer_margin(12.0) + .show(ui, |ui| self.show(ui)) } - fn mobile_ui(&mut self, ui: &mut egui::Ui) -> egui::Response { + fn show(&mut self, ui: &mut egui::Ui) -> Option { ui.vertical(|ui| { ui.vertical_centered(|ui| { - ui.add(logo_unformatted().max_width(256.0)); - ui.add_space(64.0); - ui.label(login_info_text()); ui.add_space(32.0); ui.label(login_title_text()); }); @@ -123,123 +58,19 @@ impl<'a> AccountLoginView<'a> { .add(Button::new(RichText::new("Create Account")).frame(false)) .clicked() { - // TODO: navigate to 'create account' screen + self.manager.should_create_new(); } }); - }) - .response - } - - pub fn show_mobile(&mut self, ui: &mut egui::Ui) -> egui::Response { - egui::CentralPanel::default() - .show(ui.ctx(), |_| { - Window::new("Login") - .movable(true) - .constrain(true) - .collapsible(false) - .drag_to_scroll(false) - .title_bar(false) - .resizable(false) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .frame(Frame::central_panel(&ui.ctx().style())) - .max_width(ui.ctx().screen_rect().width() - 32.0) // margin - .show(ui.ctx(), |ui| self.mobile_ui(ui)); - }) - .response - } - - fn window(&mut self, ui: &mut Ui, top_panel_height: f32) { - let needed_height_over_top = (ui.ctx().screen_rect().bottom() / 2.0) - 230.0; - let y_offset = if top_panel_height > needed_height_over_top { - top_panel_height - needed_height_over_top - } else { - 0.0 - }; - Window::new("Account login") - .movable(false) - .constrain(true) - .collapsible(false) - .drag_to_scroll(false) - .title_bar(false) - .resizable(false) - .anchor(Align2::CENTER_CENTER, [0f32, y_offset]) - .max_width(538.0) - .frame(egui::Frame::window(ui.style()).inner_margin(Margin::ZERO)) - .show(ui.ctx(), |ui| { - ui.vertical_centered(|ui| { - ui.add_space(40.0); - - ui.label(login_title_text()); - - ui.add_space(16f32); - - ui.label(login_window_info_text(ui)); - - ui.add_space(24.0); - - Frame::none() - .outer_margin(Margin::symmetric(48.0, 0.0)) - .show(ui, |ui| { - self.login_form(ui); - }); - - ui.add_space(32.0); - - let y_margin: f32 = 24.0; - let generate_frame = egui::Frame::default() - .fill(ui.style().noninteractive().bg_fill) // TODO: gradient - .rounding(ui.style().visuals.window_rounding) - .stroke(ui.style().noninteractive().bg_stroke) - .inner_margin(Margin::symmetric(48.0, y_margin)); - - generate_frame.show(ui, |ui| { - self.generate_y_intercept = - Some(ui.available_rect_before_wrap().top() - y_margin); - self.generate_group(ui); - }); - }); - }); - } - - fn top_title_area(&mut self, ui: &mut egui::Ui) { - ui.vertical_centered(|ui| { - ui.add(logo_unformatted().max_width(232.0)); - - ui.add_space(48.0); - - let welcome_data = egui::include_image!("../../assets/Welcome to Nostrdeck 2x.png"); - ui.add(egui::Image::new(welcome_data).max_width(528.0)); - - ui.add_space(12.0); - - // ui.label( - // RichText::new("Welcome to Nostrdeck") - // .size(48.0) - // .strong() - // .line_height(Some(72.0)), - // ); - ui.label(login_info_text()); }); - } - - fn login_form(&mut self, ui: &mut egui::Ui) { - ui.vertical_centered_justified(|ui| { - ui.horizontal(|ui| { - ui.label(login_textedit_info_text()); - }); - - ui.add_space(8f32); - - ui.add(login_textedit(self.manager).min_size(Vec2::new(440.0, 40.0))); - - self.loading_and_error(ui); - let login_button = login_button().min_size(Vec2::new(442.0, 40.0)); + if self.manager.check_for_create_new() { + return Some(AccountLoginResponse::CreateNew); + } - if ui.add(login_button).clicked() { - self.manager.apply_login() - } - }); + if let Some(keypair) = self.manager.check_for_successful_login() { + return Some(AccountLoginResponse::LoginWith(keypair)); + } + None } fn loading_and_error(&mut self, ui: &mut egui::Ui) { @@ -257,33 +88,6 @@ impl<'a> AccountLoginView<'a> { ui.add_space(8.0); } - - fn generate_group(&mut self, ui: &mut egui::Ui) { - ui.horizontal(|ui| { - ui.label( - RichText::new("New in nostr?").text_style(NotedeckTextStyle::Heading3.text_style()), - ); - - ui.label( - RichText::new(" — we got you!") - .text_style(NotedeckTextStyle::Heading3.text_style()) - .color(ui.visuals().noninteractive().fg_stroke.color), - ); - }); - - ui.add_space(6.0); - - ui.horizontal(|ui| { - ui.label(generate_info_text().color(ui.visuals().noninteractive().fg_stroke.color)); - }); - - ui.add_space(16.0); - - let generate_button = generate_keys_button().min_size(Vec2::new(442.0, 40.0)); - if ui.add(generate_button).clicked() { - // TODO: keygen - } - } } fn show_error(ui: &mut egui::Ui, err: &LoginError) { @@ -306,37 +110,12 @@ fn login_title_text() -> RichText { .strong() } -fn login_info_text() -> RichText { - RichText::new("The best alternative to tweetDeck built in nostr protocol") - .text_style(NotedeckTextStyle::Heading3.text_style()) -} - -fn login_window_info_text(ui: &Ui) -> RichText { - RichText::new("Enter your private key to start using Notedeck") - .text_style(NotedeckTextStyle::Body.text_style()) - .color(ui.visuals().noninteractive().fg_stroke.color) -} - fn login_textedit_info_text() -> RichText { RichText::new("Enter your key") .strong() .text_style(NotedeckTextStyle::Body.text_style()) } -fn logo_unformatted() -> Image<'static> { - let logo_gradient_data = egui::include_image!("../../assets/Logo-Gradient-2x.png"); - return egui::Image::new(logo_gradient_data); -} - -fn generate_info_text() -> RichText { - RichText::new("Quickly generate your keys. Make sure you save them safely.") - .text_style(NotedeckTextStyle::Body.text_style()) -} - -fn generate_keys_button() -> Button<'static> { - Button::new(RichText::new("Generate keys").text_style(NotedeckTextStyle::Body.text_style())) -} - fn login_button() -> Button<'static> { Button::new( RichText::new("Login now — let's do this!") @@ -347,7 +126,7 @@ fn login_button() -> Button<'static> { .min_size(Vec2::new(0.0, 40.0)) } -fn login_textedit(manager: &mut LoginManager) -> TextEdit { +fn login_textedit(manager: &mut LoginState) -> TextEdit { manager.get_login_textedit(|text| { egui::TextEdit::singleline(text) .hint_text( @@ -359,23 +138,26 @@ fn login_textedit(manager: &mut LoginManager) -> TextEdit { }) } -pub struct AccountLoginPreview { - is_mobile: bool, - manager: LoginManager, -} +mod preview { + use super::*; -impl View for AccountLoginPreview { - fn ui(&mut self, ui: &mut egui::Ui) { - AccountLoginView::new(&mut self.manager, self.is_mobile).ui(ui); + pub struct AccountLoginPreview { + manager: LoginState, } -} -impl<'a> Preview for AccountLoginView<'a> { - type Prev = AccountLoginPreview; + impl View for AccountLoginPreview { + fn ui(&mut self, ui: &mut egui::Ui) { + AccountLoginView::new(&mut self.manager).ui(ui); + } + } - fn preview(cfg: PreviewConfig) -> Self::Prev { - let manager = LoginManager::new(); - let is_mobile = cfg.is_mobile; - AccountLoginPreview { is_mobile, manager } + impl<'a> Preview for AccountLoginView<'a> { + type Prev = AccountLoginPreview; + + fn preview(cfg: PreviewConfig) -> Self::Prev { + let _ = cfg; + let manager = LoginState::new(); + AccountLoginPreview { manager } + } } } diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs index 46e3e5e..7814c17 100644 --- a/src/ui/account_management.rs +++ b/src/ui/account_management.rs @@ -1,159 +1,159 @@ use crate::colors::PINK; +use crate::imgcache::ImageCache; use crate::{ account_manager::AccountManager, - app_style::NotedeckTextStyle, - ui, - ui::{profile_preview_controller, Preview, PreviewConfig, View}, + route::{Route, Router}, + ui::{Preview, PreviewConfig, View}, Damus, }; -use egui::{Align, Button, Frame, Image, Layout, Response, RichText, ScrollArea, Vec2}; +use egui::{Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, Vec2}; +use nostrdb::{Ndb, Transaction}; use super::profile::preview::SimpleProfilePreview; use super::profile::ProfilePreviewOp; +use super::profile_preview_controller::profile_preview_view; -pub struct AccountManagementView {} +pub struct AccountsView<'a> { + ndb: &'a Ndb, + accounts: &'a AccountManager, + img_cache: &'a mut ImageCache, +} -impl AccountManagementView { - pub fn ui(app: &mut Damus, ui: &mut egui::Ui) -> Option { - if ui::is_narrow(ui.ctx()) { - AccountManagementView::show_mobile(app, ui); - None - } else { - Some(AccountManagementView::show(app, ui)) - } - } +#[derive(Clone, Debug)] +pub enum AccountsViewResponse { + SelectAccount(usize), + RemoveAccount(usize), + RouteToLogin, +} - fn show(app: &mut Damus, ui: &mut egui::Ui) -> Response { - Frame::none() - .outer_margin(24.0) - .show(ui, |ui| { - Self::top_section_buttons_widget(ui); - ui.add_space(8.0); - scroll_area().show(ui, |ui| { - Self::show_accounts(app, ui); - }); - }) - .response +impl<'a> AccountsView<'a> { + pub fn new(ndb: &'a Ndb, accounts: &'a AccountManager, img_cache: &'a mut ImageCache) -> Self { + AccountsView { + ndb, + accounts, + img_cache, + } } - fn show_accounts(app: &mut Damus, ui: &mut egui::Ui) { - let maybe_remove = - profile_preview_controller::set_profile_previews(app, ui, account_card_ui()); + pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse> { + Frame::none().outer_margin(12.0).show(ui, |ui| { + if let Some(resp) = Self::top_section_buttons_widget(ui).inner { + return Some(resp); + } - Self::maybe_remove_accounts(&mut app.account_manager, maybe_remove); + ui.add_space(8.0); + scroll_area() + .show(ui, |ui| { + Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache) + }) + .inner + }) } - fn show_accounts_mobile(app: &mut Damus, ui: &mut egui::Ui) { + fn show_accounts( + ui: &mut Ui, + account_manager: &AccountManager, + ndb: &Ndb, + img_cache: &mut ImageCache, + ) -> Option { + let mut return_op: Option = None; ui.allocate_ui_with_layout( Vec2::new(ui.available_size_before_wrap().x, 32.0), Layout::top_down(egui::Align::Min), |ui| { - // create all account 'cards' and get the indicies the user requested to remove - let maybe_remove = profile_preview_controller::set_profile_previews( - app, - ui, - account_card_ui(), // closure for creating an account 'card' - ); - - // remove all account indicies user requested - Self::maybe_remove_accounts(&mut app.account_manager, maybe_remove); + let txn = if let Ok(txn) = Transaction::new(ndb) { + txn + } else { + return; + }; + + for i in 0..account_manager.num_accounts() { + let account_pubkey = account_manager + .get_account(i) + .map(|account| account.pubkey.bytes()); + + let account_pubkey = if let Some(pubkey) = account_pubkey { + pubkey + } else { + continue; + }; + + let profile = ndb.get_profile_by_pubkey(&txn, account_pubkey).ok(); + let is_selected = + if let Some(selected) = account_manager.get_selected_account_index() { + i == selected + } else { + false + }; + + if let Some(op) = + profile_preview_view(ui, profile.as_ref(), img_cache, is_selected) + { + return_op = Some(match op { + ProfilePreviewOp::SwitchTo => AccountsViewResponse::SelectAccount(i), + ProfilePreviewOp::RemoveAccount => { + AccountsViewResponse::RemoveAccount(i) + } + }); + } + } }, ); + return_op } - fn maybe_remove_accounts(manager: &mut AccountManager, account_indices: Option>) { - if let Some(to_remove) = account_indices { - to_remove - .iter() - .for_each(|index| manager.remove_account(*index)); - } - } - - fn show_mobile(app: &mut Damus, ui: &mut egui::Ui) { - mobile_title(ui); - Self::top_section_buttons_widget(ui); - - ui.add_space(8.0); - scroll_area().show(ui, |ui| Self::show_accounts_mobile(app, ui)); - } - - fn top_section_buttons_widget(ui: &mut egui::Ui) -> egui::Response { - ui.horizontal(|ui| { - ui.allocate_ui_with_layout( - Vec2::new(ui.available_size_before_wrap().x, 32.0), - Layout::left_to_right(egui::Align::Center), - |ui| { - if ui.add(add_account_button()).clicked() { - // TODO: route to AccountLoginView - } - }, - ); - - // UNCOMMENT FOR LOGOUTALL BUTTON - // ui.allocate_ui_with_layout( - // Vec2::new(ui.available_size_before_wrap().x, 32.0), - // Layout::right_to_left(egui::Align::Center), - // |ui| { - // if ui.add(logout_all_button()).clicked() { - // for index in (0..self.account_manager.num_accounts()).rev() { - // self.account_manager.remove_account(index); - // } - // } - // }, - // ); - }) - .response + fn top_section_buttons_widget( + ui: &mut egui::Ui, + ) -> InnerResponse> { + ui.allocate_ui_with_layout( + Vec2::new(ui.available_size_before_wrap().x, 32.0), + Layout::left_to_right(egui::Align::Center), + |ui| { + if ui.add(add_account_button()).clicked() { + Some(AccountsViewResponse::RouteToLogin) + } else { + None + } + }, + ) } } -fn account_card_ui() -> fn( +pub fn show_profile_card( ui: &mut egui::Ui, preview: SimpleProfilePreview, width: f32, is_selected: bool, ) -> Option { - |ui, preview, width, is_selected| { - let mut op: Option = None; + let mut op: Option = None; - ui.add_sized(Vec2::new(width, 50.0), |ui: &mut egui::Ui| { - Frame::none() - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.add(preview); - - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if is_selected { - ui.add(selected_widget()); - } else { - if ui - .add(switch_button(ui.style().visuals.dark_mode)) - .clicked() - { - op = Some(ProfilePreviewOp::SwitchTo); - } - if ui.add(sign_out_button(ui)).clicked() { - op = Some(ProfilePreviewOp::RemoveAccount) - } + ui.add_sized(Vec2::new(width, 50.0), |ui: &mut egui::Ui| { + Frame::none() + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.add(preview); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if is_selected { + ui.add(selected_widget()); + } else { + if ui + .add(switch_button(ui.style().visuals.dark_mode)) + .clicked() + { + op = Some(ProfilePreviewOp::SwitchTo); } - }); + if ui.add(sign_out_button(ui)).clicked() { + op = Some(ProfilePreviewOp::RemoveAccount) + } + } }); - }) - .response - }); - ui.add_space(16.0); - op - } -} - -fn mobile_title(ui: &mut egui::Ui) -> egui::Response { - ui.vertical_centered(|ui| { - ui.label( - RichText::new("Account Management") - .text_style(NotedeckTextStyle::Heading2.text_style()) - .strong(), - ); - }) - .response + }); + }) + .response + }); + ui.add_space(16.0); + op } fn scroll_area() -> ScrollArea { @@ -205,45 +205,45 @@ fn selected_widget() -> impl egui::Widget { } } -// fn logout_all_button() -> egui::Button<'static> { -// egui::Button::new("Logout all") -// } - // PREVIEWS - mod preview { use super::*; - use crate::test_data; + use crate::{account_manager::process_accounts_view_response, test_data}; - pub struct AccountManagementPreview { + pub struct AccountsPreview { app: Damus, + router: Router, } - impl AccountManagementPreview { + impl AccountsPreview { fn new() -> Self { let app = test_data::test_app(); + let router = Router::new(vec![Route::accounts()]); - AccountManagementPreview { app } + AccountsPreview { app, router } } } - impl View for AccountManagementPreview { + impl View for AccountsPreview { fn ui(&mut self, ui: &mut egui::Ui) { ui.add_space(24.0); - if ui::is_narrow(ui.ctx()) { - AccountManagementView::show_mobile(&mut self.app, ui); - } else { - AccountManagementView::show(&mut self.app, ui); + // TODO(jb55): maybe just use render_nav here so we can step through routes + if let Some(response) = + AccountsView::new(&self.app.ndb, &self.app.accounts, &mut self.app.img_cache) + .ui(ui) + .inner + { + process_accounts_view_response(self.app.accounts_mut(), response, &mut self.router); } } } - impl Preview for AccountManagementView { - type Prev = AccountManagementPreview; + impl<'a> Preview for AccountsView<'a> { + type Prev = AccountsPreview; fn preview(_cfg: PreviewConfig) -> Self::Prev { - AccountManagementPreview::new() + AccountsPreview::new() } } } diff --git a/src/ui/account_switcher.rs b/src/ui/account_switcher.rs index 713023f..6454894 100644 --- a/src/ui/account_switcher.rs +++ b/src/ui/account_switcher.rs @@ -1,6 +1,6 @@ use crate::{ - account_manager::UserAccount, colors::PINK, profile::DisplayName, route::Route, ui, - ui::profile_preview_controller, Damus, Result, + colors::PINK, profile::DisplayName, ui, ui::profile_preview_controller, + user_account::UserAccount, Damus, Result, }; use nostrdb::Ndb; @@ -17,7 +17,6 @@ pub struct AccountSelectionWidget {} enum AccountSelectAction { RemoveAccount { _index: usize }, SelectAccount { _index: usize }, - OpenAccountManagement, } #[derive(Default)] @@ -50,23 +49,18 @@ impl AccountSelectionWidget { fn perform_action(app: &mut Damus, action: AccountSelectAction) { match action { AccountSelectAction::RemoveAccount { _index } => { - app.account_manager.remove_account(_index) + app.accounts_mut().remove_account(_index) } AccountSelectAction::SelectAccount { _index } => { app.show_account_switcher = false; - app.account_manager.select_account(_index); - } - AccountSelectAction::OpenAccountManagement => { - app.show_account_switcher = false; - app.global_nav.push(Route::ManageAccount); - app.show_global_popup = true; + app.accounts_mut().select_account(_index); } } } fn show(app: &mut Damus, ui: &mut egui::Ui) -> (AccountSelectResponse, egui::Response) { let mut res = AccountSelectResponse::default(); - let mut selected_index = app.account_manager.get_selected_account_index(); + let mut selected_index = app.accounts().get_selected_account_index(); let response = Frame::none() .outer_margin(8.0) @@ -83,9 +77,9 @@ impl AccountSelectionWidget { ui.add(add_account_button()); if let Some(_index) = selected_index { - if let Some(account) = app.account_manager.get_account(_index) { + if let Some(account) = app.accounts().get_account(_index) { ui.add_space(8.0); - if Self::handle_sign_out(&app.ndb, ui, account) { + if Self::handle_sign_out(app.ndb(), ui, account) { res.action = Some(AccountSelectAction::RemoveAccount { _index }) } } @@ -198,7 +192,7 @@ fn selection_widget() -> impl egui::Widget { fn top_section_widget(ui: &mut egui::Ui) -> AccountSelectResponse { ui.horizontal(|ui| { - let mut resp = AccountSelectResponse::default(); + let resp = AccountSelectResponse::default(); ui.allocate_ui_with_layout( Vec2::new(ui.available_size_before_wrap().x, 32.0), @@ -211,7 +205,7 @@ fn top_section_widget(ui: &mut egui::Ui) -> AccountSelectResponse { Layout::right_to_left(egui::Align::Center), |ui| { if ui.add(manage_accounts_button()).clicked() { - resp.action = Some(AccountSelectAction::OpenAccountManagement); + // resp.action = Some(AccountSelectAction::OpenAccountManagement); TODO implement after temporary column impl is finished } }, ); diff --git a/src/ui/fixed_window.rs b/src/ui/fixed_window.rs deleted file mode 100644 index ed82c10..0000000 --- a/src/ui/fixed_window.rs +++ /dev/null @@ -1,63 +0,0 @@ -use egui::{Rect, Response, RichText, Sense, Window}; - -#[derive(Default)] -pub struct FixedWindow { - title: Option, -} - -#[derive(PartialEq)] -pub enum FixedWindowResponse { - Opened, - Closed, -} - -impl FixedWindow { - #[allow(dead_code)] - pub fn new() -> Self { - FixedWindow::default() - } - - pub fn maybe_with_title(maybe_title: Option) -> Self { - Self { title: maybe_title } - } - - #[allow(dead_code)] - pub fn with_title(mut self, title: RichText) -> Self { - self.title = Some(title); - self - } - - pub fn show( - self, - ui: &mut egui::Ui, - rect: Rect, - add_contents: impl FnOnce(&mut egui::Ui) -> Response, - ) -> FixedWindowResponse { - let mut is_open = true; - - let use_title_bar = self.title.is_some(); - let title = if let Some(title) = self.title { - title - } else { - RichText::new("") - }; - - Window::new(title) - .open(&mut is_open) - .fixed_rect(rect) - .collapsible(false) - .movable(false) - .resizable(false) - .title_bar(use_title_bar) - .show(ui.ctx(), |ui| { - let resp = add_contents(ui); - ui.allocate_rect(resp.rect, Sense::hover()) - }); - - if !is_open { - FixedWindowResponse::Closed - } else { - FixedWindowResponse::Opened - } - } -} diff --git a/src/ui/global_popup.rs b/src/ui/global_popup.rs deleted file mode 100644 index 0e648a6..0000000 --- a/src/ui/global_popup.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use egui::Sense; -use egui_nav::{Nav, NavAction}; - -use crate::{route::Route, ui, Damus}; - -static MARGIN: f32 = 200.0; - -pub struct DesktopGlobalPopup {} - -impl DesktopGlobalPopup { - pub fn show(routes: Vec, app: &mut Damus, ui: &mut egui::Ui) { - if routes.is_empty() || !app.show_global_popup { - return; - } - - let rect = ui.ctx().screen_rect().shrink(MARGIN); - - let title = routes.last().map(|r| r.title()); - - let app_ctx = Rc::new(RefCell::new(app)); - - let resp = ui::FixedWindow::maybe_with_title(title).show(ui, rect, |ui| { - let nav_response = - Nav::new(routes) - .title(false) - .navigating(false) - .show(ui, |ui, nav| { - if let Some(resp) = - nav.top().show_global_popup(&mut app_ctx.borrow_mut(), ui) - { - ui.allocate_rect(resp.rect, Sense::hover()) - } else { - ui.label("") // TODO(kernelkind): not a great practice - } - }); - - if let Some(NavAction::Returned) = nav_response.action { - app_ctx.borrow_mut().global_nav.pop(); - } - - nav_response.inner - }); - - let mut app = app_ctx.borrow_mut(); - - if resp == ui::FixedWindowResponse::Closed { - app.global_nav.pop(); - app.show_global_popup = false; - } - } -} diff --git a/src/ui/mention.rs b/src/ui/mention.rs index c0556ac..e7353fb 100644 --- a/src/ui/mention.rs +++ b/src/ui/mention.rs @@ -1,8 +1,9 @@ -use crate::{colors, ui, Damus}; -use nostrdb::Transaction; +use crate::{colors, imgcache::ImageCache, ui}; +use nostrdb::{Ndb, Transaction}; pub struct Mention<'a> { - app: &'a mut Damus, + ndb: &'a Ndb, + img_cache: &'a mut ImageCache, txn: &'a Transaction, pk: &'a [u8; 32], selectable: bool, @@ -10,11 +11,17 @@ pub struct Mention<'a> { } impl<'a> Mention<'a> { - pub fn new(app: &'a mut Damus, txn: &'a Transaction, pk: &'a [u8; 32]) -> Self { + pub fn new( + ndb: &'a Ndb, + img_cache: &'a mut ImageCache, + txn: &'a Transaction, + pk: &'a [u8; 32], + ) -> Self { let size = 16.0; let selectable = true; Mention { - app, + ndb, + img_cache, txn, pk, selectable, @@ -35,12 +42,21 @@ impl<'a> Mention<'a> { impl<'a> egui::Widget for Mention<'a> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { - mention_ui(self.app, self.txn, self.pk, ui, self.size, self.selectable) + mention_ui( + self.ndb, + self.img_cache, + self.txn, + self.pk, + ui, + self.size, + self.selectable, + ) } } fn mention_ui( - app: &mut Damus, + ndb: &Ndb, + img_cache: &mut ImageCache, txn: &Transaction, pk: &[u8; 32], ui: &mut egui::Ui, @@ -51,7 +67,7 @@ fn mention_ui( puffin::profile_function!(); ui.horizontal(|ui| { - let profile = app.ndb.get_profile_by_pubkey(txn, pk).ok(); + let profile = ndb.get_profile_by_pubkey(txn, pk).ok(); let name: String = if let Some(name) = profile.as_ref().and_then(crate::profile::get_profile_name) { @@ -68,7 +84,7 @@ fn mention_ui( if let Some(rec) = profile.as_ref() { resp.on_hover_ui_at_pointer(|ui| { ui.set_max_width(300.0); - ui.add(ui::ProfilePreview::new(rec, &mut app.img_cache)); + ui.add(ui::ProfilePreview::new(rec, img_cache)); }); } }) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b01c62d..7da506a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,8 +2,6 @@ pub mod account_login_view; pub mod account_management; pub mod account_switcher; pub mod anim; -pub mod fixed_window; -pub mod global_popup; pub mod mention; pub mod note; pub mod preview; @@ -14,10 +12,8 @@ pub mod thread; pub mod timeline; pub mod username; -pub use account_management::AccountManagementView; +pub use account_management::AccountsView; pub use account_switcher::AccountSelectionWidget; -pub use fixed_window::{FixedWindow, FixedWindowResponse}; -pub use global_popup::DesktopGlobalPopup; pub use mention::Mention; pub use note::{NoteResponse, NoteView, PostReplyView, PostView}; pub use preview::{Preview, PreviewApp, PreviewConfig}; @@ -31,7 +27,9 @@ pub use username::Username; use egui::Margin; /// This is kind of like the Widget trait but is meant for larger top-level -/// views that are typically stateful. The Widget trait forces us to add mutable +/// views that are typically stateful. +/// +/// The Widget trait forces us to add mutable /// implementations at the type level, which screws us when generating Previews /// for a Widget. I would have just Widget instead of making this Trait otherwise. /// diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index 467da02..d44fb8c 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -1,14 +1,17 @@ use crate::images::ImageType; use crate::imgcache::ImageCache; +use crate::notecache::NoteCache; use crate::ui::note::NoteOptions; use crate::ui::ProfilePic; -use crate::{colors, ui, Damus}; +use crate::{colors, ui}; use egui::{Color32, Hyperlink, Image, RichText}; -use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; +use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; use tracing::warn; pub struct NoteContents<'a> { - damus: &'a mut Damus, + ndb: &'a Ndb, + img_cache: &'a mut ImageCache, + note_cache: &'a mut NoteCache, txn: &'a Transaction, note: &'a Note<'a>, note_key: NoteKey, @@ -17,14 +20,18 @@ pub struct NoteContents<'a> { impl<'a> NoteContents<'a> { pub fn new( - damus: &'a mut Damus, + ndb: &'a Ndb, + img_cache: &'a mut ImageCache, + note_cache: &'a mut NoteCache, txn: &'a Transaction, note: &'a Note, note_key: NoteKey, options: ui::note::NoteOptions, ) -> Self { NoteContents { - damus, + ndb, + img_cache, + note_cache, txn, note, note_key, @@ -37,7 +44,9 @@ impl egui::Widget for NoteContents<'_> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { render_note_contents( ui, - self.damus, + self.ndb, + self.img_cache, + self.note_cache, self.txn, self.note, self.note_key, @@ -51,7 +60,9 @@ impl egui::Widget for NoteContents<'_> { /// notes are references within a note fn render_note_preview( ui: &mut egui::Ui, - app: &mut Damus, + ndb: &Ndb, + note_cache: &mut NoteCache, + img_cache: &mut ImageCache, txn: &Transaction, id: &[u8; 32], _id_str: &str, @@ -59,7 +70,7 @@ fn render_note_preview( #[cfg(feature = "profiling")] puffin::profile_function!(); - let note = if let Ok(note) = app.ndb.get_note_by_id(txn, id) { + let note = if let Ok(note) = ndb.get_note_by_id(txn, id) { // TODO: support other preview kinds if note.kind() == 1 { note @@ -92,7 +103,7 @@ fn render_note_preview( ui.visuals().noninteractive().bg_stroke.color, )) .show(ui, |ui| { - ui::NoteView::new(app, ¬e) + ui::NoteView::new(ndb, note_cache, img_cache, ¬e) .actionbar(false) .small_pfp(true) .wide(true) @@ -102,9 +113,12 @@ fn render_note_preview( .response } +#[allow(clippy::too_many_arguments)] fn render_note_contents( ui: &mut egui::Ui, - damus: &mut Damus, + ndb: &Ndb, + img_cache: &mut ImageCache, + note_cache: &mut NoteCache, txn: &Transaction, note: &Note, note_key: NoteKey, @@ -118,7 +132,7 @@ fn render_note_contents( let mut inline_note: Option<(&[u8; 32], &str)> = None; let resp = ui.horizontal_wrapped(|ui| { - let blocks = if let Ok(blocks) = damus.ndb.get_blocks_by_key(txn, note_key) { + let blocks = if let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) { blocks } else { warn!("missing note content blocks? '{}'", note.content()); @@ -132,11 +146,11 @@ fn render_note_contents( match block.blocktype() { BlockType::MentionBech32 => match block.as_mention().unwrap() { Mention::Profile(profile) => { - ui.add(ui::Mention::new(damus, txn, profile.pubkey())); + ui.add(ui::Mention::new(ndb, img_cache, txn, profile.pubkey())); } Mention::Pubkey(npub) => { - ui.add(ui::Mention::new(damus, txn, npub.pubkey())); + ui.add(ui::Mention::new(ndb, img_cache, txn, npub.pubkey())); } Mention::Note(note) if options.has_note_previews() => { @@ -186,13 +200,13 @@ fn render_note_contents( }); if let Some((id, block_str)) = inline_note { - render_note_preview(ui, damus, txn, id, block_str); + render_note_preview(ui, ndb, note_cache, img_cache, txn, id, block_str); } - if !images.is_empty() && !damus.textmode { + if !images.is_empty() && !options.has_textmode() { ui.add_space(2.0); let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note"))); - image_carousel(ui, &mut damus.img_cache, images, carousel_id); + image_carousel(ui, img_cache, images, carousel_id); ui.add_space(2.0); } diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index ee12758..be3b00f 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -8,12 +8,22 @@ pub use options::NoteOptions; pub use post::{PostAction, PostResponse, PostView}; pub use reply::PostReplyView; -use crate::{actionbar::BarAction, colors, notecache::CachedNote, ui, ui::View, Damus}; +use crate::{ + actionbar::BarAction, + colors, + imgcache::ImageCache, + notecache::{CachedNote, NoteCache}, + ui, + ui::View, +}; use egui::{Label, RichText, Sense}; -use nostrdb::{Note, NoteKey, NoteReply, Transaction}; +use enostr::NoteId; +use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; pub struct NoteView<'a> { - app: &'a mut Damus, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, note: &'a nostrdb::Note<'a>, flags: NoteOptions, } @@ -29,7 +39,13 @@ impl<'a> View for NoteView<'a> { } } -fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: &mut Damus) { +fn reply_desc( + ui: &mut egui::Ui, + txn: &Transaction, + note_reply: &NoteReply, + ndb: &Ndb, + img_cache: &mut ImageCache, +) { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -51,7 +67,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: return; }; - let reply_note = if let Ok(reply_note) = app.ndb.get_note_by_id(txn, reply.id) { + let reply_note = if let Ok(reply_note) = ndb.get_note_by_id(txn, reply.id) { reply_note } else { ui.add( @@ -68,7 +84,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: if note_reply.is_reply_to_root() { // We're replying to the root, let's show this ui.add( - ui::Mention::new(app, txn, reply_note.pubkey()) + ui::Mention::new(ndb, img_cache, txn, reply_note.pubkey()) .size(size) .selectable(selectable), ); @@ -83,11 +99,11 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: } else if let Some(root) = note_reply.root() { // replying to another post in a thread, not the root - if let Ok(root_note) = app.ndb.get_note_by_id(txn, root.id) { + if let Ok(root_note) = ndb.get_note_by_id(txn, root.id) { if root_note.pubkey() == reply_note.pubkey() { // simply "replying to bob's note" when replying to bob in his thread ui.add( - ui::Mention::new(app, txn, reply_note.pubkey()) + ui::Mention::new(ndb, img_cache, txn, reply_note.pubkey()) .size(size) .selectable(selectable), ); @@ -103,7 +119,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: // replying to bob in alice's thread ui.add( - ui::Mention::new(app, txn, reply_note.pubkey()) + ui::Mention::new(ndb, img_cache, txn, reply_note.pubkey()) .size(size) .selectable(selectable), ); @@ -112,7 +128,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: .selectable(selectable), ); ui.add( - ui::Mention::new(app, txn, root_note.pubkey()) + ui::Mention::new(ndb, img_cache, txn, root_note.pubkey()) .size(size) .selectable(selectable), ); @@ -127,7 +143,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: } } else { ui.add( - ui::Mention::new(app, txn, reply_note.pubkey()) + ui::Mention::new(ndb, img_cache, txn, reply_note.pubkey()) .size(size) .selectable(selectable), ); @@ -144,9 +160,25 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: } impl<'a> NoteView<'a> { - pub fn new(app: &'a mut Damus, note: &'a nostrdb::Note<'a>) -> Self { + pub fn new( + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + note: &'a nostrdb::Note<'a>, + ) -> Self { let flags = NoteOptions::actionbar | NoteOptions::note_previews; - Self { app, note, flags } + Self { + ndb, + note_cache, + img_cache, + note, + flags, + } + } + + pub fn textmode(mut self, enable: bool) -> Self { + self.options_mut().set_textmode(enable); + self } pub fn actionbar(mut self, enable: bool) -> Self { @@ -192,14 +224,13 @@ impl<'a> NoteView<'a> { let txn = self.note.txn().expect("todo: implement non-db notes"); ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { - let profile = self.app.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); + let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); //ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; let cached_note = self - .app - .note_cache_mut() + .note_cache .cached_note_or_insert_mut(note_key, self.note); let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0)); @@ -218,7 +249,13 @@ impl<'a> NoteView<'a> { }); ui.add(NoteContents::new( - self.app, txn, self.note, note_key, self.flags, + self.ndb, + self.img_cache, + self.note_cache, + txn, + self.note, + note_key, + self.flags, )); //}); }) @@ -255,33 +292,26 @@ impl<'a> NoteView<'a> { let profile_key = profile.as_ref().unwrap().record().note_key(); let note_key = note_key.as_u64(); - if ui::is_narrow(ui.ctx()) { - ui.add(ui::ProfilePic::new(&mut self.app.img_cache, pic)); - } else { - let (rect, size, _resp) = ui::anim::hover_expand( - ui, - egui::Id::new((profile_key, note_key)), - pfp_size, - ui::NoteView::expand_size(), - anim_speed, - ); - - ui.put( - rect, - ui::ProfilePic::new(&mut self.app.img_cache, pic).size(size), - ) + let (rect, size, _resp) = ui::anim::hover_expand( + ui, + egui::Id::new((profile_key, note_key)), + pfp_size, + ui::NoteView::expand_size(), + anim_speed, + ); + + ui.put(rect, ui::ProfilePic::new(self.img_cache, pic).size(size)) .on_hover_ui_at_pointer(|ui| { ui.set_max_width(300.0); ui.add(ui::ProfilePreview::new( profile.as_ref().unwrap(), - &mut self.app.img_cache, + self.img_cache, )); }); - } } None => { ui.add( - ui::ProfilePic::new(&mut self.app.img_cache, ui::ProfilePic::no_pfp_url()) + ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()) .size(pfp_size), ); } @@ -289,7 +319,7 @@ impl<'a> NoteView<'a> { } pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse { - if self.app.textmode { + if self.options().has_textmode() { NoteResponse { response: self.textmode_ui(ui), action: None, @@ -301,7 +331,7 @@ impl<'a> NoteView<'a> { fn note_header( ui: &mut egui::Ui, - app: &mut Damus, + note_cache: &mut NoteCache, note: &Note, profile: &Result, nostrdb::Error>, ) -> egui::Response { @@ -311,9 +341,7 @@ impl<'a> NoteView<'a> { ui.spacing_mut().item_spacing.x = 2.0; ui.add(ui::Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20)); - let cached_note = app - .note_cache_mut() - .cached_note_or_insert_mut(note_key, note); + let cached_note = note_cache.cached_note_or_insert_mut(note_key, note); render_reltime(ui, cached_note, true); }) .response @@ -325,7 +353,7 @@ impl<'a> NoteView<'a> { let note_key = self.note.key().expect("todo: support non-db notes"); let txn = self.note.txn().expect("todo: support non-db notes"); let mut note_action: Option = None; - let profile = self.app.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); + let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); // wide design let response = if self.options().has_wide() { @@ -336,28 +364,29 @@ impl<'a> NoteView<'a> { ui.vertical(|ui| { ui.add_sized([size.x, self.options().pfp_size()], |ui: &mut egui::Ui| { ui.horizontal_centered(|ui| { - NoteView::note_header(ui, self.app, self.note, &profile); + NoteView::note_header(ui, self.note_cache, self.note, &profile); }) .response }); let note_reply = self - .app - .note_cache_mut() + .note_cache .cached_note_or_insert_mut(note_key, self.note) .reply .borrow(self.note.tags()); if note_reply.reply().is_some() { ui.horizontal(|ui| { - reply_desc(ui, txn, ¬e_reply, self.app); + reply_desc(ui, txn, ¬e_reply, self.ndb, self.img_cache); }); } }); }); let resp = ui.add(NoteContents::new( - self.app, + self.ndb, + self.img_cache, + self.note_cache, txn, self.note, note_key, @@ -365,7 +394,7 @@ impl<'a> NoteView<'a> { )); if self.options().has_actionbar() { - note_action = render_note_actionbar(ui, note_key).inner; + note_action = render_note_actionbar(ui, self.note.id(), note_key).inner; } resp @@ -375,25 +404,26 @@ impl<'a> NoteView<'a> { self.pfp(note_key, &profile, ui); ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - NoteView::note_header(ui, self.app, self.note, &profile); + NoteView::note_header(ui, self.note_cache, self.note, &profile); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; let note_reply = self - .app - .note_cache_mut() + .note_cache .cached_note_or_insert_mut(note_key, self.note) .reply .borrow(self.note.tags()); if note_reply.reply().is_some() { - reply_desc(ui, txn, ¬e_reply, self.app); + reply_desc(ui, txn, ¬e_reply, self.ndb, self.img_cache); } }); ui.add(NoteContents::new( - self.app, + self.ndb, + self.img_cache, + self.note_cache, txn, self.note, note_key, @@ -401,7 +431,7 @@ impl<'a> NoteView<'a> { )); if self.options().has_actionbar() { - note_action = render_note_actionbar(ui, note_key).inner; + note_action = render_note_actionbar(ui, self.note.id(), note_key).inner; } }); }) @@ -417,6 +447,7 @@ impl<'a> NoteView<'a> { fn render_note_actionbar( ui: &mut egui::Ui, + note_id: &[u8; 32], note_key: NoteKey, ) -> egui::InnerResponse> { ui.horizontal(|ui| { @@ -424,9 +455,9 @@ fn render_note_actionbar( let thread_resp = thread_button(ui, note_key); if reply_resp.clicked() { - Some(BarAction::Reply) + Some(BarAction::Reply(NoteId::new(*note_id))) } else if thread_resp.clicked() { - Some(BarAction::OpenThread) + Some(BarAction::OpenThread(NoteId::new(*note_id))) } else { None } diff --git a/src/ui/note/options.rs b/src/ui/note/options.rs index 8bc3ad0..371e8b4 100644 --- a/src/ui/note/options.rs +++ b/src/ui/note/options.rs @@ -12,6 +12,7 @@ bitflags! { const medium_pfp = 0b00001000; const wide = 0b00010000; const selectable_text = 0b00100000; + const textmode = 0b01000000; } } @@ -33,6 +34,7 @@ impl NoteOptions { create_setter!(set_medium_pfp, medium_pfp); create_setter!(set_note_previews, note_previews); create_setter!(set_selectable_text, selectable_text); + create_setter!(set_textmode, textmode); create_setter!(set_actionbar, actionbar); #[inline] @@ -45,6 +47,11 @@ impl NoteOptions { (self & NoteOptions::selectable_text) == NoteOptions::selectable_text } + #[inline] + pub fn has_textmode(self) -> bool { + (self & NoteOptions::textmode) == NoteOptions::textmode + } + #[inline] pub fn has_note_previews(self) -> bool { (self & NoteOptions::note_previews) == NoteOptions::note_previews diff --git a/src/ui/note/post.rs b/src/ui/note/post.rs index 51a60eb..d3b755d 100644 --- a/src/ui/note/post.rs +++ b/src/ui/note/post.rs @@ -1,16 +1,17 @@ -use crate::app::Damus; -use crate::draft::{Draft, DraftSource}; +use crate::draft::Draft; +use crate::imgcache::ImageCache; use crate::post::NewPost; use crate::ui; use crate::ui::{Preview, PreviewConfig, View}; use egui::widgets::text_edit::TextEdit; -use nostrdb::Transaction; - -pub struct PostView<'app, 'd> { - app: &'app mut Damus, - /// account index - poster: usize, - draft_source: DraftSource<'d>, +use enostr::{FilledKeypair, FullKeypair}; +use nostrdb::{Config, Ndb, Transaction}; + +pub struct PostView<'a> { + ndb: &'a Ndb, + draft: &'a mut Draft, + img_cache: &'a mut ImageCache, + poster: FilledKeypair<'a>, id_source: Option, } @@ -23,14 +24,20 @@ pub struct PostResponse { pub edit_response: egui::Response, } -impl<'app, 'd> PostView<'app, 'd> { - pub fn new(app: &'app mut Damus, draft_source: DraftSource<'d>, poster: usize) -> Self { +impl<'a> PostView<'a> { + pub fn new( + ndb: &'a Ndb, + draft: &'a mut Draft, + img_cache: &'a mut ImageCache, + poster: FilledKeypair<'a>, + ) -> Self { let id_source: Option = None; PostView { - id_source, - app, + ndb, + draft, + img_cache, poster, - draft_source, + id_source, } } @@ -39,47 +46,30 @@ impl<'app, 'd> PostView<'app, 'd> { self } - fn draft(&mut self) -> &mut Draft { - self.draft_source.draft(&mut self.app.drafts) - } - fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response { ui.spacing_mut().item_spacing.x = 12.0; let pfp_size = 24.0; - let poster_pubkey = self - .app - .account_manager - .get_account(self.poster) - .map(|acc| acc.pubkey.bytes()) - .unwrap_or(crate::test_data::test_pubkey()); - // TODO: refactor pfp control to do all of this for us let poster_pfp = self - .app .ndb - .get_profile_by_pubkey(txn, poster_pubkey) + .get_profile_by_pubkey(txn, self.poster.pubkey.bytes()) .as_ref() .ok() - .and_then(|p| { - Some(ui::ProfilePic::from_profile(&mut self.app.img_cache, p)?.size(pfp_size)) - }); + .and_then(|p| Some(ui::ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size))); if let Some(pfp) = poster_pfp { ui.add(pfp); } else { ui.add( - ui::ProfilePic::new(&mut self.app.img_cache, ui::ProfilePic::no_pfp_url()) - .size(pfp_size), + ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size), ); } - let buffer = &mut self.draft_source.draft(&mut self.app.drafts).buffer; - let response = ui.add_sized( ui.available_size(), - TextEdit::multiline(buffer) + TextEdit::multiline(&mut self.draft.buffer) .hint_text(egui::RichText::new("Write a banger note here...").weak()) .frame(false), ); @@ -144,10 +134,10 @@ impl<'app, 'd> PostView<'app, 'd> { .add_sized([91.0, 32.0], egui::Button::new("Post now")) .clicked() { - Some(PostAction::Post(NewPost { - content: self.draft().buffer.clone(), - account: self.poster, - })) + Some(PostAction::Post(NewPost::new( + self.draft.buffer.clone(), + self.poster.to_full(), + ))) } else { None } @@ -167,28 +157,41 @@ impl<'app, 'd> PostView<'app, 'd> { mod preview { use super::*; - use crate::test_data; pub struct PostPreview { - app: Damus, + ndb: Ndb, + img_cache: ImageCache, + draft: Draft, + poster: FullKeypair, } impl PostPreview { fn new() -> Self { + let ndb = Ndb::new(".", &Config::new()).expect("ndb"); + PostPreview { - app: test_data::test_app(), + ndb, + img_cache: ImageCache::new(".".into()), + draft: Draft::new(), + poster: FullKeypair::generate(), } } } impl View for PostPreview { fn ui(&mut self, ui: &mut egui::Ui) { - let txn = Transaction::new(&self.app.ndb).unwrap(); - PostView::new(&mut self.app, DraftSource::Compose, 0).ui(&txn, ui); + let txn = Transaction::new(&self.ndb).expect("txn"); + PostView::new( + &self.ndb, + &mut self.draft, + &mut self.img_cache, + self.poster.to_filled(), + ) + .ui(&txn, ui); } } - impl<'app, 'p> Preview for PostView<'app, 'p> { + impl<'a> Preview for PostView<'a> { type Prev = PostPreview; fn preview(_cfg: PreviewConfig) -> Self::Prev { diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs index 93e5870..b1b7a17 100644 --- a/src/ui/note/reply.rs +++ b/src/ui/note/reply.rs @@ -1,21 +1,43 @@ -use crate::draft::DraftSource; +use crate::draft::Drafts; +use crate::imgcache::ImageCache; +use crate::notecache::NoteCache; +use crate::ui; use crate::ui::note::{PostAction, PostResponse}; -use crate::{ui, Damus}; +use enostr::{FilledKeypair, RelayPool}; +use nostrdb::Ndb; use tracing::info; pub struct PostReplyView<'a> { - app: &'a mut Damus, - id_source: Option, + ndb: &'a Ndb, + poster: FilledKeypair<'a>, + pool: &'a mut RelayPool, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + drafts: &'a mut Drafts, note: &'a nostrdb::Note<'a>, + id_source: Option, } impl<'a> PostReplyView<'a> { - pub fn new(app: &'a mut Damus, note: &'a nostrdb::Note<'a>) -> Self { + pub fn new( + ndb: &'a Ndb, + poster: FilledKeypair<'a>, + pool: &'a mut RelayPool, + drafts: &'a mut Drafts, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + note: &'a nostrdb::Note<'a>, + ) -> Self { let id_source: Option = None; PostReplyView { - app, - id_source, + ndb, + poster, + pool, + drafts, note, + note_cache, + img_cache, + id_source, } } @@ -46,7 +68,7 @@ impl<'a> PostReplyView<'a> { egui::Frame::none() .outer_margin(egui::Margin::same(note_offset)) .show(ui, |ui| { - ui::NoteView::new(self.app, self.note) + ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, self.note) .actionbar(false) .medium_pfp(true) .show(ui); @@ -54,43 +76,26 @@ impl<'a> PostReplyView<'a> { let id = self.id(); let replying_to = self.note.id(); - let draft_source = DraftSource::Reply(replying_to); - let poster = self - .app - .account_manager - .get_selected_account_index() - .unwrap_or(0); let rect_before_post = ui.min_rect(); - let post_response = ui::PostView::new(self.app, draft_source, poster) - .id_source(id) - .ui(self.note.txn().unwrap(), ui); - - if self - .app - .account_manager - .get_selected_account() - .map_or(false, |a| a.secret_key.is_some()) - { - if let Some(action) = &post_response.action { - match action { - PostAction::Post(np) => { - let seckey = self - .app - .account_manager - .get_account(poster) - .unwrap() - .secret_key - .as_ref() - .unwrap() - .to_secret_bytes(); - - let note = np.to_reply(&seckey, self.note); - - let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); - info!("sending {}", raw_msg); - self.app.pool.send(&enostr::ClientMessage::raw(raw_msg)); - self.app.drafts.clear(DraftSource::Reply(replying_to)); - } + + let post_response = { + let draft = self.drafts.reply_mut(replying_to); + ui::PostView::new(self.ndb, draft, self.img_cache, self.poster) + .id_source(id) + .ui(self.note.txn().unwrap(), ui) + }; + + if let Some(action) = &post_response.action { + match action { + PostAction::Post(np) => { + let seckey = self.poster.secret_key.to_secret_bytes(); + + let note = np.to_reply(&seckey, self.note); + + let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); + info!("sending {}", raw_msg); + self.pool.send(&enostr::ClientMessage::raw(raw_msg)); + self.drafts.reply_mut(replying_to).clear(); } } } diff --git a/src/ui/profile/profile_preview_controller.rs b/src/ui/profile/profile_preview_controller.rs index 2d41244..f8bbd7d 100644 --- a/src/ui/profile/profile_preview_controller.rs +++ b/src/ui/profile/profile_preview_controller.rs @@ -1,6 +1,9 @@ -use nostrdb::{Ndb, Transaction}; +use egui::Ui; +use nostrdb::{Ndb, ProfileRecord, Transaction}; -use crate::{Damus, DisplayName, Result}; +use crate::{ + imgcache::ImageCache, ui::account_management::show_profile_card, Damus, DisplayName, Result, +}; use super::{ preview::{get_display_name, get_profile_url, SimpleProfilePreview}, @@ -13,64 +16,16 @@ pub enum ProfilePreviewOp { SwitchTo, } -pub fn set_profile_previews( - app: &mut Damus, - ui: &mut egui::Ui, - add_preview_ui: fn( - ui: &mut egui::Ui, - preview: SimpleProfilePreview, - width: f32, - is_selected: bool, - ) -> Option, -) -> Option> { - let mut to_remove: Option> = None; - +pub fn profile_preview_view( + ui: &mut Ui, + profile: Option<&'_ ProfileRecord<'_>>, + img_cache: &mut ImageCache, + is_selected: bool, +) -> Option { let width = ui.available_width(); - let txn = if let Ok(txn) = Transaction::new(&app.ndb) { - txn - } else { - return None; - }; - - for i in 0..app.account_manager.num_accounts() { - let account = if let Some(account) = app.account_manager.get_account(i) { - account - } else { - continue; - }; - - let profile = app - .ndb - .get_profile_by_pubkey(&txn, account.pubkey.bytes()) - .ok(); - - let preview = SimpleProfilePreview::new(profile.as_ref(), &mut app.img_cache); - - let is_selected = if let Some(selected) = app.account_manager.get_selected_account_index() { - i == selected - } else { - false - }; - - let op = if let Some(op) = add_preview_ui(ui, preview, width, is_selected) { - op - } else { - continue; - }; - - match op { - ProfilePreviewOp::RemoveAccount => { - if to_remove.is_none() { - to_remove = Some(Vec::new()); - } - to_remove.as_mut().unwrap().push(i); - } - ProfilePreviewOp::SwitchTo => app.account_manager.select_account(i), - } - } - - to_remove + let preview = SimpleProfilePreview::new(profile, img_cache); + show_profile_card(ui, preview, width, is_selected) } pub fn view_profile_previews( @@ -86,32 +41,32 @@ pub fn view_profile_previews( ) -> Option { let width = ui.available_width(); - let txn = if let Ok(txn) = Transaction::new(&app.ndb) { + let txn = if let Ok(txn) = Transaction::new(app.ndb()) { txn } else { return None; }; - for i in 0..app.account_manager.num_accounts() { - let account = if let Some(account) = app.account_manager.get_account(i) { + for i in 0..app.accounts().num_accounts() { + let account = if let Some(account) = app.accounts().get_account(i) { account } else { continue; }; let profile = app - .ndb + .ndb() .get_profile_by_pubkey(&txn, account.pubkey.bytes()) .ok(); - let preview = SimpleProfilePreview::new(profile.as_ref(), &mut app.img_cache); - - let is_selected = if let Some(selected) = app.account_manager.get_selected_account_index() { + let is_selected = if let Some(selected) = app.accounts().get_selected_account_index() { i == selected } else { false }; + let preview = SimpleProfilePreview::new(profile.as_ref(), app.img_cache_mut()); + if add_preview_ui(ui, preview, width, is_selected, i) { return Some(i); } @@ -136,16 +91,16 @@ pub fn show_with_selected_pfp( ui: &mut egui::Ui, ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, ) -> Option { - let selected_account = app.account_manager.get_selected_account(); + let selected_account = app.accounts().get_selected_account(); if let Some(selected_account) = selected_account { - if let Ok(txn) = Transaction::new(&app.ndb) { + if let Ok(txn) = Transaction::new(app.ndb()) { let profile = app - .ndb + .ndb() .get_profile_by_pubkey(&txn, selected_account.pubkey.bytes()); return Some(ui_element( ui, - ProfilePic::new(&mut app.img_cache, get_profile_url(profile.ok().as_ref())), + ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())), )); } } @@ -159,12 +114,12 @@ pub fn show_with_pfp( key: &[u8; 32], ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, ) -> Option { - if let Ok(txn) = Transaction::new(&app.ndb) { - let profile = app.ndb.get_profile_by_pubkey(&txn, key); + if let Ok(txn) = Transaction::new(app.ndb()) { + let profile = app.ndb().get_profile_by_pubkey(&txn, key); return Some(ui_element( ui, - ProfilePic::new(&mut app.img_cache, get_profile_url(profile.ok().as_ref())), + ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())), )); } None diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index b6ce089..dc9de6b 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -127,7 +127,7 @@ mod preview { use crate::{ test_data, - ui::{AccountSelectionWidget, DesktopGlobalPopup, Preview, PreviewConfig}, + ui::{AccountSelectionWidget, Preview, PreviewConfig}, }; use super::*; @@ -158,7 +158,6 @@ mod preview { }); AccountSelectionWidget::ui(&mut self.app, ui); - DesktopGlobalPopup::show(self.app.global_nav.clone(), &mut self.app, ui); } } diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 9ee6a9d..0abf67f 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,28 +1,51 @@ -use crate::{actionbar::BarResult, timeline::TimelineSource, ui, Damus}; -use nostrdb::{NoteKey, Transaction}; +use crate::{ + actionbar::BarAction, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, +}; +use nostrdb::{Ndb, NoteKey, Transaction}; use tracing::{error, warn}; pub struct ThreadView<'a> { - app: &'a mut Damus, - timeline: usize, + threads: &'a mut Threads, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, selected_note_id: &'a [u8; 32], + textmode: bool, + id_source: egui::Id, } impl<'a> ThreadView<'a> { - pub fn new(app: &'a mut Damus, timeline: usize, selected_note_id: &'a [u8; 32]) -> Self { + #[allow(clippy::too_many_arguments)] + pub fn new( + threads: &'a mut Threads, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + selected_note_id: &'a [u8; 32], + textmode: bool, + ) -> Self { + let id_source = egui::Id::new("threadscroll_threadview"); ThreadView { - app, - timeline, + threads, + ndb, + note_cache, + img_cache, selected_note_id, + textmode, + id_source, } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { - let txn = Transaction::new(&self.app.ndb).expect("txn"); - let mut result: Option = None; + pub fn id_source(mut self, id: egui::Id) -> Self { + self.id_source = id; + self + } + + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + let txn = Transaction::new(self.ndb).expect("txn"); + let mut action: Option = None; let selected_note_key = if let Ok(key) = self - .app .ndb .get_notekey_by_id(&txn, self.selected_note_id) .map(NoteKey::new) @@ -33,25 +56,18 @@ impl<'a> ThreadView<'a> { return None; }; - let scroll_id = egui::Id::new(( - "threadscroll", - self.app.timelines[self.timeline].selected_view, - self.timeline, - selected_note_key, - )); - ui.label( egui::RichText::new("Threads ALPHA! It's not done. Things will be broken.") .color(egui::Color32::RED), ); egui::ScrollArea::vertical() - .id_source(scroll_id) + .id_source(self.id_source) .animated(false) .auto_shrink([false, false]) .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) .show(ui, |ui| { - let note = if let Ok(note) = self.app.ndb.get_note_by_key(&txn, selected_note_key) { + let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, selected_note_key) { note } else { return; @@ -59,8 +75,7 @@ impl<'a> ThreadView<'a> { let root_id = { let cached_note = self - .app - .note_cache_mut() + .note_cache .cached_note_or_insert(selected_note_key, ¬e); cached_note @@ -70,40 +85,29 @@ impl<'a> ThreadView<'a> { .map_or_else(|| self.selected_note_id, |nr| nr.id) }; + let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr(); + + // TODO(jb55): skip poll if ThreadResult is fresh? + // poll for new notes and insert them into our existing notes - if let Err(e) = TimelineSource::Thread(root_id).poll_notes_into_view(&txn, self.app) - { + if let Err(e) = thread.poll_notes_into_view(&txn, self.ndb) { error!("Thread::poll_notes_into_view: {e}"); } - let (len, list) = { - let thread = self - .app - .threads - .thread_mut(&self.app.ndb, &txn, root_id) - .get_ptr(); + let len = thread.view().notes.len(); - let len = thread.view.notes.len(); - (len, &mut thread.view.list) - }; - - list.clone() - .borrow_mut() - .ui_custom_layout(ui, len, |ui, start_index| { + thread.view().list.clone().borrow_mut().ui_custom_layout( + ui, + len, + |ui, start_index| { ui.spacing_mut().item_spacing.y = 0.0; ui.spacing_mut().item_spacing.x = 4.0; let ind = len - 1 - start_index; - let note_key = { - let thread = self - .app - .threads - .thread_mut(&self.app.ndb, &txn, root_id) - .get_ptr(); - thread.view.notes[ind].key - }; - let note = if let Ok(note) = self.app.ndb.get_note_by_key(&txn, note_key) { + let note_key = thread.view().notes[ind].key; + + let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, note_key) { note } else { warn!("failed to query note {:?}", note_key); @@ -111,16 +115,14 @@ impl<'a> ThreadView<'a> { }; ui::padding(8.0, ui, |ui| { - let textmode = self.app.textmode; - let resp = ui::NoteView::new(self.app, ¬e) - .note_previews(!textmode) - .show(ui); - - if let Some(action) = resp.action { - let br = action.execute(self.app, self.timeline, note.id(), &txn); - if br.is_some() { - result = br; - } + if let Some(bar_action) = + ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, ¬e) + .note_previews(!self.textmode) + .textmode(self.textmode) + .show(ui) + .action + { + action = Some(bar_action); } }); @@ -128,9 +130,10 @@ impl<'a> ThreadView<'a> { //ui.add(egui::Separator::default().spacing(0.0)); 1 - }); + }, + ); }); - result + action } } diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index a3ec99c..a9e6854 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,28 +1,56 @@ -use crate::{actionbar::BarResult, draft::DraftSource, ui, ui::note::PostAction, Damus}; +use crate::{ + actionbar::BarAction, column::Columns, draft::Drafts, imgcache::ImageCache, + notecache::NoteCache, timeline::TimelineId, ui, ui::note::PostAction, +}; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; -use nostrdb::Transaction; -use tracing::{debug, info, warn}; +use enostr::{FilledKeypair, RelayPool}; +use nostrdb::{Ndb, Transaction}; +use tracing::{debug, error, info, warn}; pub struct TimelineView<'a> { - app: &'a mut Damus, + timeline_id: TimelineId, + columns: &'a mut Columns, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + textmode: bool, reverse: bool, - timeline: usize, } impl<'a> TimelineView<'a> { - pub fn new(app: &'a mut Damus, timeline: usize) -> TimelineView<'a> { + pub fn new( + timeline_id: TimelineId, + columns: &'a mut Columns, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + textmode: bool, + ) -> TimelineView<'a> { let reverse = false; TimelineView { - app, - timeline, + ndb, + timeline_id, + columns, + note_cache, + img_cache, reverse, + textmode, } } - pub fn ui(&mut self, ui: &mut egui::Ui) { - timeline_ui(ui, self.app, self.timeline, self.reverse); + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + timeline_ui( + ui, + self.ndb, + self.timeline_id, + self.columns, + self.note_cache, + self.img_cache, + self.reverse, + self.textmode, + ) } pub fn reversed(mut self) -> Self { @@ -31,33 +59,61 @@ impl<'a> TimelineView<'a> { } } -fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bool) { +#[allow(clippy::too_many_arguments)] +fn timeline_ui( + ui: &mut egui::Ui, + ndb: &Ndb, + timeline_id: TimelineId, + columns: &mut Columns, + note_cache: &mut NoteCache, + img_cache: &mut ImageCache, + reversed: bool, + textmode: bool, +) -> Option { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* let font_id = egui::TextStyle::Body.resolve(ui.style()); let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y; + */ - if timeline == 0 { - postbox_view(app, ui); - } + let scroll_id = { + let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { + timeline + } else { + error!("tried to render timeline in column, but timeline was missing"); + // TODO (jb55): render error when timeline is missing? + // this shouldn't happen... + return None; + }; - app.timelines[timeline].selected_view = tabs_ui(ui); + timeline.selected_view = tabs_ui(ui); - // need this for some reason?? - ui.add_space(3.0); + // need this for some reason?? + ui.add_space(3.0); + + egui::Id::new(("tlscroll", timeline.view_id())) + }; - let scroll_id = egui::Id::new(("tlscroll", app.timelines[timeline].selected_view, timeline)); + let mut bar_action: Option = None; egui::ScrollArea::vertical() .id_source(scroll_id) .animated(false) .auto_shrink([false, false]) .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .show(ui, |ui| { - let view = app.timelines[timeline].current_view(); + let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { + timeline + } else { + error!("tried to render timeline in column, but timeline was missing"); + // TODO (jb55): render error when timeline is missing? + // this shouldn't happen... + return 0; + }; + + let view = timeline.current_view(); let len = view.notes.len(); - let mut bar_result: Option = None; - let txn = if let Ok(txn) = Transaction::new(&app.ndb) { + let txn = if let Ok(txn) = Transaction::new(ndb) { txn } else { warn!("failed to create transaction"); @@ -77,9 +133,9 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo start_index }; - let note_key = app.timelines[timeline].current_view().notes[ind].key; + let note_key = timeline.current_view().notes[ind].key; - let note = if let Ok(note) = app.ndb.get_note_by_key(&txn, note_key) { + let note = if let Ok(note) = ndb.get_note_by_key(&txn, note_key) { note } else { warn!("failed to query note {:?}", note_key); @@ -87,17 +143,13 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo }; ui::padding(8.0, ui, |ui| { - let textmode = app.textmode; - let resp = ui::NoteView::new(app, ¬e) + let resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e) .note_previews(!textmode) .selectable_text(false) .show(ui); - if let Some(action) = resp.action { - let br = action.execute(app, timeline, note.id(), &txn); - if br.is_some() { - bar_result = br; - } + if let Some(ba) = resp.action { + bar_action = Some(ba); } else if resp.response.clicked() { debug!("clicked note"); } @@ -109,55 +161,33 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo 1 }); - if let Some(br) = bar_result { - match br { - // update the thread for next render if we have new notes - BarResult::NewThreadNotes(new_notes) => { - let thread = app - .threads - .thread_mut(&app.ndb, &txn, new_notes.root_id.bytes()) - .get_ptr(); - new_notes.process(thread); - } - } - } - 1 }); + + bar_action } -fn postbox_view(app: &mut Damus, ui: &mut egui::Ui) { +pub fn postbox_view<'a>( + ndb: &'a Ndb, + key: FilledKeypair<'a>, + pool: &'a mut RelayPool, + drafts: &'a mut Drafts, + img_cache: &'a mut ImageCache, + ui: &'a mut egui::Ui, +) { // show a postbox in the first timeline - - if let Some(account) = app.account_manager.get_selected_account_index() { - if app - .account_manager - .get_selected_account() - .map_or(false, |a| a.secret_key.is_some()) - { - if let Ok(txn) = Transaction::new(&app.ndb) { - let response = ui::PostView::new(app, DraftSource::Compose, account).ui(&txn, ui); - - if let Some(action) = response.action { - match action { - PostAction::Post(np) => { - let seckey = app - .account_manager - .get_account(account) - .unwrap() - .secret_key - .as_ref() - .unwrap() - .to_secret_bytes(); - - let note = np.to_note(&seckey); - let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); - info!("sending {}", raw_msg); - app.pool.send(&enostr::ClientMessage::raw(raw_msg)); - app.drafts.clear(DraftSource::Compose); - } - } - } + let txn = Transaction::new(ndb).expect("txn"); + let response = ui::PostView::new(ndb, drafts.compose_mut(), img_cache, key).ui(&txn, ui); + + if let Some(action) = response.action { + match action { + PostAction::Post(np) => { + let seckey = key.secret_key.to_secret_bytes(); + let note = np.to_note(&seckey); + let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap()); + info!("sending {}", raw_msg); + pool.send(&enostr::ClientMessage::raw(raw_msg)); + drafts.compose_mut().clear(); } } } diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs index cc6a820..577338b 100644 --- a/src/ui_preview/main.rs +++ b/src/ui_preview/main.rs @@ -1,10 +1,10 @@ use notedeck::app_creation::{ generate_mobile_emulator_native_options, generate_native_options, setup_cc, }; -use notedeck::ui::account_login_view::AccountLoginView; use notedeck::ui::{ - AccountManagementView, AccountSelectionWidget, DesktopSidePanel, PostView, Preview, PreviewApp, - PreviewConfig, ProfilePic, ProfilePreview, RelayView, + account_login_view::AccountLoginView, account_management::AccountsView, AccountSelectionWidget, + DesktopSidePanel, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview, + RelayView, }; use std::env; @@ -100,7 +100,7 @@ async fn main() { AccountLoginView, ProfilePreview, ProfilePic, - AccountManagementView, + AccountsView, AccountSelectionWidget, DesktopSidePanel, PostView, diff --git a/src/unknowns.rs b/src/unknowns.rs index 72ab4fa..a1eabf7 100644 --- a/src/unknowns.rs +++ b/src/unknowns.rs @@ -1,6 +1,11 @@ -use crate::notecache::CachedNote; -use crate::timeline::ViewFilter; -use crate::{Damus, Result}; +use crate::{ + column::Columns, + note::NoteRef, + notecache::{CachedNote, NoteCache}, + timeline::ViewFilter, + Result, +}; + use enostr::{Filter, NoteId, Pubkey}; use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; use std::collections::HashSet; @@ -63,37 +68,74 @@ impl UnknownIds { self.last_updated = Some(now); } - pub fn update_from_note(txn: &Transaction, app: &mut Damus, note: &Note) -> bool { - let before = app.unknown_ids.ids().len(); + pub fn update_from_note_key( + txn: &Transaction, + ndb: &Ndb, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + key: NoteKey, + ) -> bool { + let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { + note + } else { + return false; + }; + + UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e) + } + + /// Should be called on freshly polled notes from subscriptions + pub fn update_from_note_refs( + txn: &Transaction, + ndb: &Ndb, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + note_refs: &[NoteRef], + ) { + for note_ref in note_refs { + Self::update_from_note_key(txn, ndb, unknown_ids, note_cache, note_ref.key); + } + } + + pub fn update_from_note( + txn: &Transaction, + ndb: &Ndb, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + note: &Note, + ) -> bool { + let before = unknown_ids.ids().len(); let key = note.key().expect("note key"); - let cached_note = app - .note_cache_mut() - .cached_note_or_insert(key, note) - .clone(); - if let Err(e) = - get_unknown_note_ids(&app.ndb, &cached_note, txn, note, app.unknown_ids.ids_mut()) - { + //let cached_note = note_cache.cached_note_or_insert(key, note).clone(); + let cached_note = note_cache.cached_note_or_insert(key, note); + if let Err(e) = get_unknown_note_ids(ndb, cached_note, txn, note, unknown_ids.ids_mut()) { error!("UnknownIds::update_from_note {e}"); } - let after = app.unknown_ids.ids().len(); + let after = unknown_ids.ids().len(); if before != after { - app.unknown_ids.mark_updated(); + unknown_ids.mark_updated(); true } else { false } } - pub fn update(txn: &Transaction, app: &mut Damus) -> bool { - let before = app.unknown_ids.ids().len(); - if let Err(e) = get_unknown_ids(txn, app) { + pub fn update( + txn: &Transaction, + unknown_ids: &mut UnknownIds, + columns: &Columns, + ndb: &Ndb, + note_cache: &mut NoteCache, + ) -> bool { + let before = unknown_ids.ids().len(); + if let Err(e) = get_unknown_ids(txn, unknown_ids, columns, ndb, note_cache) { error!("UnknownIds::update {e}"); } - let after = app.unknown_ids.ids().len(); + let after = unknown_ids.ids().len(); if before != after { - app.unknown_ids.mark_updated(); + unknown_ids.mark_updated(); true } else { false @@ -211,17 +253,23 @@ pub fn get_unknown_note_ids<'a>( Ok(()) } -fn get_unknown_ids(txn: &Transaction, damus: &mut Damus) -> Result<()> { +fn get_unknown_ids( + txn: &Transaction, + unknown_ids: &mut UnknownIds, + columns: &Columns, + ndb: &Ndb, + note_cache: &mut NoteCache, +) -> Result<()> { #[cfg(feature = "profiling")] puffin::profile_function!(); let mut new_cached_notes: Vec<(NoteKey, CachedNote)> = vec![]; - for timeline in &damus.timelines { + for timeline in columns.timelines() { for noteref in timeline.notes(ViewFilter::NotesAndReplies) { - let note = damus.ndb.get_note_by_key(txn, noteref.key)?; + let note = ndb.get_note_by_key(txn, noteref.key)?; let note_key = note.key().unwrap(); - let cached_note = damus.note_cache().cached_note(noteref.key); + let cached_note = note_cache.cached_note(noteref.key); let cached_note = if let Some(cn) = cached_note { cn.clone() } else { @@ -230,20 +278,14 @@ fn get_unknown_ids(txn: &Transaction, damus: &mut Damus) -> Result<()> { new_cached_note }; - let _ = get_unknown_note_ids( - &damus.ndb, - &cached_note, - txn, - ¬e, - damus.unknown_ids.ids_mut(), - ); + let _ = get_unknown_note_ids(ndb, &cached_note, txn, ¬e, unknown_ids.ids_mut()); } } // This is mainly done to avoid the double mutable borrow that would happen // if we tried to update the note_cache mutably in the loop above for (note_key, note) in new_cached_notes { - damus.note_cache_mut().cache_mut().insert(note_key, note); + note_cache.cache_mut().insert(note_key, note); } Ok(()) diff --git a/src/view_state.rs b/src/view_state.rs new file mode 100644 index 0000000..33b94b3 --- /dev/null +++ b/src/view_state.rs @@ -0,0 +1,13 @@ +use crate::login_manager::LoginState; + +/// Various state for views +#[derive(Default)] +pub struct ViewState { + pub login: LoginState, +} + +impl ViewState { + pub fn login_mut(&mut self) -> &mut LoginState { + &mut self.login + } +}