From 73d03ca0f1c38803039fb267a7d2847ca2da08ad Mon Sep 17 00:00:00 2001 From: kieran Date: Sun, 12 Jan 2025 11:40:34 +0000 Subject: [PATCH] feat: zap modal --- Cargo.lock | 656 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 +- src/app.rs | 15 +- src/link.rs | 44 ++- src/route/mod.rs | 106 +++++- src/theme.rs | 2 + src/widgets/avatar.rs | 2 +- src/widgets/button.rs | 37 +- src/widgets/chat_zap.rs | 4 +- src/widgets/mod.rs | 1 + src/widgets/stream_title.rs | 20 +- src/widgets/text_input.rs | 2 +- src/widgets/zap.rs | 286 ++++++++++++++++ src/zap.rs | 17 + 14 files changed, 1146 insertions(+), 54 deletions(-) create mode 100644 src/widgets/zap.rs diff --git a/Cargo.lock b/Cargo.lock index 0c987c1..663b2c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -886,6 +897,16 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +[[package]] +name = "calendrical_calculations" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ca2b6e2f7d75f43e001ded6f25e79b80bded5abbe764cbdf78c25a3051f4b" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "calloop" version = "0.13.0" @@ -1174,6 +1195,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" +dependencies = [ + "libm", +] + [[package]] name = "coreaudio-rs" version = "0.11.3" @@ -1366,6 +1396,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "dlib" version = "0.5.2" @@ -1459,6 +1500,15 @@ dependencies = [ "serde", ] +[[package]] +name = "egui-modal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297fa5697b1048198cc12f5f101312a6fcae42e55a637c861a316d6028797e42" +dependencies = [ + "egui", +] + [[package]] name = "egui-video" version = "0.8.0" @@ -1567,6 +1617,15 @@ dependencies = [ "egui_extras", ] +[[package]] +name = "egui_qr" +version = "0.1.0" +source = "git+https://git.v0l.io/Kieran/egui_qr.git?rev=f9cf52b7eae353fa9e59ed0358151211d48824d1#f9cf52b7eae353fa9e59ed0358151211d48824d1" +dependencies = [ + "egui", + "qrcode", +] + [[package]] name = "egui_tabs" version = "0.2.1" @@ -1620,6 +1679,15 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "email_address" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1019fa28f600f5b581b7a603d515c3f1635da041ca211b5055804788673abfe" +dependencies = [ + "serde", +] + [[package]] name = "emath" version = "0.29.1" @@ -1869,6 +1937,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "fixed_decimal" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0febbeb1118a9ecdee6e4520ead6b54882e843dd0592ad233247dbee84c53db8" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", +] + [[package]] name = "flatbuffers" version = "23.5.26" @@ -2374,6 +2453,399 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "icu" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff5e3018d703f168b00dcefa540a65f1bbc50754ae32f3f5f0e43fe5ee51502" +dependencies = [ + "icu_calendar", + "icu_casemap", + "icu_collator", + "icu_collections", + "icu_datetime", + "icu_decimal", + "icu_experimental", + "icu_list", + "icu_locid", + "icu_locid_transform", + "icu_normalizer", + "icu_plurals", + "icu_properties", + "icu_provider", + "icu_segmenter", + "icu_timezone", +] + +[[package]] +name = "icu_calendar" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7265b2137f9a36f7634a308d91f984574bbdba8cfd95ceffe1c345552275a8ff" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locid", + "icu_locid_transform", + "icu_provider", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e009b7f0151ee6fb28c40b1283594397e0b7183820793e9ace3dcd13db126d0" + +[[package]] +name = "icu_casemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff0c8ae9f8d31b12e27fc385ff9ab1f3cd9b17417c665c49e4ec958c37da75f" +dependencies = [ + "displaydoc", + "icu_casemap_data", + "icu_collections", + "icu_locid", + "icu_properties", + "icu_provider", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_casemap_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d57966d5ab748f74513be4046867f9a20e801e2775d41f91d04a0f560b61f08" + +[[package]] +name = "icu_collator" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d370371887d31d56f361c3eaa15743e54f13bc677059c9191c77e099ed6966b2" +dependencies = [ + "displaydoc", + "icu_collator_data", + "icu_collections", + "icu_locid_transform", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee3f88741364b7d6269cce6827a3e6a8a2cf408a78f766c9224ab479d5e4ae5" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_datetime" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d115efb85e08df3fd77e77f52e7e087545a783fffba8be80bfa2102f306b1780" +dependencies = [ + "displaydoc", + "either", + "fixed_decimal", + "icu_calendar", + "icu_datetime_data", + "icu_decimal", + "icu_locid", + "icu_locid_transform", + "icu_plurals", + "icu_provider", + "icu_timezone", + "smallvec", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_datetime_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ba7e7f7a01269b9afb0a39eff4f8676f693b55f509b3120e43a0350a9f88bea" + +[[package]] +name = "icu_decimal" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8fd98f86ec0448d85e1edf8884e4e318bb2e121bd733ec929a05c0a5e8b0eb" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_decimal_data", + "icu_locid_transform", + "icu_provider", + "writeable", +] + +[[package]] +name = "icu_decimal_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d424c994071c6f5644f999925fc868c85fec82295326e75ad5017bc94b41523" + +[[package]] +name = "icu_experimental" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844ad7b682a165c758065d694bc4d74ac67f176da1c499a04d85d492c0f193b7" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_collections", + "icu_decimal", + "icu_experimental_data", + "icu_locid", + "icu_locid_transform", + "icu_normalizer", + "icu_pattern", + "icu_plurals", + "icu_properties", + "icu_provider", + "litemap", + "num-bigint", + "num-rational", + "num-traits", + "smallvec", + "tinystr", + "writeable", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_experimental_data" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c178b9a34083fca5bd70d61f647575335e9c197d0f30c38e8ccd187babc69d0" + +[[package]] +name = "icu_list" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfeda1d7775b6548edd4e8b7562304a559a91ed56ab56e18961a053f367c365" +dependencies = [ + "displaydoc", + "icu_list_data", + "icu_locid_transform", + "icu_provider", + "regex-automata 0.2.0", + "writeable", +] + +[[package]] +name = "icu_list_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1825170d2c6679cb20dbd96a589d034e49f698aed9a2ef4fafc9a0101ed298f" + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_pattern" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f36aafd098d6717de34e668a8120822275c1fba22b936e757b7de8a2fd7e4" +dependencies = [ + "displaydoc", + "either", + "writeable", + "yoke", + "zerofrom", +] + +[[package]] +name = "icu_plurals" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a70e7c025dbd5c501b0a5c188cd11666a424f0dadcd4f0a95b7dafde3b114" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_locid_transform", + "icu_plurals_data", + "icu_provider", + "zerovec", +] + +[[package]] +name = "icu_plurals_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3e8f775b215d45838814a090a2227247a7431d74e9156407d9c37f6ef0f208" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "icu_segmenter" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a717725612346ffc2d7b42c94b820db6908048f39434504cb130e8b46256b0de" +dependencies = [ + "core_maths", + "displaydoc", + "icu_collections", + "icu_locid", + "icu_provider", + "icu_segmenter_data", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f739ee737260d955e330bc83fdeaaf1631f7fb7ed218761d3c04bb13bb7d79df" + +[[package]] +name = "icu_timezone" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa91ba6a585939a020c787235daa8aee856d9bceebd6355e283c0c310bc6de96" +dependencies = [ + "displaydoc", + "icu_calendar", + "icu_provider", + "icu_timezone_data", + "tinystr", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_timezone_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c588878c508a3e2ace333b3c50296053e6483c6a7541251b546cc59dcd6ced8e" + [[package]] name = "idna" version = "0.5.0" @@ -2660,6 +3132,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -2677,12 +3155,36 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "litrs" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +[[package]] +name = "lnurl-rs" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41eacdd87b675792f7752f3dd0937a00241a504c3956c47f72986490662e1db4" +dependencies = [ + "aes", + "anyhow", + "base64 0.22.1", + "bech32", + "bitcoin", + "cbc", + "email_address", + "serde", + "serde_json", + "url", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -2993,6 +3495,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aad4b767bbed24ac5eb4465bfb83bc1210522eb99d67cf4e547ec2ec7e47786" dependencies = [ + "aes", "async-trait", "base64 0.22.1", "bech32", @@ -3828,6 +4331,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" + [[package]] name = "quick-error" version = "2.0.1" @@ -4029,6 +4538,15 @@ dependencies = [ "regex-syntax 0.6.29", ] +[[package]] +name = "regex-automata" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9368763f5a9b804326f3af749e16f9abf378d227bcdee7634b13d8f17793782" +dependencies = [ + "memchr", +] + [[package]] name = "regex-automata" version = "0.4.8" @@ -4504,6 +5022,12 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -4576,6 +5100,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -4735,6 +5270,16 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -5099,6 +5644,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.11.0" @@ -5936,6 +6493,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +dependencies = [ + "either", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -6015,6 +6587,30 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + [[package]] name = "zap_stream_app" version = "0.1.0" @@ -6027,10 +6623,16 @@ dependencies = [ "directories", "eframe", "egui", + "egui-modal", "egui-video", + "egui_qr", "ehttp 0.5.0", "enostr", + "fixed_decimal", + "icu", + "icu_decimal", "itertools 0.14.0", + "lnurl-rs", "log", "nostr", "nostrdb", @@ -6163,12 +6765,66 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerotrie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb594dd55d87335c5f60177cee24f19457a5ec10a065e0a3014722ad252d0a1f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 9cb6617..7f89de3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,15 +20,21 @@ itertools = "0.14.0" serde = { version = "1.0.214", features = ["derive"] } directories = "5.0.1" egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "d2ea3b4db21eb870a207db19e4cd21c7d1d24836" } +egui_qr = { git = "https://git.v0l.io/Kieran/egui_qr.git", rev = "f9cf52b7eae353fa9e59ed0358151211d48824d1" } # notedeck stuff -nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49"] } +nostr = { version = "0.37.0", default-features = false, features = ["std", "nip49", "nip57"] } nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2111948b078b24a1659d0bd5d8570f370269c99b" } notedeck-chrome = { git = "https://github.com/damus-io/notedeck", rev = "06417ff69e772f24ffd7fb2b025f879463d8c51f", package = "notedeck_chrome" } notedeck = { git = "https://github.com/damus-io/notedeck", rev = "06417ff69e772f24ffd7fb2b025f879463d8c51f", package = "notedeck" } enostr = { git = "https://github.com/damus-io/notedeck", rev = "06417ff69e772f24ffd7fb2b025f879463d8c51f", package = "enostr" } poll-promise = "0.3.0" ehttp = "0.5.0" +egui-modal = "0.5.0" +icu = "1.5.0" +icu_decimal = "1.5.0" +fixed_decimal = "0.5.6" +lnurl-rs = { version = "0.9.0", default-features = false } [target.'cfg(not(target_os = "android"))'.dependencies] eframe = { version = "0.29.1" } diff --git a/src/app.rs b/src/app.rs index ee99c85..54507af 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,8 @@ use enostr::{PoolEvent, RelayEvent, RelayMessage}; use log::{error, info, warn}; use nostrdb::{Filter, Transaction}; use notedeck::AppContext; +use poll_promise::Promise; +use std::collections::HashMap; use std::sync::mpsc; pub struct ZapStreamApp { @@ -20,6 +22,7 @@ pub struct ZapStreamApp { widget: Box, profiles: ProfileLoader, + fetch: HashMap>>, } #[cfg(target_os = "android")] @@ -65,6 +68,7 @@ impl ZapStreamApp { profiles: ProfileLoader::new(), routes_tx: tx, routes_rx: rx, + fetch: HashMap::new(), } } } @@ -132,12 +136,13 @@ impl notedeck::App for ZapStreamApp { let tx = Transaction::new(ctx.ndb).expect("transaction"); // display app ui.vertical(|ui| { - let mut svc = RouteServices { - router: self.routes_tx.clone(), - tx: &tx, - egui: ui.ctx().clone(), + let mut svc = RouteServices::new( + ui.ctx().clone(), + &tx, ctx, - }; + self.routes_tx.clone(), + &mut self.fetch, + ); Header::new().render(ui, &mut svc, &tx); if let Err(e) = self.widget.update(&mut svc) { error!("{}", e); diff --git a/src/link.rs b/src/link.rs index 2b2026e..17eb650 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1,10 +1,11 @@ use crate::note_util::NoteUtil; use bech32::{Hrp, NoChecksum}; -use nostr::prelude::hex; -use nostrdb::{Filter, Note}; +use nostr::prelude::{hex, Coordinate}; +use nostr::{Kind, PublicKey}; +use nostrdb::{Filter, NdbStrVariant, Note}; use std::fmt::{Display, Formatter}; -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, Hash)] pub struct NostrLink { pub hrp: NostrLinkType, pub id: IdOrStr, @@ -13,7 +14,7 @@ pub struct NostrLink { pub relays: Vec, } -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, Hash)] pub enum IdOrStr { Id([u8; 32]), Str(String), @@ -28,7 +29,7 @@ impl Display for IdOrStr { } } -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, Hash)] pub enum NostrLinkType { Note, PublicKey, @@ -58,22 +59,16 @@ impl NostrLink { } pub fn from_note(note: &Note<'_>) -> Self { - if note.kind() >= 30_000 - && note.kind() < 40_000 - && note - .get_tag_value("d") - .and_then(|v| v.variant().str()) - .is_some() - { + if note.kind() >= 30_000 && note.kind() < 40_000 { Self { hrp: NostrLinkType::Coordinate, id: IdOrStr::Str( note.get_tag_value("d") - .unwrap() - .variant() - .str() - .unwrap() - .to_string(), + .map(|t| match t.variant() { + NdbStrVariant::Id(s) => hex::encode(s), + NdbStrVariant::Str(s) => s.to_owned(), + }) + .unwrap_or(String::from("")), ), kind: Some(note.kind()), author: Some(*note.pubkey()), @@ -171,3 +166,18 @@ impl Display for NostrLink { } } } + +impl TryInto for NostrLink { + type Error = (); + + fn try_into(self) -> Result { + match self.hrp { + NostrLinkType::Coordinate => Ok(Coordinate::new( + Kind::from_u16(self.kind.unwrap() as u16), + PublicKey::from_slice(&self.author.unwrap()).unwrap(), + ) + .identifier(self.id.to_string())), + _ => Err(()), + } + } +} diff --git a/src/route/mod.rs b/src/route/mod.rs index 9b93685..3a2e0d9 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -1,13 +1,20 @@ use crate::link::NostrLink; use crate::services::ffmpeg_loader::FfmpegLoader; +use crate::PollOption; +use anyhow::{anyhow, bail}; use egui::load::SizedTexture; -use egui::{Context, Image, TextureHandle}; +use egui::{Context, Id, Image, TextureHandle}; +use ehttp::Response; use enostr::EventClientMessage; +use lnurl::lightning_address::LightningAddress; +use lnurl::pay::PayResponse; +use lnurl::LnUrlResponse; use log::{info, warn}; -use nostr::{Event, EventBuilder, JsonUtil, Kind, Tag}; +use nostr::{serde_json, Event, EventBuilder, JsonUtil, Keys, Kind, SecretKey, Tag}; use nostrdb::{NdbProfile, NoteKey, Transaction}; use notedeck::{AppContext, ImageCache}; use poll_promise::Promise; +use std::collections::HashMap; use std::path::Path; use std::sync::mpsc; use std::task::Poll; @@ -46,13 +53,31 @@ pub enum RouteAction { } pub struct RouteServices<'a, 'ctx> { - pub router: mpsc::Sender, pub egui: Context, pub tx: &'a Transaction, pub ctx: &'a mut AppContext<'ctx>, + + router: mpsc::Sender, + fetch: &'a mut HashMap>>, } -impl<'a> RouteServices<'a, '_> { +impl<'a, 'ctx> RouteServices<'a, 'ctx> { + pub fn new( + egui: Context, + tx: &'a Transaction, + ctx: &'a mut AppContext<'ctx>, + router: mpsc::Sender, + fetch: &'a mut HashMap>>, + ) -> Self { + Self { + egui, + tx, + ctx, + router, + fetch, + } + } + pub fn navigate(&self, route: RouteType) { self.router.send(route).expect("route send failed"); self.egui.request_repaint(); @@ -103,23 +128,76 @@ impl<'a> RouteServices<'a, '_> { Image::from_bytes(name, data) } + /// Create a poll_promise fetch + pub fn fetch(&mut self, url: &str) -> Poll<&ehttp::Result> { + if !self.fetch.contains_key(url) { + let (sender, promise) = Promise::new(); + let request = ehttp::Request::get(url); + let ctx = self.egui.clone(); + ehttp::fetch(request, move |response| { + sender.send(response); + ctx.request_repaint(); + }); + info!("Fetching {}", url); + self.fetch.insert(url.to_string(), promise); + } + self.fetch.get(url).expect("fetch").poll() + } + + pub fn fetch_lnurlp(&mut self, pubkey: &[u8; 32]) -> anyhow::Result> { + let target = self + .profile(pubkey) + .and_then(|p| p.lud16()) + .ok_or(anyhow!("No lightning address found"))?; + + let addr = LightningAddress::new(target)?; + match self.fetch(&addr.lnurlp_url()) { + Poll::Ready(Ok(r)) => { + if r.ok { + let rsp: PayResponse = serde_json::from_slice(&r.bytes)?; + Ok(Poll::Ready(rsp)) + } else { + bail!("Invalid response code {}", r.status); + } + } + Poll::Ready(Err(e)) => Err(anyhow!("{}", e)), + Poll::Pending => Ok(Poll::Pending), + } + } + pub fn write_live_chat_msg(&self, link: &NostrLink, msg: &str) -> Option { if msg.is_empty() { return None; } - if let Some(acc) = self.ctx.accounts.get_selected_account() { - if let Some(key) = &acc.secret_key { - let nostr_key = - nostr::Keys::new(nostr::SecretKey::from_slice(key.as_secret_bytes()).unwrap()); - return EventBuilder::new(Kind::LiveEventMessage, msg) - .tag(Tag::parse(link.to_tag()).unwrap()) - .sign_with_keys(&nostr_key) - .ok(); - } + if let Some(key) = self.current_account_keys() { + EventBuilder::new(Kind::LiveEventMessage, msg) + .tag(Tag::parse(link.to_tag()).unwrap()) + .sign_with_keys(&key) + .ok() + } else { + None } - None + } + + pub fn current_account_keys(&self) -> Option { + self.ctx + .accounts + .get_selected_account() + .and_then(|acc| acc.secret_key.as_ref().map(|k| Keys::new(k.clone()))) + } + + /// Simple wrapper around egui temp data + pub fn get(&self, k: &str) -> Option { + let id = Id::new(k); + self.egui.data(|d| d.get_temp(id)) + } + + /// Simple wrapper around egui temp data + pub fn set(&mut self, k: &str, v: T) { + self.egui.data_mut(|d| d.insert_temp(Id::new(k), v)); } } + const BLACK_PIXEL: [u8; 4] = [0, 0, 0, 0]; pub fn image_from_cache<'a>(img_cache: &mut ImageCache, ctx: &Context, url: &str) -> Image<'a> { if let Some(promise) = img_cache.map().get(url) { diff --git a/src/theme.rs b/src/theme.rs index f6ef8c6..571a533 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,10 +1,12 @@ use egui::{Color32, Margin}; pub const FONT_SIZE: f32 = 13.0; +pub const FONT_SIZE_LG: f32 = FONT_SIZE * 1.5; pub const ROUNDING_DEFAULT: f32 = 12.0; pub const MARGIN_DEFAULT: Margin = Margin::symmetric(12., 6.); pub const PRIMARY: Color32 = Color32::from_rgb(248, 56, 217); pub const NEUTRAL_500: Color32 = Color32::from_rgb(115, 115, 115); +pub const NEUTRAL_700: Color32 = Color32::from_rgb(64, 64, 64); pub const NEUTRAL_800: Color32 = Color32::from_rgb(38, 38, 38); pub const NEUTRAL_900: Color32 = Color32::from_rgb(23, 23, 23); pub const ZAP: Color32 = Color32::from_rgb(255, 141, 43); diff --git a/src/widgets/avatar.rs b/src/widgets/avatar.rs index 9be670d..f6d47a3 100644 --- a/src/widgets/avatar.rs +++ b/src/widgets/avatar.rs @@ -50,7 +50,7 @@ impl Avatar { response } - pub fn render(&self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response { + pub fn render(self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response { let size_v = self.size.unwrap_or(40.); let size = Vec2::new(size_v, size_v); if !ui.is_visible() { diff --git a/src/widgets/button.rs b/src/widgets/button.rs index c089edf..47c1374 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,13 +1,35 @@ -use crate::theme::{NEUTRAL_800, ROUNDING_DEFAULT}; -use egui::{Color32, CursorIcon, Frame, Margin, Response, Sense, Ui}; +use crate::theme::{MARGIN_DEFAULT, NEUTRAL_800, ROUNDING_DEFAULT}; +use egui::{Color32, CursorIcon, Frame, Response, Sense, Ui, WidgetText}; pub struct Button { color: Color32, + disabled: bool, } impl Button { pub fn new() -> Self { - Self { color: NEUTRAL_800 } + Self { + color: NEUTRAL_800, + disabled: false, + } + } + + pub fn with_color(mut self, color: impl Into) -> Self { + self.color = color.into(); + self + } + + pub fn disabled(mut self, v: bool) -> Self { + self.disabled = v; + self + } + + pub fn simple(ui: &mut Ui, content: &str) -> Response { + Button::new().show(ui, |ui| ui.label(content)) + } + + pub fn text(self, ui: &mut Ui, text: impl Into) -> Response { + self.show(ui, |ui| ui.label(text)) } pub fn show(self, ui: &mut Ui, add_contents: F) -> Response @@ -15,15 +37,20 @@ impl Button { F: FnOnce(&mut Ui) -> Response, { let r = Frame::none() - .inner_margin(Margin::symmetric(12., 8.)) + .inner_margin(MARGIN_DEFAULT) .fill(self.color) .rounding(ROUNDING_DEFAULT) + .multiply_with_opacity(if self.disabled { 0.5 } else { 1.0 }) .show(ui, add_contents); let id = r.response.id; ui.interact( r.response - .on_hover_and_drag_cursor(CursorIcon::PointingHand) + .on_hover_and_drag_cursor(if self.disabled { + CursorIcon::NotAllowed + } else { + CursorIcon::PointingHand + }) .rect, id, Sense::click(), diff --git a/src/widgets/chat_zap.rs b/src/widgets/chat_zap.rs index d5f3589..74e900d 100644 --- a/src/widgets/chat_zap.rs +++ b/src/widgets/chat_zap.rs @@ -1,6 +1,6 @@ use crate::theme::{MARGIN_DEFAULT, ROUNDING_DEFAULT, ZAP}; use crate::widgets::Avatar; -use crate::zap::Zap; +use crate::zap::{format_sats, Zap}; use eframe::emath::Align; use eframe::epaint::text::{LayoutJob, TextFormat, TextWrapMode}; use eframe::epaint::Color32; @@ -43,7 +43,7 @@ impl<'a> ChatZap<'a> { job.append("zapped", 5.0, format.clone()); format.color = ZAP; job.append( - (self.zap.amount / 1000).to_string().as_str(), + &format_sats((self.zap.amount / 1000) as f32), 5.0, format.clone(), ); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 8ac8ca0..460d561 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -13,6 +13,7 @@ mod stream_title; mod text_input; mod username; mod write_chat; +mod zap; use crate::note_ref::NoteRef; use crate::route::RouteServices; diff --git a/src/widgets/stream_title.rs b/src/widgets/stream_title.rs index 3a01476..fe475fd 100644 --- a/src/widgets/stream_title.rs +++ b/src/widgets/stream_title.rs @@ -1,8 +1,10 @@ use crate::note_util::NoteUtil; use crate::route::RouteServices; use crate::stream_info::StreamInfo; +use crate::theme::MARGIN_DEFAULT; +use crate::widgets::zap::ZapButton; use crate::widgets::Profile; -use egui::{Color32, Frame, Label, Margin, Response, RichText, TextWrapMode, Ui}; +use egui::{Color32, Frame, Label, Response, RichText, TextWrapMode, Ui}; use nostrdb::Note; pub struct StreamTitle<'a> { @@ -15,17 +17,19 @@ impl<'a> StreamTitle<'a> { } pub fn render(&mut self, ui: &mut Ui, services: &mut RouteServices<'_, '_>) -> Response { Frame::none() - .outer_margin(Margin::symmetric(12., 8.)) + .outer_margin(MARGIN_DEFAULT) .show(ui, |ui| { - ui.style_mut().spacing.item_spacing.y = 8.; let title = RichText::new(self.event.title().unwrap_or("Untitled")) .size(20.) .color(Color32::WHITE); - ui.add(Label::new(title.strong()).wrap_mode(TextWrapMode::Truncate)); + ui.add(Label::new(title.strong()).wrap_mode(TextWrapMode::Wrap)); - Profile::new(self.event.host()) - .size(32.) - .render(ui, services); + ui.horizontal(|ui| { + Profile::new(self.event.host()) + .size(32.) + .render(ui, services); + ZapButton::event(self.event).render(ui, services); + }); if let Some(summary) = self .event @@ -34,7 +38,7 @@ impl<'a> StreamTitle<'a> { { if !summary.is_empty() { let summary = RichText::new(summary).color(Color32::WHITE); - ui.add(Label::new(summary).wrap_mode(TextWrapMode::Truncate)); + ui.add(Label::new(summary).wrap_mode(TextWrapMode::Wrap)); } } }) diff --git a/src/widgets/text_input.rs b/src/widgets/text_input.rs index eada647..91a9278 100644 --- a/src/widgets/text_input.rs +++ b/src/widgets/text_input.rs @@ -37,7 +37,7 @@ impl Widget for NativeTextInput<'_> { if let Some(hint_text) = self.hint_text { editor = editor.hint_text(egui::RichText::new(hint_text).color(NEUTRAL_500)); } - + if self.frame { Frame::none() .inner_margin(MARGIN_DEFAULT) diff --git a/src/widgets/zap.rs b/src/widgets/zap.rs new file mode 100644 index 0000000..1b990b4 --- /dev/null +++ b/src/widgets/zap.rs @@ -0,0 +1,286 @@ +use crate::link::NostrLink; +use crate::route::RouteServices; +use crate::stream_info::StreamInfo; +use crate::theme::{ + FONT_SIZE_LG, MARGIN_DEFAULT, NEUTRAL_700, NEUTRAL_800, NEUTRAL_900, PRIMARY, ROUNDING_DEFAULT, +}; +use crate::widgets::{Button, NativeTextInput}; +use crate::zap::format_sats; +use anyhow::{anyhow, bail}; +use egui::{vec2, Frame, Grid, Response, RichText, Stroke, Ui, Widget}; +use egui_modal::Modal; +use egui_qr::QrCodeWidget; +use enostr::PoolRelay; +use itertools::Itertools; +use lnurl::pay::{LnURLPayInvoice, PayResponse}; +use nostr::prelude::{hex, ZapRequestData}; +use nostr::{serde_json, EventBuilder, JsonUtil, Kind, PublicKey, Tag, Url}; +use nostrdb::Note; +use std::fmt::{Display, Formatter}; +use std::task::Poll; + +pub enum ZapTarget<'a> { + PublicKey { pubkey: [u8; 32] }, + Event { event: &'a Note<'a> }, +} + +impl Display for ZapTarget<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ZapTarget::PublicKey { pubkey } => write!(f, "{}", hex::encode(pubkey)), + ZapTarget::Event { event } => write!(f, "{}", hex::encode(event.id())), + } + } +} + +#[derive(Clone)] +pub enum ZapState { + NotStarted, + Ready { service: PayResponse }, + FetchingInvoice { callback: String }, + Invoice { invoice: LnURLPayInvoice }, + Error(String), +} + +pub struct ZapButton<'a> { + target: ZapTarget<'a>, +} + +impl<'a> ZapButton<'a> { + pub fn pubkey(pubkey: [u8; 32]) -> Self { + Self { + target: ZapTarget::PublicKey { pubkey }, + } + } + + pub fn event(event: &'a Note<'a>) -> Self { + Self { + target: ZapTarget::Event { event }, + } + } + + pub fn render(self, ui: &mut Ui, services: &mut RouteServices) -> Response { + // TODO: fix id + let modal = Modal::new(ui.ctx(), format!("zapper-{}", 0)).with_close_on_outside_click(true); + + let resp = Button::new().show(ui, |ui| ui.label("ZAP")); + if resp.clicked() { + modal.open(); + } + ui.visuals_mut().window_rounding = ROUNDING_DEFAULT.into(); + ui.visuals_mut().window_stroke = Stroke::NONE; + ui.visuals_mut().window_fill = NEUTRAL_900; + + modal.show(|ui| { + Frame::none().inner_margin(MARGIN_DEFAULT).show(ui, |ui| { + ui.spacing_mut().item_spacing = vec2(8.0, 8.0); + + let pubkey = match &self.target { + ZapTarget::PublicKey { pubkey } => pubkey, + ZapTarget::Event { event } => event.host(), + }; + + // zapping state machine + let zap_state = services.get("zap_state").unwrap_or(ZapState::NotStarted); + match &zap_state { + ZapState::NotStarted => match services.fetch_lnurlp(pubkey) { + Ok(Poll::Ready(r)) => { + services.set("zap_state", ZapState::Ready { service: r }) + } + Err(e) => services.set("zap_state", ZapState::Error(e.to_string())), + _ => {} + }, + ZapState::FetchingInvoice { callback } => { + match self.zap_get_invoice(callback, services) { + Ok(Poll::Ready(s)) => { + services.set("zap_state", ZapState::Invoice { invoice: s }) + } + Err(e) => services.set("zap_state", ZapState::Error(e.to_string())), + _ => {} + } + } + _ => {} + } + + // when ready state, show zap button + match &zap_state { + ZapState::Ready { service } => { + self.render_input(ui, services, pubkey, service); + } + ZapState::Invoice { invoice } => { + if let Ok(q) = QrCodeWidget::from_data(invoice.pr.as_bytes()) { + ui.add_sized(vec2(256., 256.), q); + + let rt = RichText::new(&invoice.pr).code(); + ui.label(rt); + } + } + ZapState::Error(e) => { + ui.label(e); + } + _ => {} + } + }) + }); + + if modal.was_outside_clicked() { + services.set("zap_state", ZapState::NotStarted) + } + resp + } + + fn zap_get_invoice( + &self, + callback: &str, + services: &mut RouteServices, + ) -> anyhow::Result> { + match services.fetch(callback) { + Poll::Ready(Ok(r)) => { + if r.ok { + let inv: LnURLPayInvoice = serde_json::from_slice(&r.bytes)?; + Ok(Poll::Ready(inv)) + } else { + bail!("Invalid response code {}", r.status); + } + } + Poll::Ready(Err(e)) => Err(anyhow!("{}", e)), + Poll::Pending => Ok(Poll::Pending), + } + } + + fn render_input( + &self, + ui: &mut Ui, + services: &mut RouteServices, + pubkey: &[u8; 32], + service: &PayResponse, + ) { + let target_name = match self.target { + ZapTarget::PublicKey { pubkey } => services.profile(&pubkey).and_then(|p| p.name()), + ZapTarget::Event { event } => { + let host = event.host(); + services.profile(host).and_then(|p| p.name()) + } + }; + let fallback_name = self.target.to_string(); + let target_name = target_name.unwrap_or(&fallback_name); + ui.label(RichText::new(format!("Zap {}", target_name)).size(FONT_SIZE_LG)); + + ui.label("Zap amount in sats"); + + // amount buttons + const SATS_AMOUNTS: &[u64] = &[ + 21, 69, 121, 420, 1_000, 2_100, 4_200, 10_000, 21_000, 42_000, 69_000, 100_000, + 210_000, 500_000, 1_000_000, + ]; + const COLS: u32 = 5; + let selected_amount = services.get("zap_amount").unwrap_or(0); + Grid::new("zap_amounts_grid").show(ui, |ui| { + let mut ctr = 0; + for x in SATS_AMOUNTS { + if Button::new() + .with_color(if selected_amount == *x { + NEUTRAL_700 + } else { + NEUTRAL_800 + }) + .text(ui, &format_sats(*x as f32)) + .clicked() + { + services.set("zap_amount", *x); + } + ctr += 1; + if ctr % COLS == 0 { + ui.end_row(); + } + } + }); + + // comment section + let mut zap_comment = services.get("zap_comment").unwrap_or(String::new()); + ui.label(format!("Your comment for {}", target_name)); + let old_len = zap_comment.len(); + NativeTextInput::new(&mut zap_comment) + .with_frame(true) + .ui(ui); + + if Button::new().with_color(PRIMARY).text(ui, "Zap!").clicked() { + // on-click setup callback URL and transition state + match self.zap_callback( + services, + pubkey, + &zap_comment, + selected_amount * 1_000, + &service, + ) { + Ok(callback) => services.set( + "zap_state", + ZapState::FetchingInvoice { + callback: callback.to_string(), + }, + ), + Err(e) => services.set("zap_state", ZapState::Error(e.to_string())), + } + } + + if zap_comment.len() != old_len { + services.set("zap_comment", zap_comment); + } + } + + fn zap_callback( + &self, + services: &mut RouteServices, + pubkey: &[u8; 32], + zap_comment: &str, + amount: u64, + lnurlp: &PayResponse, + ) -> anyhow::Result { + let relays: Vec = services + .ctx + .pool + .relays + .iter() + .filter_map(|r| match r { + PoolRelay::Websocket(w) => Url::parse(&w.relay.url).ok(), + _ => None, + }) + .collect(); + if relays.is_empty() { + bail!("No relays found"); + } + let mut req = ZapRequestData::new(PublicKey::from_slice(pubkey)?, relays) + .message(zap_comment) + .amount(amount); + match &self.target { + ZapTarget::Event { event } => { + req.event_coordinate = Some( + NostrLink::from_note(event) + .try_into() + .map_err(|e| anyhow!("{:?}", e))?, + ) + } + _ => {} + }; + + let req_tags: Vec = req.into(); + let keys = if let Some(k) = services.current_account_keys() { + k + } else { + bail!("Not logged in") + }; + + let req_ev = EventBuilder::new(Kind::ZapRequest, zap_comment) + .tags(req_tags) + .sign_with_keys(&keys)?; + + let mut url = Url::parse(&lnurlp.callback)?; + url.query_pairs_mut() + .append_pair("amount", amount.to_string().as_str()); + if lnurlp.nostr_pubkey.is_some() { + url.query_pairs_mut() + .append_pair("nostr", req_ev.as_json().as_str()); + } + Ok(url) + } +} diff --git a/src/zap.rs b/src/zap.rs index ce40953..63eb66f 100644 --- a/src/zap.rs +++ b/src/zap.rs @@ -1,5 +1,8 @@ use crate::note_util::NoteUtil; use anyhow::{anyhow, bail, Result}; +use fixed_decimal::FixedDecimal; +use icu::decimal::FixedDecimalFormatter; +use icu::locid::Locale; use nostr::{Event, JsonUtil, Kind, TagStandard}; use nostrdb::Note; @@ -54,3 +57,17 @@ impl<'a> Zap<'a> { }) } } + +pub fn format_sats(n: f32) -> String { + let (div_n, suffix) = if n >= 1_000. && n < 1_000_000. { + (1_000., "K") + } else if n >= 1_000_000. { + (1_000_000., "M") + } else { + (1., "") + }; + + let fmt = FixedDecimalFormatter::try_new(&Locale::UND.into(), Default::default()).expect("icu"); + let d: FixedDecimal = (n / div_n).to_string().parse().expect("fixed decimal"); + format!("{}{}", fmt.format_to_string(&d), suffix) +}