diff options
| author | Adam Soltys <[email protected]> | 2026-09-18 23:12:19 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-18 23:12:19 -0700 |
| commit | e07023af899f6682e088aee9ff3329b16b8f467d (patch) | |
| tree | 7b3f6926d476ebb3c41ad2f325a1029bf58d82aa /src | |
| parent | 703ee6e1281e72ec00f65768d7de7a015c242abb (diff) | |
| download | coinosv3-e07023af899f6682e088aee9ff3329b16b8f467d.tar.gz coinosv3-e07023af899f6682e088aee9ff3329b16b8f467d.zip | |
Zaps: the sound, the sender, and one action bar for every post.
Tap feedback: a bolt strike, the amount popping off the button, the post
trembling, and the zap.mp3 clip. The clip is 62KB of base64 and messages.js
is boot code, so it is its own chunk, fetched once a zappable post is on
screen and played inside the tap once loaded; the standalone build inlines
it. Ark zap receipts are signed by the wallet key, which has no profile: they
now carry the login identity as the sender (NIP-57's P tag), readers prefer
it, and our own older receipts read as us. The action bar is the same six
slots on every post: the zap button and the who-reacted chevron are always
there (inert until they have something to do), so the first zap no longer
reflows the row, and you can zap your own posts and messages. Community
rows centre their unread dot off the edge. Also in here: reactions with
custom emoji, reply-composer focus under the tapped post, profile fetch
retry, and Lightning zap failures that say so.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/assets/zap.mp3.base64.txt | 1 | ||||
| -rw-r--r-- | src/features/ark.js | 15 | ||||
| -rw-r--r-- | src/features/messages.js | 234 | ||||
| -rw-r--r-- | src/features/zaps.js | 9 | ||||
| -rw-r--r-- | src/i18n.js | 2 | ||||
| -rw-r--r-- | src/style.css | 40 | ||||
| -rw-r--r-- | src/zap-animation.js | 83 | ||||
| -rw-r--r-- | src/zap-sound.js | 59 |
8 files changed, 341 insertions, 102 deletions
diff --git a/src/assets/zap.mp3.base64.txt b/src/assets/zap.mp3.base64.txt new file mode 100644 index 0000000..10ae0a5 --- /dev/null +++ b/src/assets/zap.mp3.base64.txt @@ -0,0 +1 @@ +SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU5LjI3LjEwMAAAAAAAAAAAAAAA//u0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAABQAAC2QAAGCQwPEhIWGRwfHyIlKSwsLzI1ODg8P0JFRUhLT1JSVVhbXl5hZWhra25xdHh4e36BhISHi46RkZSXmp6eoaSnqqqtsLS3t7q9wMPDx8rN0NDT1trd3eDj5unp7fDz9vb5/P8AAAAATGF2YzU5LjM3AAAAAAAAAAAAAAAAJAVAAAAAAAAAtkB3gM/SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//u0ZAAABLpo1gUlAAJCxTp4oYwAFnFdf/mXgAGWme47MFAAAAGHEYXBMNisnJEBAKBQKBQgYe+ESpLFBQUFDEREdyxcXFxc+/IFAaA0BoDQPA/FxcXFxcXPuX/4RBcXFz3v/+CBQUFE/5d4FAbg3BuLv1IdgKAKB4ZLvoLi4u78VKU4uDcG4fnwLi4uLn3CCguLnvoiI78IiIjvfCJ6IhAoKIiO+iVwguLvcECiOKChlfwguLj8BGBmY/kAAAAd3d3rhwAAEFXd3REREREd3d3d3RERC//3c9Hc9ERP//yd3c/3AAAMfwQBDEYP4nBAMdQIAg4EAQg+Hv/4gBB3mZfbt4qodkXSRMkkAhAgM9aaRUNxF/EcVwrmR7afAzYyidDuytW0tzBbEqJ7AqROQUKoTahOpznRKMnULx3q7gi1LhRKpYZpaOsKWNKpnkVgbL5j6pE3pS4cWqHpsxJEiSVj3vf/EDHpe8sC8l7Q6zb3a9t0zXeIj2PiuayalpbcDeZ9TVtvePakbLyz2ry+Yvp/P80xq3pXc/vu2t7gmg0LwkEQ84VzQOf/MJjWf/j6mqN3eJt4ZFYwEiI3BwAB/Av20xFaS2X1ZZFJDAQKS3lJc1OkMlEVzbfVnShu6KdRASWpbiaAIHB3K9xYgeD+tn1aLVoYggLGMcUExBBcUOqcWaTAUVfXLpcZcWFRq1Pdr5Y5v4miBKoEhGMgDxXthDmX3dooxLY3MxF4Gx3YlKK41qF7KpJ/n1qoz1fanQuJ//u0ZBQDBh1xWidhgAA/gKsO54gAGz3DZowljcjrguvwcwQYqxavY6sUZi9C9qx1DYWPOJU3m9jQsm45G64mJVw5nh2j4+YHoTxqZD7io6e3AqUh0Q0ft3NjpUXbF8uj2TiSVDgqIxweMidimBCMx/Tn68toZETS8WBLWKVqhZVTVGbq7eTz1LhaZoeRMPIZjWzRfEfVx6sef5oRsY5yXUJ1ahRx0sx7zzLNFkTvJK/tm5ayud1v9dGtsrq1O5D8AYoKMJlQAABzj0/bxRbh7iKYRHMjdD+kUTyz9XBVwGDV21xY8zuqqFyTQVcJQVd+o9EThERpXErjGCqzrctIm6W0kEmrAyBJ3VLRU7BngS1aQnSoczhYF6w+cExOGkiRx0bRawQoxUyabK0iRKYRlUz54MFQ62JxEjC5IG0UkaTQnNHQwsLyQdBwKx2iHUyMUpaQYh2gscBOfrkorMRWExk8+JXmR4gCEciXHEsjKAli4GgdrBQOrJgeLPKpaTtCAYMh+Rmy/hucAg2cLB9I5X0TqHSQSD8llQ1Ka0zMNTUOFuOujqfr1CGrOCwvPKq1Risgf8eRLLMfup5gQ1ylZi967CNhYfvMNHCF0Or8veASWqXWRL0ax2QFtiEAwAGATxfARhp4o9tGMJanUI+4cAHSju8XB9qZBzoYsFemcpQFlv8uHv+SFRyLdONf6gz/R6uzIXZM9DFAA+mHC5yhKcy9mFKUq/ThdB0S8bL36YarcyF0K75wI9bvL9jdLIZJ//u0ZBYDBhhvWtsMTXJNDBtPZCdOF5nBbcyw18kjguy8N4AYQcwhMvUqlWJH4tcYQ2T5wvHB71lq0zRr31RohmJRuPEadM4STR5csTclqeKCSRXl6EfH52TcTpcJqVxKbFx9eJSOB10cS0vOlCE0Rj40Zq/biUrlg4BU8Yvi91Uou0jiuaURLCm0DBOTllt92giajJI3Z6U017EeoTb78EipplWblp741vy9w/XXvxl13qZ3a2zBJtfaGJRJpCEDLIAAFc6TBp+hk8TsqWpDMkXsZAxGR/avz/O/639m/atFo/X3v///b0/b////9+1f/0LsyGIIkeINqVHixw8Y4XGhY0Ve71+MVqmGRSEg9UWTlgXKAyAGHCJ0cIskg1Mu6l07TXWgQ5Gneid2PW3xuQ9bpKT5Zcs1flEEqWPUTqxFA8tZsZa68kWL2SJQ1JGLIqDmQ1RZWHh448vabUxdRpBqVVt/svscGR+KTLsIRw35WKTPQIa48w8fWHJtX3nCiyeFM2g5Q0h/Ge+vc+T/bn9NtkuxppYUoS5la71qmrZMRRJEIgpBk6tMrNG0UhKUJTy7d0n2MouVWTmWqsnlmvSnZkQkFiDEBAAwAABDcCSoyRFhvGIVzxRkCbyrLHU7d95adeTOPA63hABB8+Mm6FZw+Mctq22Pf0f//7wiVA63utSudBZVGLB1SeKcej3U8iiiK4pxIMHc/BsqkWgtncF53WZHEYxATY6F5ZztV4aWh1AywdYU5SKzLGjQpqXv//u0ZBkDBm1xWiMMTPBCJes/MwJQGWnDaow9iEj5AywwwzAIOyjktFIvnxu4SXlUQ8qYqKUx+lDyx4RMN3oAbnJuUzwciYWRnApDktko1IKSXgniq6FLpbVkwQFx6XyC4vFRsI6gtuB/g0BIfISEwM4NryVLIykQJBoaNlTYxCZ2RNFkUE6dJUVHES4fFyFIRBZpGzUpHWxSSIEy2K9YhSEZphdRY7q6NSK6eIbsgbT6XowlZ0/cCwhSTZqkr1ZwuxsXCApgAAEi8gEkXDrWnjdUwwzUQCaU6pfpr9K/T26panR/ZHu9slklO7ZmV0XCUotUxYARQ/9Lv+9Tlt//Z/+q73PwtNoSlTLYYACrQh5XqW6fJmk/CaO1OB2H4IE9haHZMWFLoj6F6E7W1fqyeMutONkl6NeeLB2PV7ZVLxdw708P1StKtBssHANRe0cnD/HSPGlL6CVCUcJ7D4zYqDyqPxpIQyLiVKrqLD5GPLjaU/NFaaFgfGS4IZyT4hKcFJ4vaiQbnyd4kY+6XfXClthC5aXDNewePEhwrK2+ZKzrozfPqNWuqXr1cZMhgxphehb3Yiq65FMmk+43Vd19tbsXUn+P+T5Rf734/8dtXRlCAAAQdF7UTApSIg1AOcYcpzgpmkFPlsrEQtlTpVZAs0q5JUFWp6wmyaGWPpsdZR7Ff+juQn6Upv/bQqppV3VlMygAAD0qCi1SA17Exko0EJbtiLDWMBgkuH9huFvcHRGqQk5XCZYWGULy6SlvlZmz//u0ZBoBBgVsW/MMS/I7xIrkJCJeHMnBZowx70j8BOs5gwBoUSI01WnsvUfMulSkJUqXFcB8wLi8ZsqFJf+IbRxnBowoL4rbQiIg6U5FXB5kF9MNDsUZ0vsW2dECZLNYifhI3QoFRIyxBJxUjcYIBM2ZIpo4JHF5TGqcdIzBAh18bNGEtXmTnRIFxUknFVcjVbqSAlXvNICBmEPZ6/deE9q3NrpbHKRTn1IZkjofn8s5wZIBAAsXAlAqmBgGkMZWFySS8zc5BL8n6fnrVu30x2GvHMAvqpezGikwmDr7RRJJLa69/6V//T9C871SxtRIAagRRU1L0MFT1RSWej7I3Jii5XBX/DzQtEPyqyksrYhWIZvdh34iaT0MoQLBISAUOFkRbEIAorLvvFKjE6yqw7DnOZzHA3HyarcbhcY5czyS79slLihBM3jGyE1JmYzcbiWPk6kMTpjrJv7O6dNsp1CwIcX1RlyvKnlKdBB0saS4Z3r6AyIQcjY704xzGliolHj6bJFQX1GMSnXb+sqEM4uUj1WL0rgu6NZpP95hJA5G5OMNVyuISU6Q8Czyjikl+Rbca0iYxEng53BfvZbOnt17wmuJCpIwu4V4Lt7Forb0mJJAAAiAACAACicWzMZghUBpkgeYibipxFQjJCwWcTvTcKMUZWdAPy54MYdZ/seDAVP4Jjub0SB2xWx/6mbHM1V2TGZzIwQAACfS+iRywDdlNy4wKCF3UNH4bsoklQ7cFPlSy2KTA6F05ds44jSJ//u0ZBcDNlRv23NMTHJHBbq0YeMMGRnBbcyxMckJF+qhgRn6rnfPdRLitktJh8cKbvJ3FlEI2MiEHqsfoSycFskitSOogmA4LTU2KpZYGzyUwgZWqioYDSc3JBReXjzrUQkaeSajSwFHG0ZIXFAhHxmJY5TSM6fD4eSMidErTz8jNrKsOmKG2dY6pyC8pjXUTWVWgiodgXOwYgg91MTw2ZZfsQ2Day6qsWVEdY9OaexRI0N+J5FizEJyUYbUSRBo1MuVgAFElTCPottNxGjtEUVxkrauS7hLO9rU42pmJq8/+aR3yX/4cOz/KJmx3MoefH+EHeITiFBBwd7HjQiP/d/+/2+r//72hjdnUxE+iB4YCkEDa+AoA47AE0BUNmic5chcq6WxUDpM5maVKfIZQLldVHBXPeUHyqE3hTrDU7ZqSEKKE5aVONNwlJcXqQGQnr7n7ZtGbqB3VFsfgPiVLgXoRmNa0muEMitAVWpXk1yxKGUZpAxtkCFNCjm+TjWAQBlUlwKo+ICIqQCYZR4UQjANLYdV2QmiURFZAIR2XXbVctNhqxoiFXOz2kEVRm0BDDp+RCauf2NyZe3GNKQxu1E0l8aYZYqk2eoteXGE3OkYUEASe5oACLK4+7et3lcEUcpz1lQ73ahUHV7Km/0Uclw6/9a5UaS1rWYhes7AVEtAluDVWGsdd6/7fR9gsr1i/s6ljfdrBMABQq6ETmRM5bBPoU0C+igyt4OKwWXG4fpLocL7xkViVa7jx0ThKPEK//u0ZBaBBoBwWtsMY3I+A4sOYCN6GenDa2y9gYkUDuy5gIoYWTWxyqJR8ZOqnT582cQzBwlh0nihE9o/Eo8PS8bjueUKA7ryGdlUxKy4eh6Ix7ELQKCQvSh8hMLSySGywTljV+l1o1Xl15KlVBxHRSTiQuLZeaHc9HNEJ+vlHdPIgIK7rzy8BViajLVLkltCcaKkB0ucHyh1SxRePq+upZdiFAPmtUarCkYi6CKv/dxHV+jMbC19ujV51K6stG5U/Yhtdh9zWFBqVCQAAAAAEZHJr07ThQIYlyo6uW9CLXeWJzOQEkYfo2e0JKFpoCchEBg+pTaIEU5ayUfR39Pf+zn//6Pp9UrjRKAWEXGCbKFgQxbBckA2rBoIkdhnEEUMgq1mzh6IbDh4jRHlUSnDxo8PlzDSJZdtQRSE/Tjw/X3A+YLRsCYZkyhbDkniGJY+lM5EgzGAowc9EMfUhPRNG4giECpVH4XULAdplKVYVys6Io+l0TCnmxHzJvDQqohAJz9DhfUe0jKh+0J4ctD2uq061gaI0zZwnWOrkKBtBcx55tgr6lPkZVW+tfNW5jYUenpkUnrsM3SxVPoDurLrLrdNSewtWxOuussu2hX0a1xrHmoVmmXg3MgAQASqhgVFiQTpgM7kDKEP14O3BfU52lxu/nV7lYgbTG27td93BoDKgtxAIeBHIEAIDhGlpwM9X+37f/t/+yn6VbvNmklgCEhFEiQX2QxHTuinMXcQiFmw0eR/FIcUc6CAMpf2NDjh//u0ZBMBBeVuW9sPSsJSY7tPYYNmGBXBbIwxNckCB629h5ighMyQZ2dTg2qI0ITi4ujOqAmwTrxkSoyPHogTIWExdkVhttYCS5G0aIzZ0nFiIeHYgQQvQxZKFAdB4FFGUbGI4zMNxEVihKbJORnpFkBEhPSN2q4isgO8QIyBdPKk2uWnOcumI3MX75Als4Iak3aUDupi3KZ/bfrCjfhksx/3YVrO7JWmKSbq0E2opKqyhSTmkdXU0giG28TDs5mygIE1JrpXOH2LijIgFBeSOq20DVLrQkFkqhR7Q/IbxwVAagaWnpfPDwqCIjF8/nz2QAMqjHBsEIUDAwYZJPCxnFW9hYRa/21r/s/97etu7ytgifE9Ua3YLkTjAYaWmLBaSnbI44uSLuQ/UguONTS6IUGU5NZTTjjTjL8ap9MhqjVyTzD5ItPaCOmLRWXPGBYKrEBwoKriAkLBS1WiLBVEGyJ5CZXKIoHSsWmjQtHcGK5Xq0KNM0vshvJ19BrlDVpbNgogWQqvHhCOS4+zDx4cFQPd5mMaQQZpVjh61e8RKpek/N5EGVBXHwQqVeNhOn0sZgSMb9lMkpsqu5VlDG0vWyttm4RS6BmfUxp8m4AxxUJDwwqyAADIteCgUR9QMNIcuSHYZjCE4TztPnQnzRTPAM484suTUk0I/POEtLKvv8QGH1HMhRQTDH//tr8t9CpbZttoIDFMBoysaszQwMt94YZvPNHgxuS1p1rr/unIG4pzFm1hmcZc2rEhKprv4NnK//u0ZBiBBpRxWiMPZHBIZxtPYCN+F629b8wxMckTF2x5gI34UyH0G7XKx4mlbmbavL852OVTplshPjecF0fjK0znijS/n8ZaWSSVKJnUBys5b3qjnc2w91H2sfjgvCeycCLL46tnzxaJ53xN0x1DPw9XL21pIua2F6EqqpZYCJMoWv64HxYTQHTHH5yVVOnKxJGrJ3D0+Xk+rI/iq3b5K6atXqUaM10zVZarSj4jBY8fN9FEcdeWWFLaKh1b2FKx65dhm7lHiIl4VDEmEACV31dE1GcKNjQRIskZM8Epdh626qllUWpheZbL6/vlX88REef//3DAAQQCCmJo+Pi1U/jSkIYG1EChC7///r/UkM7syIJKAaxt+AYloVDhZAkEa8HaL/NeTdXE6jSF33ngel2Gdl4mOHhDeKrq1rmVMS5KhJUmcSzWWU+WhWr0h080dU9Sv1WaratqD5kuwsuFIeDc7bGJCcq/qoQE6U6NCITuWAqyfCwiTVZjAkRzJypKjICWRLwobWiq95LEfbRz0nL6JEa+yZLIU2JIZuX5bLQRWXkeilpgf+xlsJppKrGcp9YjORpiXki2metkJyqasIS3wjXq5qV5125jHN0gSAAAAAlATF63ofQQqXxZ7ULVq5ZXs7/7xEYDHlLyfzwcsoBmpu9N7UuY1IVTZsnZmtAQVBUFYd4NBUFTv///3ffrihfTJAeVG2TtKeRpMCPGTAuW1AUDiK+HlYRE2nAc4+DBEIF12iNQlwSgofPoKE4U//u0ZBgDBoZwWqMpY8JDg4r/YCJqGSnFbcww18DfDmu08Il4GjwyOu1l5hIV8odEcj3OXGAsP0JHEJY6ilszJHK0R/FcmhyT7pCyOpPcJ64crCRGFRwckwgrTQesKZ3aNiBaVTspjlQ/PXDktwK7mrJ0uQywP5fLnLKLCQtVwEA+aRkr6qCynQViZowlVC8uMKLDlDGy5drtlik9WST2HZvU+L4/qSvtW6QRodLwo3ooftE5VUlQm21spU2R8/eJd/w2eABKVQzAAAAAAYB5dSB9Y0IzNDT0WdKMNRKC6Y6NphPldmuhmyb6qTKMNPLGuBI8I0IeBnEwYKFS5UWC3/Vu3+r0f3/6GdVVoUjIJVDpdgBFV3QMpayraHDQCIAXcVwl9L3qYa+j9QFWhNeH5TqWU8l3Vs3JbdZtEseToExxwpFbheViWOoxDjzgqNurmKJTUIOW+W6GZ2dWOxKLxkdoxLWj6dFw1IqCl5MehSDQvKRJdNS5i5cb5Z9AauaYPbUaMnlc+BZW+4UyAeIdCIwwXUSc8PlSeqrEh8mk9HzD9DQola7VihFDS5qnx0mSssKJog4+PhCT01pCqjGxtKeIc1u7Qh6OOqVVuYqU7uNRoasBIAoAACpIRoXMhReIMKFaJe1uZGE7/KW/yfmpoAocPF+bUgoyE2oTNulj//ds/9bTaBX47ooqZoiWZFIwAAABLSbQ5qUIMJsK2Bx1gy9TKy6sfV6sx4XSfaHG6TdJYjF5xIs/8qtY4Y5Va7cD//u0ZBsDBf1wW3MMNfJFpsreYGIcGom/ac09lckKHOy88Jo4D7EKY9Ya9mzpVxt8pHxSVmF0IeuXoZ4fk0uO8Wz47TH/KUNQdrixEYFMVHQ/Ghs02yTyM5c3gsuacF6/SefvepXkmmnpq5jtj15yyvPOZ14plmYXT+zXUvJOlqinesxkEB2z2wuOMn0B6orHil0WxloKjUZnWLTtz49TGFdODYLeyr25itjLlegEqREAAIAgAAA7KpWZO2FHcaLGhvLXvup5O//RX6M4KQilBkWq95l2Ki9HlO50Cqx20Ix0J1MAMgggTCCy//nNEn1ZtX674N2l1ISA6qwACGcq5IQQGIo7EAIgENKVUQrjSk1YOQXWicE5T9JL6tHbrZ3Mtkjfdtfx3zO8s2JZe+19d1gr59wWY5Y7vCpnZ0Q4K1VMZ7zMsitcISuUr9CWyHC00Npyum9kVmkvEk0oXTMvaip6I4Q3aPUENXvGZvXCfdwkUc73qiGlEShZkJY0nVYaG9YHlC3zi8qjzsTlksG7qGt0a5afZXGJcVkIt6MXrvQl+s+VzL6+hIBcP/myWt0zc5fcbheZ59iC7yOynFdGN2M6y/ISLhmlcVVBg2EABiAAloriSUWbNsPPzr63s9IMDHQ0SWc//o8iyWKv+X8r/L671EvdBdtlDAtiFomHgilspIB47PGv+zrvhWSFRHIIAAAgijyWtYigYgYypRBAIWoXCkLWWQ9rPYw5bhwNFp+Zj8NYVbEV1jOx2UU0sOU///u0ZBsBJfNuW3MMNfJChxseYYIaFyXDbc0xL8j1EOw48I24816/UNxfVIXWbLT5aXTNScEo5HZ9ujRyfwxHX11lwtJaL1A7n0oxENWTSM+UF1s4jhPFxNLBw4cV2yxpWchLC/3LlhfaeSoystMTo5ZYMrKTxS3K+ZmFEnRMltmtZtqxli0XsVa0buiNfmfzBsjYYqNKqYJIYhd17+sROvNLZu2Wt5pJ6qzQJG1KaCaGAAAALxL3OcjMkXi85N22/tVO81eibJLubdN5P09EuUyI5ymKmpm/2Tkax3vZ0rupn19WMG/OnmFb/o/3N9MK7SyIhmIByAzIlxISVTF90qk6HhLaNZULg2CnOvvQ0obGRifL1jq90gXEv//WV7jD2k0/jYh7UqF+HiZSe+Xe0u2QrqG2BesUiajUOQ8VnT9QTFVQ2yhE0zBKLMIy7LbDLem2kZxEwjVNwkkLKdGSOLK9lYnih/OlTJFgXXEb2Cb5YUUnVMbZbYYTqUti8l2BtKlaj33nbYPN3A4eWZz+e23lWeijZqEOqclFZiXYbUbUh8jJ1/EmJCSXUoAQAAIkFAl+G+BUHyTfD+0xhD3LxM3JfODyA6i/I8ZtyyncogPkDdrhgge4DPFEP+6//qV29t3V++TqZ3lmdlMpQAAHdDop4D0m4Dxk1ocT6Yis113wZOsiMPJCbTvRogq0zioluJ4lr8Lr9KvL70j97jh9x51557OhT2osRyh7aXCtJwdoZmSEJYqVKKoUZ0qSBOPb//u0ZC0BBbRv2/MMTHJGRXsOPCOKGJHDa8exNcD1BSx4MzwA/r7tXMH0DCNxtlp5CnaZ8gMqaK5pTQILOJmO5QmZC6dE7jCMifBBKWFWHL9iH6S0UOKGyPybkqOdSX6ZvsqJT6DOS2RQyvNq5dNdT5muu9punqOqFydUIteLEk2xOJZgMxBAAAizbSpfwKI/YZwoceTh/m88bUWpdAZewOl+fpcizUv93SI9ciPvSoQI440PH7ut6v5z9ik7XkjiL2yCRe0sVJqd1cyQqAIObY3Q+AVYSNMhnAQhGmcU8oR6RCVS5Oa430bArBwp1NNFc9E6crXH1sMJjymAy57nImeSlIzS3SHx8ugOUM6MUB9ceH7Nljysl8YnA4NrwYNpnVpnQ9OUGxEjPSIclhwkFktni8ktrD45TnB3NXvJKErpWAzFoaHixW0/Yv1WShm4qZi4OKDLWUuuRE5EXKLPeRD54UKJprEk0kmethSLyETkkLrmmamtlvvETfdXZuKuWtG0K2VKoJbG0OJI7qpmGICAAERAUFFCLguBgg3RmrIuz5aPYvSEtCRmJ4r2PZr8l8j6lC6ntljSb3h93s8oPfs9Kta+Z/XQ+iR1SGRDIAAAF1T8XRXXMAhsZXW+TI0S0xV3v280BSeGXqjMXIFUTy5wHRlyDW5XFKbxWHmnCg2Snm2MKiNNa2YjZGaGalh9YPjF1e+TEBcekBZ44YYmyg7FuryxpfDQo0SloioZ0oJpAPia4DWASR1L5KKokia6//u0ZDuBFnBv2nMJZFJC4JsPFe8CGYG9a4wxjcj1Bay4bCQYrUFEM3MColMuMJqsnxEcbThKZ30SRJiJLdiyfsorMJZSHMn36tQWVqYuFf1DTEBdaKrxOQa+3DWB4S0zmMnGQP5OdsDrSEs6Xa9l41swNX7Jgq42tsIkBKkzMRIYAQAAQzANAJaAWAuA0JwQkCS0H0WF0BIKoCLg8bdZs6PavTeKuQOSFTJYsFBYKTTirbUPUHINFr//2MDXKJzW7xLABsdGnum4oGxsgG7jS1F2epzuAkS08vtCAFSkhsxMIS84jCtYdiSToVJJjfX1zPaWv3w3Nik006V2x3xgcE6GUTiw9Di8jMgJrR4iMk5uH9CcK1hLiJLLw9EMzWElaMyC7AsYLoxOD/TWrIkqErCYPGPdbKiWiFYG6Yv+JATKnkhynIokM3vMJ2SdP8VmcKU+XCTUSKuNRLYClReL4YFBzb1Ti6Bw0YTKUJY2k3aNInad/vbLGMUmK8FFzcLELufjGx3o+jkoEg9PSnBGWABChFAYWAGDDGXPSFEARRx02mREDZWNWoXO3VA1vcamGpYcXctk42ALOyhlT7y2/3Srqer/0fTVZIRVdjEkAAA61BlTSYC5UZSDjrEyQo+hxj8LCW4buEWXPHEN6wdJyOGFx4mYONvkvOcmh8jcLMCmETyKBEkiLoZhZtlxHIVGtbIQiAohBHInx6QSGTgqOjKYbPMqA1yMeFQ89BJk2SriMLJCsOriolOMshKBY3Mz//u0ZDwDFiNxWvMvSUBFIqr+YGJUGF3Bacww18kfIGuwkwiYEhHQ2bNkKERPNBIm2lFCqDjBpTkScooTqBGPYWe9SQy5Cs0hi0ZtlGOD9GVpxkZZyEIRaplVGs1K5FprKVk2W7brdVYWtSk2NhULbmCG0MRABIAAHYdc104s3abVAYCpMtTZQ5hgIeSV7Uk/VtKjIIHjyToTCRA684GpEKHkOGNFClCjDr3ipRxOj/d1/R2fNJU3hnZCMgwaFrLKocdI1D95k5C7rVgwLDnRWHa5BDkTMPQ5H6R/u35Vci+7durOU35GwhbAnY89YU3SzRaR0NCXwHJgdx8dkYsmMJw2TUSGVmPPo9J3j2dl/Dst2fsmMxzPxCYKZosOzAyMni6ugdjSmgjHS1pOaGRSK7kvNn6QtBefKVqsm7CuOzBxUR0iapiYHx/Vd7UVnKs/0NIDguKTdGGp9Q+3wZbAZUwYhYHEmvaPra7R3bv8JItT7ExvRbaM0eE2FAyIAj2wsOAICRtbQKKIPoQera2+X103vTsi1McjSDGM2+v2oyUa1SEIXf/926OVSdKsCZ5mMPmBcsHnNanYJycc41TFdSq//2wkwAAA2eJDIOlrUOiHJ+i9qGqaKmbfyp22zgaKhQEx5o8uMqoteaGUclEAi8WVDTuygbPnbYQCIdFJoiRN9OBrES5fjZJRyKT3F5y/EsD+h7QvJS2264WWFiwmMmDpyfSP6QsFZWrYOjEyQXSyZiSmM140CWnPUixlwups//u0ZD+BBjdwWmMpY3I/4etvPwwSGEW5acwlj8lWIG49hImYSCQoSGT9krC6h9A/Ah6uWWPm9rBvLnn2nWsNTNc4cNbAXljqK0LDjW+SSnC9z0/1omZhhvjmO2js17S9xx2z94GHNpF9Zb8fsXdRKojRIgMEgbyVpg8h17Z09Ucw+BYABTYRBi3+rHN/KZXnHYDgnby/T7lMq6P01QTPoXGgSwSfP//9JzX8UNnh2NQEUAG1txR6RMSrEI0O4scsqghIBF9m4wpT673nlDYiREgOGnvwLqrIphtCgLFtIkbSdJzctbyJBgrTdEjp5dRyoWE4uKS4zETowy8iSij4rH7i1rVxiMTy5ygl1a+VbIKhSfHy9ce0fXJzkomxLJSU8MUKchacXn6AYjSZHsDaJeYpsqeL1FmFnxH0a5ulqQXxE+aOOandb3ki1+CB3P9yZfXQpP9Q2tYYaq8/15hp8dN+z+TB1LR9u3q/5CJJUVlvkQ7KRRhoLFzU4p1yJLL+EiFxkZmft1EbCIJTQDA7NgAopRjbLs0gq8nXIWVEOWy5PsnKIGDEBRBBTFWSUj9jDvRk///9zL7szmBFeRETU9HpTVU7RjZSEQAAACdU6THA1knYELsoIFnK2vGmU0JtmmQE/8kiUvmUtYBGsoeS1tqV1vmIptn3THx9dnVEeMj/AR3isPJzjjbiwYE4qF4X0JJZdHFM4ygVOzF8GiwzxglFbzFYtdHVRDBHOZmQmAMkcKsnCICT1MihCmKnAMae//u0ZD4DBiZxWfMMTHBJg0t/YYZkGJ3FZcwZj0ElEC188w2odYB3nUD4qg2HR01ylLiGcDCAtmYQsfaLIpsKmipREzfWIFJivAsmVTdlQXL9m18VIlu9dha19aKxLL3PVpNxpFGC0OhJqhurOXbyYuZhv+iCAZbSpvmoY4xYMCgFRpIpfdcFLlvWthKTIR5NC8VnkoiyvDtlhVGafvctRhYSRhsTQsOIWKaQmSVPf/6+/r////rKVhmQhECtbdDiIHMYZRtdBXZp0UqHfpHcd+PSeeCgUAjwalnQL0s1AOMVR5JLBSBJGcKYHCUUQxnl3XBL5IdLCh0IgraJzRCqTmEg4gNhHou3PUSJCeSlgLyUvH1ObkyZSHxdOVzZ+oOB24tHRmU78UV7ETK08XNnz5BquZeWHLCO5fLxtAW1zUEZjG6hqHTl7Ti1735ost+ZOUuV1ry02hImxzOoQTM1nDiGBpLBWfWtftvZgjV2raOdnus08111y5Pdu4TL7lKCPvWABBrm05oeksK44VEHGoH51mgajn/yfhrlOccmoPWGGo0bZFKdwjMZp51Oq4894i+q/9H//GgisBGSIGc2wTMO+/Kqk8q7aMAAALhjSpRFPEVmpV1ccHpU2oDS5WMxEzD9aWROSn6cpOoyFy1GrTlk8OjlFjCpW+PnIbmkwuLDp5CiSA7pvey9we3JhdJgnRFzLvrRKBQzE8kl0T0mj4bD9EUoSWTGDd1OPYXiUPbvpCPUqm5NWklMcPhCbXI5//u0ZD2BBrJw2GM4YFJDo5r+PCN4GinBY8yxMckNDav48I4YOWLS6UFJ4kMQvjPCewmVukEyPimNVbyfssrEouUKYLCZGr6Nw24uXGs7M1cBBKiFazKN5cbn2F1owHJGv6116V7I4lC5xj7nyuu3UsLNhu4taXrvW1O7roEzggoVYhlMxhAAFmcAHgqj8ZqMpwivRSYRnsjxUWZ88/tmX7z3pqV6C3VY9eExQKCqTC1H3CghZcQeemkVPq6or7q/Vs/XTpDGiEQgAYKJhTtCQyfKVqaFni8i+JC8DHG7Lpl73OBHrNlGlTvNOlk7RPsvv4pSH69Szixkn6OSR1CU0HAkIhISrT8yMjszLDp8hFTBEPdQnywOJkrP5FgfMlM0WGC0fz56M+RDSmNoyANhEkFSAVvHiEqAihAgXcCtkqZGDJCmEMYEzxscGVhKqZLroNVD66i2CYkGxIwkYRI7eMJnJvGkxckmZTMBWSOabaFsU7x8uBsSnhQop0axw4xHShChWn0CXk8mYhGcLa6TCm5CTYA8szQcIQSgAI4FGlhgnUX5fZCVrKXJHHhxmArS3wbby75IFmr9NadPDuB2LdrLvaBkOAqnzEyxl+MFdW9P/7/+rquDNYpmNXAgFAAAIsnKYrgG6YmpewUtq6DdWQLbVsUBfWN/nJH5hytjelsEK5SdoJhi1p9CTFdx3jwnFq7JaRPaWjBdAfHxGgHReeeCY9GJdnRataKokkwjCEDpiO0JyoLxRK5ycuiJ1jnh//u0ZDQDBp5w2HMMTPJGA0ruPYJiGUHDY8wlkckGA+t4wLzI2No1qZx5MuYHwthNUfLuFQ2bHUlB8biD4aTISNqQUEJg+KlhC6R5CNjAggkODUIERAcIxSiYZf0xgZzVhObEM0oTN0pNJ6zWsTMmjVLlZIUJ5HcWUBEg1GqmMNKR5NKDyBpykEdXpEspazIyZajinMTBAKwACTNskVhCCdAUpYRfygFvEmRQADRhDH/MqFMEY/6ezZXaJC5IwmUaxMRGgkHN1O73nebFFdX9f69zKPmnKaGZXREMDWNRIVWhITabq4Kgkaamw9PVlL6vzpbGb9OrSh0oXVhSqGCyw60VfMohcqhNlgSMqIsWVOEjY5YUWWGEBqInAMVhxIYmMqIJgwsHzIqI2RWdHA0BbwgVOD6xOSh4hnZDbDorEuhNJTJoU1tUqE+tWYXV3RLD1MY3VvGfEtOYmiiiWrNn3S0cu9rK2pd2FhWXFBynWVfSK8PkrhwulNVdGs9ekedPYKNPvunUOIV32JY7bMWszE4kggm8TT5/+uLW4F93bCyN2MIAAEAABWX5eHIqBsUTKQQ4W44l3urET0jJOgSjzsTmS7Q+4ETxd7D7yIKFCIASxxk+pf+zbQU+jq29wxfeu9WR6qogQAEh2FKJQ9ALEU+1SM2VA3V920aczshAURFcCrAHCY6KcVFSMPN0KUnlXGSVufOCJ6x8mKnyIifKhnE+hlAmxGoKF0xVHDRBUJnRCNaF4olUwLavB54CSQQI//u0ZC8BNrRw11tJY2JDJBq+PYMSGn3DXIw9j8kSEWmgwIi4yeesu1ORxqbjgYDibA+kKxNVk0vFMeBqstllwTiYIJwJRwmTsEcwLrjx4aO0P2Yi2+dQHd1xyQWkTqw/tigpQoTJbZLB+tXKIjUtoChcVl52c3UsluithNEclM1OF9vfdRHkSSI4le41W2PQQ3XKXGOOcpalGVrAkoaCAzABAAADE0KaFa8FQsLZHI6owa+3V29UpbfDP/7+sK3qZQ2zDwONItDaxgecw6fM57/yZ5ykK+vVu2ufmnr7nVG72+tAnd6Vqmip0A6DyWD8K7WAVvQCJdsGdRhjY0+cSsmRcNIUw7gRn7E43lZZo1GuG1qiK9ftvrXLc8w9UDMyxVVFsfiaUtzxuWUa8CK2rLxra9Ox4KC4lG5GqtdHLF5NNBFdSCCZKpJhwZwFAczQSaF01QiWHwkHBVLBlc+IpMaWmZ4Wi0Zl8iLG03pD0+4/QX7jEmH+qE2JvVtOlmhTOnZLymKnnuqKF054+W2ubMM+pK7C26k6d+qGsXau6i6Oyymo0ytVWJfG2pOUkGs0RpFowwAAAYJyJbKJ0ZNZlWU9GiFehnb3mp6rc5TGLgx0ioMYkiLMFR4adOgIDKa8Y8BgG4HhcOnUn1GhhHbMQPX3sp0qY66OBWZZViIxQAAetJWDIytQEYEATydTkYkJBlTQrrrOe0mchqUXhNTEn0cr63mvDctOecK22bOLkfzuGqJ2eFuHO2MVNRXk627b//u0ZCQBJo9wV3MPZHJDBVpYLCNeGcnDW2wxMcjsKOlw0Iq54jzrKFP5oL83n8NIPnJXsildKIiz+UsFRYUqgWGCMO8CBAsodNsrVIqOSslRixe26jQFTJXMC2lXttEf1EmZ+wuMqrD11akfQGz29k0MTbbSpup5CTIrtnB8/DWiVxnCU2yTbtXcqsKpZs6g4scKhXunkuem85sfPtQH1btXePoUNRFBZfZdQ7tOAMQoACQe03bW/PdaFDzOu1VYu0atf1fNyIofms5qR1bDY1GCRUsbubeNAQfFRaZkXUclrSRtUxxcuOEaKlLYevCz0D37ZyFBNqSjwEBqCzHCUEQaGyF/VKIwoy9lJAkBOk7kOD8eis5h/S2sPomn5ULU8Spk6dW6teTPwQL1r7i5ZxUMmz+IteHTMbKlWV2hIkflKt0F+NrFpi6URmzk4SDCGIGlRPhcHRCJEziEqj0sgDKjihxEUQqqKI22jJE5FAD0QkJpgmXEqHHiiBcdIZ81gwbwQrLGZEoNQRAcJzBNt6ILueITryFvRD0RUEypOubVWIUPYeiEAbNSJJqliRYjUW1oUkK+nhASrGycwiZGApRJNIIAnJmOu69Vdt676vaH31PiR/+Z9Q/Pvnfw+9VkT5e/n/Z8rwE/o5tqtMt5hNgYl33/KSTmMCAqWZh4dUMZAAABhtyHtpoDgm6iiFHkzHOYSypEySS6HXDdMVsBEJLnBhAtI+Q2CRBrCyFJEjiu2sucZQSFMZ8myZs4aRYj//u0ZCKBBh9xV3M4SGBCSho8PMIAWanDWcw9gwjeiaiwkIzwJQukiJiBRFMywXsKidBJEDIj0XChMKBkUIWUTLJnlyZdsKsSbLCCRPE0UFwTWQNETTD2YoR0u1ZVg6MIyxHS8lTNKFVUzZyyqOuxBPTBRgqKWDpoinBHJAoTZJuhK0+9SZXYQYRIJyZtVmlo7Nak0kpIcupyW6Ob5uPwRseTFKEuJtAAAgBoFkgFKge9K9E3b+lVnmVtNFc9P2VvTKr6uxYzop01//bN1X1YnL+6aa1/+y0T0uswZ4IrnYv+c7yi+JsjMkIQkAAkZmhdJEckZOQlgqyjKQ4CjEEEiQwvgsHhBQiaqsXbjweJ4lXoZcy69H8NVC1lZC959ZY8V7PVVIV1h+sonMkhDcXHPkolrzwprfJsZOHAttnbqpszHwRlZ7hW5rBKb1WRHl6oqlw3SKzI3poeJW0mqG5LKMvnq59ceFYezo8U+1DZO4qOml5idFpdlGf5CDo/Sxp2Vx3iJI3ZD9z5TUU89EcsetQKFaVC5RKGjslq8dwrl8Ths23RqF1le8wn1a6h3pAibfHiAMoIwCAwAySFUlGl6A5TzlJSnL+2BBqx+joaiIVLPAokiWKg7/07eO72Ytf6SVUSjNg+zTLUlWi1V3czJRAACS4MDOQsGABwMhBg8En22FgabqPSPDyts/rcqtqDSlEkWtlhkfGn6oR5DBVYSGWnXuvi5PZhhw7XcXEI/udr+qs+zV2Ds0Sm2NqCrq08//u0ZCsBBhRxVvMsTHBBxNnYMSIeGanHV2w9kcCYg2o0IJgYd5SgjJs7sdrEyf1hegZkeIpTNAfbwobZOPcZXI5LUJCNZqwq7S5ac0+Kz1nmkJtgwNskL51ziUU/MkUHhVBGm9AditTd7u2fIUznQpZ2jhG51oEZFNuMMuBsvHsErUHH9nBDz9wgXZxCZRa1EEAghECCw7HSJ6Y7GlFiFG8MpZ+cT1fe69SKgs6fsnpr0/1I6CaOGLB8iY/S1DFuanTs/+trC5I6KJY12AlqagSG5KvNISgJ9DasbbKUQBGmhtCdedaol8ppR12UPrcrtBZXrbhc0cWF+2W3NV0rorBBpSM9hy0pbDixUjKjvYu4znWdfbVfZmiF97xSPWFOwcRzUfn2+RpSstmKyp3RncEjEJx8cOL2EhSbJxfWGIit4PckmJKtfM2DUooV1IqURJ3nlZ64uuVSmU4XOiJsrV6tyOtYrr2lgnnTypDq/G5cnw6cOsn1lDa1c/AWHD6InPVdZXPuxwZWVyk4vVbdSvTsw1TmTR6erXnDjmI4ItKh+RKYPIN8omkemVAsy9J5mcwK/URb/FDn1afjf3/1//9X+nSgrUiZeXYyJQAABf4DO0kTQdGACy4GMTFdhHpe4vEAW89DzRiBQGWCIamQxyREuIUM2EmCJpkgGXDcFcVZX2ybYkRASoas+iZIjJbQfZI0RdlsyyPKGxOouTNFQctinQLoTTAaFRnvUOBlsrYNkRswSiuAsVFBglAQjBlY//u0ZD0BBi1x1nMPSpA5xYoKHGI+F1nBWczhIckDGug8zAhAjMsEJlolWo4jevadP1DmLNyXHI5gqLl0STzjZMQybpjO0wfSbmh6LwDcWUJGXiUcwbbxEpJuELNPKooIIkzDdxi+a6zTaBp3bYNK0CYIAEABqQST70vMzOUhYut2/8VAzILosK36f/QyxACYKKCwbHdWcrPd0tQHfImXej60//3xbb93RU09MxDqZMgdpzZxDMCEB7hCJBlRxSsiGwNfaAVnSznBaC47cqJ8rQhchD9rQVaVkwrAskJ7NyZCzDboSbcoq0kRl7lHUiQunO5rtzVOFZkpK2Tm5EJzTMVhIiJujmTspf4X7KFE1IjLJsnSMo5hCeFM8YtPNL+MSwnPhSx2NLqRhO2TG0ojubauEZRhAIrUJWhe4ihff9g/sMLyexVKCmckZi0JZaa01MbXRd24VUFPJSyMvamybqC103OAYIGAAIAMAAyi16kIxBbyAyM3TMFM+iEbSkzoUq/Z//0ujO76ZVlLZE1vuVv90WnCNEgJg+///6lRLBlwHZ9MTFeriIdkNUAAHBRQIsHCABC+kTRhVGpQksqm0NqKB4Gxy0rjiOp8WwQOULMUJ1XZY9ERqncQKERDaGzhEh8qRVkJwXbPrxOiEXVC6FRCpFOIWI0jSwO6dN6qQoppMLzIUOUWMG4HNTIx9gcJyJMERKbJoshQhoVkqLsMsJaRoFUijnJEsYhSENUwsVsQkt8zBttRldHbUINik98V//u0ZE2BBgZxVnMMSyBCZzn9JCJOGYXBVcwljYkKG6cQwYi5pFHFy8IpRtPCcrj5sovJdEq9SKqInRQxdBOSmIWWlNXQOg0b6kAClCAAAQAEaM0OmZ3r3W6pkwFUYhOIi3Zr2dT//JcxalMhDK68pFan0/TqLZVagwscQFQCHYED/0eJ3//h2w6pmwYq0rq6ERIAAAvkREV6iMXHoV02kBMEylYNTVZx0OFAPOxQ6K1xhYsKFylNGXkMGJLYeXIV26QxcPYjezHn2WfGC5EjEkZpao9IRfYXvmR84nPjkGKUmLDE1KxPEsczhGWliglKiQePkptecLMDjdXFWUIyb0vPn5mXy2sQXT2MswHFjV1c6YQGS1RlFkJxRLdIrPUbBGiX3VH8t0rRtj4qIzFDWvHFHJd/D05q+cs2MKFRe4eNOQrdbW6qOFD8D6c7Jx2huxHpLw7upfYAoAYAB14hoIESRBW0ZG88ECNsAgNa+vrZyuOhkRTf+Y0WUyChLdUpK1GR7+yJ7lY52CkId4fSdFtUAQB2WVe0qfHOJ1hZhUIkAABSEhDDjR0Oq01+t1U4cJEhFxcqKrJLccghsLpw4enOWodFZdy84gk+dldBs6pg5mVyl19EwykSpW3KNCS4eOXYsnXQFdDaM3S2eIRJWlo1dFaTXDoT+OUbrosRrImBwgA+JMHiWYVghC5MVcSvWRORFDIlaJDBKaTEWE7DKfTVkhrHkxVWcmXIHyJdIXIRtkdyklB5o1iDptojc5Sx//u0ZFKDtjtxVXMMTHBDxnm4JMIqWKXDUowxMckHlKZgkqZgg0UTSeeaREqBKmmUjWLETkS0SDWUyJFJ7i7CU3dddDjBC0ABIAAH1yiQLLJUudeP+5YIIsCDLlK2v0XQUsyt/5zhxUDFkO4QrhEYiCA4GZG09P0UruoJLGyygyjBWMYYPCYOD7S5OTXWMIOnD6nV18nXacrxeKRL9KdNdnGrUzZYZxcOYVWf4pUJB+3Vb/uo3aUhu0/EVE72RfLyhlCL6bF2GSkqp2DGjlGUc6YDiWWyy+c3PHzMgjvBxdFQ5C8n3OBwh1G8QJi75jIYJRAiEKQhTXcjYAY+bE66aIjOFUmUJLOIWKCtmJos3XISZBBddCtAUrHBVRppyEVavr8VeuWFXI8Es0BlBJt6MrTaISNxQIeiQICaititMlYW6qcicsuqggTlJxVmWkKwASYVp7EdqWX/voVaGLMxdqzkujfoys4mjf/DqgCLAMMmRTb1Y96FYiEyhNAig3ZWjYxphQfawUSkNGKhabhM4glVJ1Y4VVIUAAAMk4QLC9hKBQMuUnS0JrzEUT1LY81lxHSeyMuG8IaLsLiJCSokB0PztDCDGoe28slPZtF4F2BQdURsyRy0LNkQ8QYCU3B8nXHOGAMIReTtENKPoqOLLh7WmbJVXMqU6CSLlBw9NNPS6vduAx86I3oIinjiO50jPxDse01cfu2Lp8oWmK9cfuYZtupjxOTGU751CddTTJqbtornzLb546+vdfM1R5Bf//u0ZFcBtm1xVHMJZGBCpPmFJQZOGc3DUWxhgQkpE6VAw6ZA3sOo6UXpCrZw8mJLR5TE98LjJ8r+X/pXVmnbzUKWWqgQAAMsk1lWI7mNF/9yOg2LidpDsgJDTP9WzEGvmNhRaFpmHobL6ais0FQkiixLKqlnkQko9/mzZZk+GiodQswc3Kpm19jzWBq6/WDJ5oro2smZWHELgwUskCJREs6FaQrhuZvj+T1GQurS1erq6iE8nw/ZbUxJE9n6bcrwnKdm0Z0fL3UC75gUl70ag4QyeOXCatOi6WTqE40u1PkOhkVjUrMWjwrE5AqlM3x+HArbp0ZRNlVoftaRoK4QjE2MxpXnJ3iMyHZ9BxaoqZCBEJJ9pbaUqnlxTgXltE+7xYsnPW065cQitRK2uuq5tR0SI7fjcPVqd9a+vy54V41R17B1ejyJuIvopmVK5M5p7ZjscEAcEhTEsadfV/dphYsccS2cosPDyfBzSJEhnv7bCyiJ42WTdOqBRIVECyrlmpxd5oEp39usV6Wiz/8SvKIB2VirkOBYAMoEqFQXLN5NdMjAACEjeWUES0OMHrqTiLsjQoMY6sO5anNC0i+IY7mOrEntJ4T5lvFihYqvXGKMl2I6UsslZ5oZGmpcgaOkZWWOPXSEOxshlxWYojkqiWHhMdCLQWE0XA80QAjSEyGCACjUi7QJI7kKzaB4oRCMKga+sDZ4HiuleJkUBKasA0wTXIOCKR1EOmDANMIDC55skwy0GAbiF2REyfBBiW1u//u0ZFCB9npx09sMS/BJQ7kwYYkeF5nDU8w9JwkYlOTA9KUwUVLMJP7JYuC6M9KYUFZhdM+qTQ9MoGZLsoiVzMj6onJebZVTVYYRoUSExA8aVoHMQLTNsQdw6iMEJdKJIWFBCEOtGF0BmJtYpUhIiCIrHpetmnNYaeVNLSIWVR0+sldqyAwYWCZoVAQGKGk/8rVqENS87nTk9NmkeISIVzJgCibN3EGhlwEAQQcrOg0AiwRgt5xhKF0TdvPYRIuxhCIxmKw/yzeIIayzsSSaJdZTJrbFdUxI2nDEDGSZZjFiRdS1nFxI2KWTmm7JGmFaLipCygOytKylhYuXSeSkEThU+hXMOcojYXSKbAhIUJITRYWYI6XJTlOYXOIGyZDiqBmqaRoNZcMH2LJWTLM8kywUVyFETCj09y0tDC5GwgReDlkH0QOQsHDNSkrNhy8pJtNqNrYqjdDCQC3KOZjwXxDCaGOkNsQPiEGGkxKhPohhFVusKkiIn3f7qcI1a+4rkoLH4uW+hcTwvo+mjkgTegkwT1tF0QYSi/+dpt/ilZ37bIQLvuuWvLcqoM8MMsaPgRSahqVTOYcWbDLqYMghUNQrlKeFN6ll2TRXFyzX1MK26Y+QO48uth90xYL9WiW8UzVKNR2TVh2s2pWHdY+amIpJUSyIfh7oXTGI4Mojhc8IVwijQE6oiD4s6h8HR1YCRIqcLE48Fijaxoi4bIjBgZD6QVBcmARdomUKh6dzChKxYhTOEwuShtWikCR3TpK2//u0ZFCBZnxvUyMsTHJGBSlZPKmWF93FUcw9JwESFKSlgaZYO0hItwKDyFkfJSxGQamcC7ZkTHeECMgZExdCKfUWUZ6Wn5kw+sdg20JYE6yg4AAMAAADwDxRkyd58nIlEGHhuYbXcsR9w9QZHs9EW6PBxT0dDVK+en5XDYkDIeNLA6oKpkCOpFQwuwPCoFgq9/9Pehif0dS3mJd3UzFUDqB5RQhhM+4qAMOJYENEMCiJiNpWFOriHoo9JW0TYCwVaUJG+oQqQSmmsdg1RYvolID9prolnLSWIhVuwSaiTF0B4PFydQuhMkw/LUjIgGF3ojxWfJxWkRtSBvHCUnIyxs2gMIh8mWOohdkgHizo0SsJvWXmd1AjXpNt7ZtdiycYVnNNYUvn54iTy2pLibmHEjNumMvnHLwt0eY9qCvYWXZtnW2NJEMpIIG2iMqSO9ipysrjIzCMX3IAQA3gwWWK/MMuTDkDNel9fK/vLjeX6HKtZ3M9WmXQJz/5r2bOXNVlH2QgFM5NNmxKcNmwdIBQmHg0aDIcZiQjx39H0DiFRURDGQAAJhWhICTIjKNCWHKAN8yt2034rLWPQ21aLuE/MpEiAhrI/PV0KkT4bcmlJyVWy83iHqtdEssut9Loayy7LPPnkDKlWZ9YuLEihcTSG2f3POKp+dhSftGgyWqi9EB8kEMkTJRGJBK0JzRKmAgWTHGmjBhUUHm1BtaJ0h071CoMh8TUxNAdihZP8nISRXC0TCFyjZ1YoKyEkR7HbC8Z//u0ZFIBNlxw0/MMTHJGJSlKPCaWG0HFSowxjcEYqiXgkol7mGVwRUNNLcjxVCy0IGyVUmFLloMF1ShZ6Cc15ESst8FxAinZZJdNcwAAAAAIABPxPGF3SrxBLKciNlsKmAg9Vta+ZN8yZfsl5EjGMf54MTyfkwm9JKIXgHbpmhQiFf/9f//xBLg+/PkIYKHOGFTfXVoBXrc0nm0FRrrXIJRRWXYuR0Im0lp2AG2MQViBJwkolrB6UrtVoaNtnzRaKUByfrnTpSbJoT5ClO4sOl64IHArOi+X4DGJCfhOiclMFx0yTB3hVCIWl4gEQ6Tj+DcJFqsKw/LbxKKI9uCSaIlJVL5eBoLVZwValiFolWPGBIaIZHjEtAMGzgkixSdDtcRHtaaSFlcpKTV33g8ORWZ0VHlYjmKryAatewmRurYaEvkNG+XIKnJbRI1pNLidKvbVnhkofJKHZYZiWP5WODuCOy0yzDg6YPJeqAGARmaN9ZMmFJGFWaI4dWa/p6f3FRo0w4phFwUagqDhHVWvJRL9FKh1Ldv/////////+piupbFFmCEs9zPYd/ZsrGSI7jKImIVlIygAABqyVyaJjInUKPa0WpL+KUvMz5gKb7OJtitNNwDGCa6RyOvbw1cPTArGD512vJkaq7dDJYwq5O3G8s5pY802WGm4V3GVBJcWvr0ghLl5tEZXuwgqjlN11YUH7yU6TDyKBttQBRsgXVB5s0ZolYpCFUiHEYGXkCJCDUgN9AfBFqJZUgC5csQD//u0ZEcBBnxwU/MMTHBBZpm3DCO4G1nHS2y9iED2mud8Z4jA6YhPhEYFgOL0KyxER280RwNMLsIGSE6SINtcnsPPgpMPNclJ4noI2SZ/Kh4ltERlJkkjyt2XIcQHyjPWTkRTehnugAAAigBHCAIWVAMCJzlh8jnE/3N3cXOSCg8M1Kkc//xfDqH//0L0R4ICxcgfPk//8gBx7AiLQAiBywjPzjVrgR3r9rbQwPvrZ96rL1pxCxD6q+NlBHSrykLpGsehOBwtlQSYoFvvK2iEsY0pQNlmBKlH49iTrKXIi7kKx09KQ4RJx8OSeh4uVHJmPj6gntRDsYiMOawvqxpORqbLR+Bwvmasdy2WTgrFsnohwFYcFY5AKSToO1kZmrOkADCteoJQ5mMZytHkJyM8i/Q7qJavrJCadqTszYVHTbkRW/1fLC8IbnnLx8hLVJlZtcrdPDHl6AyhkovVuzxfE++lpYVjGrERfjvVglG+l8vVLJ4fEZ8+JU2M0zfwpSyfr7ApBACQAAALYoCvYHrdMnQIFG3tocimbqygzrVb/+/r//3/0//9VIRzodTAQVOFDD/Sz/Q21ESWDUt33iraqihUdjMgIAAASyhZcIasDdWBJkCNYCIJ0OU5TyH6bBCqKSGTEGiY6JTStk+w8ROS27VXE4pdXFde+4nTvkAsxLGDgk42fj2RFBEJQuKSheyPYkqzIJ/KjK9ErPxGBwzMCGGTJmPQlh2FB0cnAmVOpJ5kpAUTxKPkhySWXjAtGC8n//u0ZEABByVxUnMPYMA9Rqm6ICJOGpXFS8wlkYj1AiZQkYgIlc2gGOlA7DsxVn5SYHNCMCaMDhLEWikgohPjP0AvGJ0WFa8xGY8Kx0sJjwVRGVjFCeXmRTN2SQdXM4BkQxFMFJ4U1Ty9G6XIzCBEy8jVk8kolKCZKWcWoDRWLjxXSH5zC2wx0PiCMACAMAAFEFYbV15O73MIV6tuT/+iMDdzud9ke9v/7t/8y0VeywoBcDV7xjnjjjpx79LE9BJ7Trt0XlRFNxCu7GYwgdWNKIztClRc4sk2WD1h1N1U1OFdKo11xxt6ozAo+RkZ1aiZcysPDhhGkcKlZQJjuvKiB6NAf0gQNozWkUBgaQECF64LtCwVgVQoRw5GMyNNL4gJSerPxHTFxdcsKyZY/NcPSVQ8L5UCuA3KZwYUKx+cMLjI1MVy55lCeOdfWEu5kJZ1UrrDpWdKKMyeLVtjR5I7YcWa+46sdVrZO1jRmtT3K7TquNIfari1c+nSF9ctMD1o+QaXQPw/fZOaIapksS5qR+J6xmerDEpLnoVG0QYwACMZMBEilqNMcsz0oNxN8u4PqDTRdcQeTh6IDnoeDMVODC5UIoDZ4wKqg4VBFQ8bvvarosKKVcSRsltIVmRDEAAAExWgDEy4gNpS2gUdsyhisq+10rWTzhlwcXwZK+rIwWpEgVIg29Mur0B1JB0DAxJ84PyQU4ZD03dTHxxU3XsHPL7JjAuDsOJAH9KUCSZlYpj6UTASkRZAKSi8SiyWhCHN//u0ZDQDFyFxUfMJZDA8hRmuPCIuHO3HRce9lcDulCYQsYhYUsiRrS+H4SFo/eeFo6C0SzIHj87aD44FJ4KBxEkvD0mLCg/O1A1EwqHCse4xamNIjx0flraGIRdXtCclqJxcTE5McVM/lgejxkku3OFJeNzy1BynBwIjsG65ElcfUJ6tOwHSwkRo1k+wrIcZ4YmNB2PlR+cxk9fcsKolDAQQEBqiCgAAReoAh9q0EiA+XORfVE6f/TM1W/+cSZSI5iKjjHOlsazQ2lNtzBb19/UEossXCbXSYiyVcPw7obKaAAYxuiykrCOGKT4VZkmeEsCpHOTuGii3PT2No+ora/XsQG5Yet/gzyp9wszTWpVSIbRvc2w/mw+2CM7cJlY2K5XUXWroG08JRNaWnU6dPdPHYZB5yolCWAoEILCdBuH6aMGE4pBRKJrVyGN7Y4IWWEdhzrtiOpCUW9XaUUL9kJq0oNuNE2MuUA5jteqJJscVqmZlzHJNhcTNmsk8iviItI6J6q01LqCdGDylc6lNVYoRR0fP0Yl6vgJpgt5xchNpYjBGVIMQKoztwhHpZNiCXFxWWMrcPXlh6cGxeaEKrY7ldXcugBRATFCwNKu1Y/btqc7qLdPI9fO7kZWKW2n/1KhWQwJVOMWe0Oix6S8K0e+K7P6Sjz5mkhS4ivnId21adGRkYAAAACkcoGxJDI8JDNzb9BddThM8VG1VULitJnZS3R6NsURug0dQ3zVeDHjQpoj+bOrNrxlhLuK3skPU//u0ZCABBzBx0XMPZHBCpRlUFMOQF9nDS8xlIcj2k2UwkIzwjgzPnKPEjE9Yy6n413W08erLdNx2dCUQYzgqEfhaVA+S8rSFRhMj+RythsAhF04HxtkETxwRERGGh83L6RDCAjpROZLfLh0Kq0wLUBWKYTEYejonQY+nKi09dZLK84ZvrSVE0TVBAM6UM01VFGL+oPHl90WlE8Uo3EEuk21EKNs2PimV+QFBaPRyHwfDgzRoRy04PiounerlZicmK5dRo2QyIcma/wkYJaAOmA4MKpqUTL0zJRRp1hzHvCKmaH5BnFKE6mUQ2f+c/1BNloKcSbjILGnBN1uzbv/t/Q2p5sSQinN0oNuGHlxsxCwzEOgAJRedYweeh8sabU0Q2RoiLCHSXsqR1m+vRB/HU0hESNXstGSIgLqJqoGY4gwjRtoUz9LrmVqXfGRmIwJUAhIB0k0VRWQmJBbiJEgNXsOUA4gRNjiBtRAqSmiRGulO6RiIfVVIZMZMYSQnT6sNsnRto5plyVtcvbcEzUTC1pFZJbBbYNsiAzBLEz3sgFFVaZy1pwKKxPo4Y9laLUqYR4dWIVsWIEqcuxbhXY7NAzE3ycilMkRunN7NIQABiAAQcY4wOpxi+c1pcphIVp2z92Y30hCoMVqyU6f/z/X2OqQUJncXv3+S61I3VCKx3UvrfIxPtzxiZgGX6/tAIAAOyaChJgO8BtZQIojItYhZDhoEGFoPtSohSMjbKRhy1+Wn2po0SyI6WokpzAOXnvbG//u0ZBuBpqZx0VnsTHBBhLkYJCM6GRHDR8wxL8kOLySk8xQQ26pbUq30rtj5KU0zBcKQcFs7NCQlMVSkekaOAtH5DKR8fj0S7GyHVehwAXTojOkIZcEwfMHATBwMGhETnSIsDjZ0QE5FElQ7B5cKtoFVAmWFwsmXIFSMcI4lRRRpDLkRSBAPly7iNrBtnWHn6CZmmyh5tdIoQrIVw+vWjAWEBEGT5MHhETsqE9rjU0TjLAqEhG8UFuywRZIhUHA+zTwAAgAwnbkNyDUjQlX9cbVDFDdI3Ow41Ckx5ak5QyIv//0N4xGFMCUYdWzs4yEba/9H0n3RdYASGFZh0PgBbxQQABqp2aVMRAAKiburwxLBoUCLNA4KQaPiwjK22Z8gNX4zqLCokPxF1spJFVF7OnzyJTZq8Muy19VjPQpuTIqnK7zo9hgo6VhFGZ8lUGS7Cuzy6WkFCNCvTwPNgqVGiVE4LCNE2ShcXoWCybxIKVhoCUBAPKKMzgJlhCUEB6AOiFEpKVQXJEbCRSpnVUfQo2u/EDCZEQtpHRZcJW5WBMYSbo9wqYMzKNckWfFUlLo1+zGbIeSYm0sYVPdNtJCTQXGka4t0bVmG1cTYlDAwB4EXYWhWrav+j8apVb3fpeuqUEXER7iQdl/6Oun9P////f/////5iNad0ERIijB5AW6KKi6so+JC4tGshys5jDGVSaVmdRIkQAAek9oPgFgIYjC3YUtcpY8vXKzpZjB2luBTnUkczHo0VB4rucLGHyVd//u0ZBgBBnVxUXMMS/BEI6lcMGIkGJnDRcw9JYkcJCUkcpqwWsxa3znGJduyydsIl5kVzWhaRn8IlaZ5ECo8H5WQDps9iHgfSmUzMVDVnhQSMkoNs2So3CyaASBseYaFj78Kly54fEyNeJUP2XiOjjBONorSFqMQYFbEnlDq/G1g+KUyVRcvaajDTYuXSSFcXqNFG1mT6BBB6TMUiiI6iPxaLHTqhY2ipFIw8qdJGbxjCkGiIXRmWESapi1IT1qTMzyQrAMDAgKAA+H0IwGD8u2oixf78OLad9vpPMEbIrkKQeMIJ+lb2L///DIPgQMDguD4EJgmICgwIBUU4xYZWtyF8SkHLFHmXVWURFAB2w0NiRQIQkQxAmEiFiyJEnIN4vEZFGMhu6pt8rUUk3OaMrAMyA0zikjhVAukJsRJF1MIxpucqcQSaOBcKMInCpEiSaa5omGAMAeK0RCq3BCmDQmWWCsxQyHhdE88F22RhEcRMaRkZo0uq9g/jDZVzSekqhMJcZ3kQj1pCb4LONvJr6rNYXXOlycZnbB5kPqlGRGOstqHIUzmsLoy+dXpiVdokg2adbCCRTvTRNRR0mmhg98VnrMICxooxZldoAIIAAkAo57LVA89P+fqChdyI2Url+MORQK4qogsQCI5lUeH2//+eiv////pJ/m7h243StEd9zmmfKVxOqxHsFBlKlqfb1aViZl2Z0IQAAAnzCJkFAYICAk/1kI/Fx0cUPGZIJn5f9Rq3DQpHAjnhsPZhRmM//u0ZBaDFg9xUXMsS/A+RomLPCI8G8HDQcw9k8kXoaXwgAlA3bO6F65x1l61+VilrLROHKO1rXelFVM2ch7dA9o+MjsqKKwmD6pgqBYnGlx0oKVnDFjbMUJ5keKGkLuaBAmRHiFxJpA7JQJyAqjK0qg70SRFCMiFLcVVaPPyXSmvNzKq4jaI5QNNWjGKXYxpNhIU7NitZPQMmFS0MIhVCGLpvPtstsajeRfFYyeuajeU0/OlhVd3clkxonwAgAnjGK4/riHFQjp8o00A5JJK/7uvdGlo1zLV0f0V0Q9nOqK3/+djPYXTPk6gsEJ9xWWapBr6lXW/9aM0obmgEEMe5ItKNlaGojY/rDmruHOKqQ9AckdNu8Zch9rN6DJdAjJOeryOvMNHaolY255FlUqqhaitrI4sLU3v3kNTZaFlWvEkm4ybfQG5DGN6ilCoMvp3BOK67DHH8W1kbnilVylSS+loK2jGUs4ijVBKT+gu+gIqZexUeZkYdkita2Oa8eyZYim5orH0/DwlqtlQcvIZGKit5W2atqbIySoMFuXOi0QqiWlZbSutwQsdw8nVDysNFiImwYlKTip51omnSNecLj6OZaijVI2hLeIryZkuFiWmGF9mBAIIogAgAB5gQ+y60nkU5UcGXRev69G3R3SnW7mUGNZlU6IUx7M51//7C3BOQ4YK7R6PZGEqz0oe7OzsLDEcobvq1jSn1e9rtnd0QjkAADTsiqaDA6CGReRiVkagXEYaMlW6/yzn0ejKrba9//u0ZBKBVgFwUXMJNXBJ5qlNGCI2GX3DQIwxM8kCnCUgII9QWlsFvTjSzMtrCslIrVk9HjydpRiSajabKM4624j0G4oKNEZ08jZEraAQHi6M8gaJWfNIqBgMoBUqsMyEkQOQxWVtGhHRGfAy0bkcIVpIpXEm40iJh1tzDCrEnmYpCAgTiOC0E6NAcUiThtyhZyknCDYWgWQQoWwX/ID0ik5xsJE3JKBWApIlWxpx+QsEx/6OUPlAtM8OLFfqFG4VAIGAIAAAAAgvNAwGipsl6VhJTAgXe1fuMIKpyociORXtGfPYiOQxqg3Qjqxjf//mkMOjAQaWISsOLBU0be8yKidhg0WOeyn7rt9b/ISs4eLHSYSQy4FVkunBTPQRMTQzZYq9q8akT0MumLciiUwnqWx0LtGmDWFTE/G3R47eJS+yuUN01WD+nk9Sx9CkSmRKaHlxtxlWbokZNNiRLBT9XY9YHk0dUsojczMl685FCwyJPvOxlFeybDs5CdKWgTLSDDJc45lFAEAZcKDJMuVMFTzAmLn0DcUaASITxBOrI7kK7wVFiUOppoSE7ZRBA+urzzBFKzB+zzEldOiEEE5Eihw2i/nlWbcwog78RliyQ0WtlCQvjABggmsOAibP/l2Kvf3Y8jJYIwUREUcx0UQuZBChek0JDy//4mHtZFNBCMIEuKjwim3PcijQD6T9hSNRUxaKVzSVZ5ZldUERAACTBk1NkgFTChk20klXLMacvh15lmy6IizloNY2ecRWwQHW//u0ZBUBBm5w0HMYSGJGZzk0KCbUGPnDQcwlkcjrm6UxgYgoBXag5JXTLY22uiJU2yfChFFHIfgWNHWawUqYnousdsXsUtj7JgbFINoBcuB4pFQmKBqZAMmlDK5KKDB00QrCYnD6MHWx08QUFgqgBkTUSIaFUkEwWhRIsIR4uRNqmfM+wRn3Ez0sOyWw02PlCxMWgjFRRyZoqcHVmlKbH+rzRaUHYhcQj7BCwoex5GSoibjxMiYIVhlq0w9J4bNLl0hIgMlSG1ygEIGCwCZyupRm9vrv9PU0ilzCjIacjEjqvU15epV1k1X/9qJrKROdFAqAKElnuCgxt3XkPpDTWNcNErQRJkBUeNQ5g28OoisqQ6CIAIcgwzDH1cAtw9zjwE3V02iFQbmIQu3C3AeNypwmJtggXYFSznrIWCyRRk1URohQmRqWSUZLvFRcwxJCWNRNEbyFdIA4ZXEQlME7xlgNEaspgsCBJ2UQATric6ye6bqk5crCtWsnKCL+jXlRY0SYT9dyo5UwJaFP1JaOqxFddD4kHMnsDeN1c2BaQ43LLvr9bFM51Ltl1ElXEeW1fb0rVD/HlaZYtTGS5YxSM4afXQS7E7Cu6qla4udighUzKpr2mhTIDgEKAmIAAIMEBjOBrr9lNPUrfb0HCzuVamM4P+VHft/R/8v//vYpAdDGQMJGhE+e9UbZ/V6mMR9WYXNLfYlAAKctjs1RbQDAkY1AQI4hmjiLqWEYKuLqd7GxGSoAoGBUFLNKrHIh6CO1//u0ZBeDBnNxz9npZHAxgEl9MYIAG43HOow9iEEPIiVQYJV44ysTa9yMWZenkiOljkyo6oMICEFAyuEVESlmE0CACROUJlCISiQnREqJDMbkoSjDgmeCVcTy6cHjhyZieHgk8Ob9UJcUESV7DcqaqheMqDu+lRrSyde/BGncQqH91SdWuVLJJM33lx37Z0tOmHLP67Y8WHLjBKOrzNVKfUIu2P/KJj69Y6PTR60hJUp5Ccu7ZC9Cq5LL6ds5OYaLmzFd5gBZVngAcASDs8A6RwbYhxEVPXkV9ZndT8uD63s81W/VP50UxgGVv/d/GOcn7e9K+xT39cdIEI2UgQC9qwcZFkiILDLokqN0qTsJWagaqZAOEhSbPB2iktoL5WOB6W3gYUVK6pZzy44hVq3NEg+Ol1HE5XKpmHw4ksrmaDayATdNkJewOw+nxwfqoyWVH3iSDYqrhEHxCC1OerSCwiJsRs+SSkB5CFBPbEVUT7OHWxMpT0+JLJy4SCMusOzAYENOWjcpNpmIGxw+RKXLC0oukPyP7KwUmjuCGkHmhaeODpyNFckQMQpCMs/kS5ASISo8omN7pF0KYp28puUQ0E+XOlho6eXHSVIiOyKnQjhzgIgABcMCTaWju5U0UmvqWl+RIiXmSWNFu5CDBAFEhAgoKN7dn/6FzvTp+ktd9q6IZjTTsWyj0IDVrLzqN0jOMdvbRSlZl2RlQRkAAHReQAKBrgNQD6AeinLsAnDtJCJaKWNRFwH0ikEhT9ZiIn8X//u0ZBWBBmpwz/HsTPBDxBlsMCVeGEnFP8w9KMCnBSd8FhjIEypmVOuqLpcdIL58W15imWa1ETkjpOOj9g07sSkVqRTEdFpDWHCiNOVzlcVit0L68+Voi2fTRbKQ/hPYkqMoj07UrrHCqDcaj4mmtzVSSoOeJCA4CwkVXWFUCwpKkhZhMRLSQSMCjJqzqnGZBJk0gp9DZM0pisiImUW1tU6Soh+HLFo4j1qa+azNVZfsGychTWnqNdY8cxDtlULkrUZic0/SxagCDgABATZODxu7Dm1agDBG4PL/zKoVEb4ThK17xIWLCQFGCd4IEBe6jzcbZBEBPeeSPErxU6JiLRDj+lCLiHdkURd3iXVkQhgABCQPQ2ZyxAuqADqMq5WaK4ZQjY43BYLEvpI/lGaFyJVHrJciG4paosuom20SMtzOckUsmUFx4rDU3xOkrSRAK5lBWMzEaAkFfDziLEyZEVEZC0Rh4TJGnEC5V0JmxAsYbQhlCbBbFDI4B80iG0oDCiyKbCoswYQiso57RCiRSQECBPUkop01cckFjEAWJEB4W7cb7odEq2XaOuzf81IwlSphCqn6JqRmCVRdC7bTIGdIGUB9A5fXuN1Vvs1RHg0aI0aCjLCg4hJ54JzHP9RYSjG1DdV6fu5H1dqOjV+37dP//b/Z6Wl1lURTAQAAR0CDBhEIU1VAWbsmaypctZHhbQZNV0PlWlYSEFQiFgkmePppWUufq2XR6WPxsrmU6xsz4+48qZoS0m1QR+fGSNA8//u0ZCUBNtFxzvMPYpBAoQlrBY8GGunHO8wlj4EQmyVQkYl4S0NWsMTluEp0ODlbDpOTvlhKV5hNDk9D3sPSodkwvLTNSrTjwYAS4dVJSIFYTZgrk1k9OZKowYcMzk0PjsP150cNF3S/Y1OUA4QTo6jhX+eDpqVtuWyS4+h1RnpyZoTRVXG52kWJLegPn1VLBn7BwYIXIQ+niFRUv4qFxiM/ZO33mlRwkZKziAymV6pcSoUXnVbIAABISyLoUzjvulhG/TU/BptYMCzQ+IHBV+RKIShp4AoMlGKYrnfTiNpkYTBZ4vMpyPR+7SfZVyjWOa1KElHiZdWZBAABK7XjCNc7hKGg67gtEWqslhTlpXMRhxyl3jYEA3hAAxR7ECqNI5MeMqCOeoDYqYOB5YJVEUMvQtB5stwHJ4RQn3UB8+5hCOFBgZmq09HwulskFK5k4pSmJeOT1MRnzW52X0Q3eUpC4VgrIicxJ7qQgLDo+XjXQoCIcKmyybPn7x4SVq1WyTy+kXcoP0ZixSy1RZtOuJ8f3PCTyRWZRuqeqshHA6PCmdN3vw4HxvE8hKaHsnuuPwCQ+Vlp5TUkS8rNJ3C39UCGOjaxh5UrcXnzNyLdEQBlzkXRCRYDw0CwOoJb0h/2kbslpDHZ7kvbS94RqtafvX3ke7o3//ojjXVVEOIjgvcz/99KmHIPLAN8RicVZDzlqleFRlcxAAAAR5fJBIiwYwINkDIYPBnG0dxkHkK0zSYJEhki54nLtrPoD1F8B/a5//u0ZBeBBqVxznMPYUA9aglMLCI+V7nBPcwxMckArCSYYIl4ftZNY3oWKvlxMkOSU4YkGFQaRVSwLT5swHUp+foH2DMqj6J5bOj4zj4mSRlKnUTug+UrFouoBrLBX1Of6VGmIbharPH0PCQXn7OSJKREbsHMKkzhaUnHulhI66wyWExOWNVU5UmcVrmJzo3z1906jMP1KRuJc+sOVjNHVBaNj0uliy9stnS4zMOOLKVJ+dtGyIurT25OKxr5cO4FDcJr6GXDVaigsS6AGAAACBsRTzAWB13pTIvqms1lra+tUWib/77O0QgpCFYm///9NdKX6////+V1vnrMaNwwfg3bRa+SysN3mmdVMxkAEqFmkA0cmVBgS4QRRyVblNnIg5ravpFD7MI/BUuQ1zXnUeHLhfXmb1UzhcxciXxrnVdHEJE8eP4xifXz+qxerO07R3EzAVVJ80rOkVUMXHdiyP/kyrbpdj93cI9Gpo0b4qBo3Nd83rmkbnE7aYgOx7TiA3IuuKl5yUe8pMoowm/rKsZKME13rdw4hmRxrIFEbBzE0zAts4lPRTE0K/Qroa8pIkNwZXpaR7CzmYrSXyWvXslZgpggQQCBgOma9Stpn4cyy4VFLzn/WbtdP/R0UMYpWK6IAopgAouSQn6/lb9r+v7dv/8qPYzpKWYc0hupHWhnHhW0RVaERzMAAAAAAZEYYRIml0xUSzm8JhM5ZKnOwBd8HEJIdryEusaIRKTVeF3sEEvo2TY/0uHUCGIESYUu//u0ZB2DBwhxTXH4YFA6RfkZFCOMGi3DNowxMckQFmRkUI25qjIfya4OzJqhLIy4ZnotThI8ZUKzqYkJrsEkfVZYQRTI/LDg09IVioTmT9JQQ3xEjK6paXywHUJoJ4UjuoYPHWDArkoRatCWvBqTT/SSts2vVGhsiH4Tz5gourTfnkaxWuUcVh6LrENXF5/GPprGpDhGdSwqHt/TLVIkumFj5YvYPEZjLKOxrJUPWCe8YJzMS3DGyAhnjil9GYnA/nJgf0LB07GX3xAiAEAMAmiLUc7I5FJXjWsw9ZFrPIeXWv9/PGcxOymTufbVhXCO9SOxnT7vMueLs3B1IeahhVJ4+6azm+rJIlNV72OEo+4TOGwigGDPoqishMdUOD/vrUgq0fHbRbWadnTqpZZ9paiOzA+WRCSWrI/8rGloEY8xGL6EOQ/QElZcfWCudOls5WE2mLTYyPg7MV4gtDA3oHTxiWiw+WEBoLChGuSrgqFUAkIVTgGpmoioqDpubRhMTAaChw0VDAeRKhko8EjCAoKBclcqJTikrGC8CNZClMzBWbJ5EkVMHQgvXi4iHjp4wSSSQ2RosMrGbGkQibgnS6Rdbm7O0YQEJUiULsrRXpgqWXQMxKAK0HWAAARmSV66Kz5TS+g5/nxLzf+0YnHqf+35KsaNmVCkE1L4hm2KQ7ZJESk/mj/b//vt39v21NO+etxpr/FSr9OM2nV6aUVCFAAALYl1kc2OMALvOossoI4IhofZMlwhBBE6fy0fksVi//u0ZBMBFlVwzfMPSkJCpxkJCCOOWWHDNcwlj4kMreP0cBVQogV2COCBGmMPccsuW+igmRRFK52w2jRF7c96FYqgFzZlZGfXQLCxwxrzINTGopISNscI1iMyPiQ4wqKEL1RUaJk1yEYC5clLrAyOk1YZhM6kVHiqNQuacfRiBNG20qkWXQLD6jGM71FiGCJ4n2ScCNsnGxEWjNReBwrSEeHkbQgGiZHyRo0ig0mgCKJCZKQViwRu2ElhVhU9p1EZSabOdRN6iyigEcd1gAAEF6zvRF/XDZS8/L+Lqf5kNIQQLv6nyspNPjhaksQNQCNKOOs7+VMfJrv/a/u9/asUH2EECf37YY5HFMHIVc6hURAIAGVoiFuHOUSdoUIgwhxUNcqHZVCJBm4VODpUSOdREIZF2CY44FOSuQLPEbCQsuwRswRnYDgWSroAskQHoEMTkGC+NDo4fnhefgLB/qGWari0W3rqglO2j9fFpe6jpfNHzk1aCbFrI5QD0eOE+qWM7eKQ7OmlDk/QYDwrsrDnl7TJ16ByJZqyWvjd1JATG6F8kLVZAR1X4nV2gRIS5QoYWP8ezc3ojlDbQ6NrXTSXzCN1tYvaxcil3j9hyBzYneo8fIaRZHEABJADqkAHAn1zylt6pSXX9P/uX9XpQen//26fq53GDyCQ8oCzIAqiIucOfVvt//89bPVRdIwTMw1pKNV1K7qdpschVWhoh3ZyJAAAZYjSIziRWHKpBUimQtZxYDYpHFBHzbnIMlDYnHrQ//u0ZBMBNghxTfMJZHBILXktCCPOGKXDNcw9J0D8OCSgIArYRGYipklWa0PRgjRMSQnKTShEVoEzSBKavqjojUSHZoVCDX9yckJUyoiihBUUyKkrKlKjLz+o5kl+Xc4sWGkcSBxwtQjktlezHrVQ0sLeasWVFIGDu7qlMvWdq02f75jZ667YteYmi3OLac+JOQq4EcayIzWW19kxXQP1dgs/RRe0W+09zDMfvpFcNaPlou64/TTaNLKWKKBjD1oFEMMxD8EAgOKbWUu6ntp1p35+z3X4Nf5b5///+SUrzsdoTjEYkXy////otXnGLASFJGkNgHohBNWdGPy+n8J4n5QnlgiiJw+0PETKogAA2yzQaBIpAuGkZhQoi4YZbxljFIOIG/JO4q9JPjsRa0biSkyJ0QrMxEVn3rHViiIeWhTFikhUELYnVksuHzkjFBZGW6NxEkFYkAykOXM4JERwno+oXJyMWRELAtqSrYuooaoGYilAsfERVkfslomJXLlxdcnI+SInEJKwcKE5wLNsPcMEE4r7REo2Wms4UlCx8oW8Xy7DBBIhRRQIZQ8dJeLDy1pvDLEUTM1ZP5ubRljEcy0YYb2CxrlYqRNp2kAjCLv6WzEG3Jr+roGv/VrKjI8zv7e82ypb////66ZVZauZVZV+7rMWf812VEYUeV2MrRmYU7kIU21StMZXGRVGZ3p2MxQAANumCqqtW2q4u4rsiC6SCMtXLXXRbYgyluVeGJU67vOQ04qLnOOlxMQ1q9Y4//u0ZBoBBrFxTPMMTOA+ZAksFAKyGcXDM8w9hskdEOQsFIlIPuMwuMunLB+sdTNWUofOTdGvTG4nnsL7cBweoY4NWKKEkL7RdXHScrHxLZIByPiZw/TDcmQi5+RYqoIFwk8iEogC6EBAyER5uFCtASMC2mKRikuyJUQ0yMg6UwhJbUWTCbGnZEYeJiLYEaQiItKNkmKnz1niFopEvIUjTKFFMpIYOtClCiG1ljcpqnRCSWQoDickj5lZAIXH4Fzky1mFHyKMAhkQAAAAgSnSnsCnM1ZLt5bp/sikBGQc/rGLTGmPR5r//RrG2Aqvck81i1Na0sDIqSUFA6WWwk8DGlNaPGk0WJeHZSEAIpNBx3iQoRjXoJjGGNsXUb/IOXJIjeLuYBIfGZMfGtYweOrGDpO6waOLkJ5PrVTJ4/aeYXNMFt5zV7C+9uTLifyIvHKEn91YwZIB4hIZeZswX4PYUq9QT0tlk4ZKrJOvEwJAZO4itHAJaR5Y2f9CWTDXz06arp1CVKJz9sr0b9Ua5RDUn+JIqHcCWJR30MStZtYo1ahFVBTJWzJFCjTkgqxp7HjidIuhPXl5yeIJ8cHVUNY+w+4Xl8MnEaxIcPrU0mFolx8qYIgIEOkIhBmpjs3+CZEAFDViH/eVP//5PQ5GVwZRH+2yhBp/Lu9tP/5pc4x8GVYsNYTQKsZFQkeCAeCZ0RjTIkYSWHgZLmQVGldZaJYxNAAAVeugxEm0yHSTRBV1M4MQceMgEwtwBSkQyHCAuaLk//u0ZBMBFiFwzPMPSwBHCKjQGCIsGCHDM8fhI0khnyNwII/ICRA0KkNIkZ22HUttH0TaJAckqyXYVCk7aPxFTQjUNGUEDUEdGgfNZFsiXDwhfSMPlwtImdAfPTKCdGhKk5LFGLp0AUtGQn0TaKUY4B6NoSCMkLPLiA+xmLXzRiZoSfoHV4DS8aTQWZkyEKoRKCUWKGharF5ZoeJfbR6KFimU6mq3FqSjAhZURiJ6NlG0KINrOJGT1Jxqa52j7UVlW9SMJr5diSecLJEGiP/T//whzAaIJGgLBDldbP//0r7at/6f84RGFkOrEDs7y0KdCi4fr702m70VnkPPfaHVBo44+NA5vUwwtiq8VLsZCqAchwBSp5HoMTBgACFRBGIJeFt2Yui4jrvoTlhuJEfkhgiHnkcy6sXRQwm0yKkCYnjFh5QiUJyJEoyi2JKUidFb11j6Ok8KE0hGfFCKcCWRZAnYnEBYjI0hBLDLRCSLpohXNh0ThBjxogKOkKxU0AyMeTTW8VTCBs82RJ05uPmKFRiKiMrBXFka6WxOt8VlW+goyiJUMUyFXD47PdoouKkR1hcwzrkKLYQkXaqKGVQnsFdtNctHBSTSOWsAAQAjLASYgMCUyv0s8WxK/YqUoR//2BgKiAEqmKyGV0JJRu+EQ4P//ajIECeY2jtNUUgFAkKw6SeEkmbW//1U8kwjTqp+uDK6NVhGdyEIAAAk6dBoHfIRt1CgF6o0SJrimq6KzTwLtDYvlxesKq3uROGFRYJx//u0ZBaBtpRxy/H4YFBEBzi1FEaOF0XDM8w9KokenGKUUZsIbLDZNWLV7rSRmF27kB4sWm5FIpfRmCHe68d1eqW7rDqKMvqmR5PLlqJ5WVMMjdaVIVcAll/6xkN1p4sKbevEtcJ7gesQmCOEeykvdVHi48eNjhp4qNlw+OzKCBlKtigUompRFs+WU1PRAlfj/qlhwWkNAurWo06JaYPaUz/bd8Lb9iMVW34D9SlT4yZHbhexQjRULy1lGoPDtOpiabla8/WCNQoYwBQAPAUgUh7/rY7af34cSQK//Ocww44JjpxDX1CuwRkDHiXKx//7M+J6/ghJWhVteMYfQBXjUNEoIxSTH///6NPXr/6VZ3iIdSNQJqDw6s+jKlW3Zcw4NOEt+xBBFvcQwlUYJy0ZlQ8kbaHFF2yxx5+LKx2b76RAvMStIEDR3FEi8YNIUZokQJICUl3ItlXB8UGaTNEUaNkCNHBtC20VptAqi8LCiUouSQxBchHUTcEdP6BDzFRLMSaYLJougcaLkCjGbbSVCgrUcLAqigs5grFBHkFL3jPEsBI2eadcSjSa0YoaWg4seiouU0UyciKtRy+9W6NLwdGBI+LMUCijFgaiKVPzIMDw7/+URqJf/21YdoJsuE5v6Snag6xAOk5SJu9AnEXmtJTxdI+2JvNa9UIJJEQGccDyWkzAXEJc0Iyf7P//+WqqNmbyIAZDSNbTYGXoViSla11s5lyNuLeh66XCphkyhnhXfNcKL3yqVn8LKY7oS1TS//u0ZBgBFsdwyqB4YFJEhijVCCN8F8HDMcwxLIj8t6QkEI85wzZ9YecWDZiTh0GqYehDXOhUd1HY+LsYSF8vLUxKWHQ2EIndAVQYC/FBLH89EMfB+LA8rx7RkWBQcH2FkiCKtHgQVhdTuj2YnhnolnzI+wNQlodfJh2T/WH0vkIflTR8VmV6D5iTjId7UQ0poPKpomF4dqsaf1QG1heLBnDCWpecm4xKaAoiJJbPj6lkJ+BqGOygmILkB3RHx+2pHammJes43pePyalfHhIgAUCIq9v2gxr//Wf/9eL6/giDytVdRwFfgsbNCNDJ2gQngdps8DwWYReUvgIuU1mEN///4gBMP4gB974wEAxicQHCCK8w0IgigFWVoSJaIjKqJZFrlyIYIgNfWLFXaH4hCEZeL1SQyq2hOquKpvREKaqzCMVJsTUYUIUAYNTFhwrMlU0wVJ0rYOxQo7EAmRsRKQp5EXWPIwrEQHUzCIgRqqaUJqWFYaxwqLItQuLGWWkSCxVjl+sOm+hhAjRxPoJY2gj6CiNFEhL1G1Jh9Z5vcyj5t/I+yK8VP/UaW89RhGNNJokbWO3VUaUWjCNNlc5FSmYKqLqa2aJG8XZciNMLWsEAGFUQOP/8If/+Uv/z2//8prnzLlP/jDMAbCeJm2gv+1//5///////8Y0hBKajgEMIDZ8dwzo5CBItKEM1GbqXWm1ABwE6CnLsAXARCBjUFJH/DTIsqEq9kQoO9qfLcR/W8Kas7FFY3NR2piNAhwGO//u0ZBiBlpxxyqHpZ4BCTZj6CCW+WLnFL8w9I4EIt6OEUApRMpldSC5qlCpXVFCiKNa/GZuajFAUpNFyuYUVnXSKvDWD+Z0Tg1MLoGhwkMT4CILYBkyJJYPKNvGZ0rOB+O3Pl8fxuP7x8Wh7q53ahrXnkRwUlC0+qerztheWqn5gkZXExZDCciKtxdaxwpQsN7O1OjY4RFZ5kKVakzhgPGHzmxTErWCurVA2PTp9t7nSj+TYTToyX2bePZX1SsMrn2WIDNe3cAAOBAAAAANiL//RkbJ//X/14//f57P4NjeN7ifgNGEvSXL/////v///f/+ogOKBTCLI1xWsoSGh0ioxpDCShKKsAV2aEdFIYAa+NHA4kggFNPCQISGWZKdKEKsdbinz5dKRSHXZKKsWZQMLErKM+TI45a8zJdJdHxppUjKpjAnNEysFUauzRKNlmcJTLCImaFeB8oTNzUSD5hR1iQ4gMCzHRqKEBtEXbSRmQgTkzIoY2qVFeoSBolwgRpstsJBg2SOIxTaGS6FotDKnU9kOoQQENNJIj2GbnTGChAOIEJ4Dz5pCXNmyAdpUvIiZWXJUaGZtYw3apckxGTafixFBCS0rJmCyizKJkJARv//tMX7USpGL//ftb9VR1e6u479Vand2LP///f/6/7zp737z33RmozK+hQrqQUQ0QUWyQT2LERaHPLIU4M4sXUVpiYZTCQAAj69R5LaihWtF2kwV4LBrBLtR7jSsI3mVRKBQnO8QDZO/ze5Qy20E//u0ZBeBFkdwS/MPSxJHLgjoCAKGWVXHLcw9KoEUOKNYIRs4GqpNCyRoGmV1yEZc0UmaCyoMEyIaKJOXOkZsmkRlmWmtExlCHxUyJQuNmhgiLC6pPPUBQYQET6MGcHGjROkVshI1FyiyMlY8AWoVGRTTMUS66NmZdAWTaaYI0e29AFkl28jAwy8pMmZRG0cx+SqEwtJGgLMidthg514I1W21RVKR8hpVdwiRG5F0DMllT2jxC0xE0jPJwWJ3yAMDgAgIUbqY/0V9/prlvqdaIQn//RKlKyN1Wv9v/+v///7S/vZbWbbv6yXrSRFOFcSVymcOJYKDQWEYwZyKOFFAjDnCPh1kY3eHdTEUAACBqKQccVIECaQNbRRcCFLtBJmkIqiTwVrIiVAZKh4obbNKso4B9xOGNIVWSXUTDyeYqc0oI011zAeksdFTQKEmAQL6RJW2TEKaImWcJXiwX02HDQZWQYqqhIDKEhJRknNdslWCUGUAo3SJdRYgeKXkRdZugukRMUIVjYqFRk/0ckjaFO1FWB9o7KKpOm4kFaNxsFyBs6/aGkFHkkZkRFtVEE5EbNCpo/KTKJCpFtKljZM1zbTap5gh5Ck2ueWw0pjBx1BAIAOO3jP9Ef///+c/9PWqFYOeLb//pZ5v//2//////67U5NyJZFp1e9HVdclRxx3MpZsY1MVRzRqVl5BvNbbRaR+3hWIkJ4WVIgUAADrZA1FyMNxXgFxBQ0W8+wHkpE4qE8VCmfrpGICxxY0Y2xmS//u0ZBWBhmNxSvHpZHBBjfjFBCPOGNHHLcw9KoEKOGMUII75szaG3CtNkVkoyq2sgOqm1z8MUOycAOKCaBCyufbkTtgwKELC+1AwFkZE8WIljqE0FSV7D7q1Eyhoz91CEw3OSTLDT69IsTY3AcQiGQkxsfHQnjvaGIprDyjELr6V1ccGhSZiSu2L8TTN2oDCM8XGC96FtiIc0NCWPGTOlV6mNKTlt9O7KVTErgRJMdU0ZgPIUzpWpGcPfU9hfK6KIxSmjrXBcBBH+LluAv//5WTLN//B31//9nLJ//n//////mzssmeZjtDJFwEeEovYigNkzGCm+Y5sDFhgdC0S/sKhBBxQpBZsktMqplADuJXjp2dLSexKlHlmyYyOBeUB1R6POs/EmX5fUQnB1Y0MLM4gVOpgnO5karRMvFY0RI0j66gjFyBMnApguRqudZCsVPmkaIRCkeQHSVGsK3IDdJlRCam+WIJDpaaBBFuxShuTpjzIgamiajFkP8+uqRICUlRIF08neIh88m0hbXRNNIGjEUKqMfuInRxNuJWSJFtbG2uZZIyNknQmJIEBZSyInFBA20TWdVZMMxgUequUYsq9VXcKllF4bDZqkLWwEBxHP+bZ76N//8JTQVX8OXikBFkj9f/////////+kacUlrUUK/8tj4ZMf9+oBhxlE4JmHbOk0pwMTN4YHUcsowYVddksQEAL3SaAonLSff0vkmY158VfOC0BqSawVsAJCKg+TIkQ2MJmT5Y4WRihZQ6w//u0ZBgBtrhxSdsJY2BGjOiVFEPOVuHDLcexLgkjLqJAgI65XkWFL5IgaAOhWIGnvE5UZKXExkJKMzQ6MXufwBKSSe+PAVjo6elKHk8SWp4PkZIPLHhXPT2xXODkLU9WcfKpKChcORKQqBKO5itTHBbsSyVx5pWXEZMMV2tn7J+gkcuq06g2JRw8emGvjXGrOENIso0kM1PWuweKFx2IqNM+WtRurF6QtHQkll9ZFG+gl1tDPIiUke1iEzJdFdDlyrkCRZFtspyKySIBIAMyoFGFX92bDxioUpUW7f9P2J//N/IoYxGDiBbmBp//////////2L10sqL1b7vQ3b9lM6HdmGBJHMLVliBMIz247/2+dFaIZWUylAkZxg8cAyx1iIhCzjCuJiUJnotYG8WEx0wsC9fAFQE6Z0VMmiCTgOQMNJSs2wSxe4yvxxSSjKskR0PmWnOISZlOKUk0LkO6tyirBQkPkKU1lCLXLNEEiRhDaSeyViVzM2NnVzNxKNql0kayFd9DBp7JJhATypzCZpSJyMGVTo7PMYbjFSk3/JvVsgJLUJkllI4onNzQwk6DSKlo79mj8bXZZk3NzbLLUaZi2pJCm5ZiQBgmIZob+EiP7hLZu///8kN02r8uX8mciELOd//8bzKZSdrJf/////WBs5xB+5lx7CzhNu8cnrnvCMg86Umb/mchHesFfvVwptUuvSNAUMIEjv0kYWirMnUuhKHRwRsrMNhREj2HKTysfM0+pUuqfu0+/d2B20KX//u0ZBcD9mlwyaMPYpJG68iQHEOuGXHHJoThgUEVriHAUCPAtuXCenEoyOGlTBfPT89fUEdWoGp8qHp1a5dTUPWCW6oXO3iJBo8iMA1QziN19hlekSn52cunZfEloiGf8lTvp1awmozuHuSNpEbkA9HJioXLUhtJkuRkqlF58+hGzrS44QEhlGvQT46ohRXH4vuoiylZTEptCdP30o+L6tFQvmTN2Ds6cZLZ8rRnylGkW1dWJVuHbbZ9Bazqi6GrcPPiFALjE83xOiP71Sd//1RZuHaX6P/19F6X//9yKhCGkBBo5NCf/////6KVGOq51SQlXp4mGYqSrKaFBadDgrU1BIwaFqtG8gkM3faEqCVAQyVolabcCKN824Q1jKmj8KAFCUJZgMXGiuU18EKkyWMnMsLquw4w+dJnTJGaXiP1GRNHtYkapDFRWWUMTL1pOvyVxWZIZ0exL0IeBYfvXJh0Ym5xc4Q6QD3MJwrodRHJATNWeWjYvRmJodPrGLDokW3qSnale5a7mCsSBJM7WgPKRJElKIadpM2FRevShqSaGMRJWIS9bY4VEM62tCsfpy3ChORzZujLSk6QsUn9a8uTNzdDiTnd1zb61tk8UVZSnp4hx0ODPX7GUiTT+v//qxVlY7f//7fblxA4LMsmyhKNmlYNjav/////////7mZ4hSYD0b+PKtrKeUaXoYvbkpBkDTXW0d8U+p4a+f5gQBCQAisMislaxsSarmGFaUFE232ZK0Nob7lwNOMsrHJN//u0ZBKD9kdxydi4SGBBi0hwCAPSGcHFJIZhgUEZLOFAoBvBQiiZmQqWYePsoJY2hNrLHW9QmEupCTYmEjDBZgjFywqYE6IkE2orPFiokFBEcKoFWCFUJlEAOwiCVGSAlUmXL5INUjIS4IIRs0SKKoheCxhcneTiMZi6ArRFmjwNlCYLl3P5YyGmbOI9HFJolEm0BFQiTNW9cgRkrKr2sNkJ5qkCNGsjVk5wwWUWFSBxCICAeViqeb3TfTRg4ugPYsSE6FNY0rhxh9Pc62d////PkQmjfn/////YKcVSBQ6AkjAhbQnPQuvE0RCfQnppYhfqYlupndek60LFhSkzTIyDGdBwq6v0fWa5rqmTIXBdCr004crOKyMQBolp0iY7bpQvHlkehJPDkaRkwsMLtMtxrVy1KYWWI8PK2gRPquZQENJC7pssJJW86Lokj5QmaRh7MiwyYsMwLC/p6XFo7dVsztMa+5tGp8lJC+rNiIVjAyfUD+d1QjqN6i092Vx4sL53ZhaqT1MDg/L8RVbKTGCecEytW8ZCZAaOkO57Ik4hVlSfoy75yfnP0E9LsZwPWRtNPn3xLrJlryk4N84unKDaBCZMlBWM0EsktxWyiOW3pKzaTeMFgqhqN6elJ71///6nllJ0NUuzf//n23++4a5mKWbc5qEd5ydqm83ZRnesKYncGadBb8CPv9uxuu7evXf/cwr1FflN8c3V8d0kVneHUjkAALldJl2CEsLtOO/LDlijgZ+KLgbxCakkSRa+//u0ZBEB9ddxSnMMS4BByzhgCAJ2Wz3HIINhgUEOrqFAoBXZdRoHA0LkBtJStWkjptbW1eQIIoFnrolgY04uYILBph4rMEJVpclRWQ6ssKkShYiTPmllWqUEWPSFU+l00Eqs2dMFFoQMshUqhJ0DCxZ8mWlWsmsltuORU2Ii8E4zQHCsYo7QoBxGQsraYKoCJQgRKdgurqTIfchiZX6iiz00DdUgVd0KDE4zla7T6lKaG4TI1KZplDkVI2ggRAT/+zK/sac5jhS///UgotE2nVbf/9H7I6ruYxjEO6lZno2pWuvhdEqDDYBMDFiATli0OVQMp5wiMrhDmjkzLsBgbWmbICRQ9aGEiE4+N+zlCyAqRaKmyGZyKCqGnFg6x5N1Il7HFQyWH59EmUpkW4Ylak3Khghq18JVHvDREvKhXNwZJi/EUQ0Vn8Q/yX3TwoLw6HtesR0sRjkpHA5m5dIJEEQzL4TGBNECTJ4fyjDiMfwXMxs6vXlgRWztWnKZ4XFY8oRcjEFeIJ0eopVrl5BULsamEylxCK0SczfE5aoQDmDhDLjiehgSF8KOB2M+LUnR6f62OlTciKVzpyerF5npqek4nJXBPJYxJ50TTwyRtolP0SHiublLOiJg3HDLf+nq+Yvf//8VC2I46Mc48gREG//6FmPLZuyX7m2pWZVmQy39HWtBG6iLoRhok48SVw4JuSc9GV5RWUWFEQXSKjHbdGy7cSAUL5UAzBBdlS73dkKaKGi7YZuAZj2SKZhxZtrP//u0ZBGB9khwyKMMY3JDK8ggHAU+F+XBJWwxMckSLuDAIAj4OT91qiw/3oq0iOXSihqIz9TKmKN3lr2lovt88AZ20BmS0z9DuAE2iGxCnLKgvmZfipY6IbKbzYqVbEswKBeNlnJCy48U43GyKkehOGUSQpNnZSOYFZcQ39TEtbdo8WST3VDLZ0OqwklJ9qLFMQ4EnXj1EtcgQ3rU6IcrKk+ijDLiP1H41tFCuA/Roi2Uut8enqFGjRrWF7HHnYXXZx4uoadptwxfDhoEJ7L/dI5HYj///iQgIs5FDkYpEM///Irq3fTv1uq0U2qakffJdEUDHEhrK6u4mJSmHmOqS3cBVFRhgmpY51ZjOccXL9ZUYdpxwbDFVlmsPYoX4Q+kyqcp20d1WvRSGXWhqKhzWzR6hX99CWnEsGfRLkvwPLXPUMHq5Utxhui4qpVhJjX3ZI7zkbB3GpMaL1g1oatHEcKUZOWnVXD184KSE6xKVDJ2BQZjNl8CiNq5roozbvEIpIRphVixwNI8geCjnvcpaxY6s2Q/UCKBdyzJWZs6+Ri+ornMI295YVlnoGFRYnLGjRRNVFhYmxqCi5FzaBJU4Ve0xpo0jTJ5J4WtmSEN/1TZH///2a1GMaQ4Izf/1iGdEIUDRCORThGpWjbmRX7lbcO7M+oMEVgxGFlOEq4IggEIUWjHSosII1AAowGLccACog3WZYxqbBxrqHq/Q5hAYqB0fLeZZDqoscI9gkFATDmSh62atGi1bY1TOXifX0PV//u0ZBeB9pFxyCMPYUBBS7hABAhuGdXFIYwxL4EJK6CAUBVp7o9M0KjpTVpmLE+J5IsQ1L5GLSswMyklUG+EtazhEP21Wmg6DmkXHVB7KzPJXUr6Ve3xyhkY4OxBbJZqMsH84Q0b8ri0P9paOkRIVj5AmdXnw/n50fFMpFNYh8Y6rLmcXHkZ/VsHBK6KVjxOSl8uRIjJmjKdk0qJ4kpGnxDdxOV05yfF8mPIllUJ0kVjOC2oTK+MHj5tZda8ouWpeLsLqVE8zkv//8/XH9/X////+yJsMNdBaCB4mkxxlfVV/NvTzP0OqViY42gwucbQ5IQJxW73o0YMG2PJaWGQfYkD6VQOLxhBAbllgJgCXzYclTgLBZOtJ6Ur2fN5q2n0yV/12v1DTEMlrU699Inu86mpcvHLizVsNSQXYS2a1ceJyhGcIaQqMi5Mz5MXC444YbRJIlYkw8GQSGUQhIAQFJMwQA2YU5IpMPIgs4lIiMqixQnkwChkGA2RlV5JCkhQka6MZJSpWmqMG7IxLz9HBdfQQ06hXiILJyNRl9nCWkI7eBgXEUgHI2ToaVKNnYLFVWbmREsSxOaJhCRl1BSgIiISGVSQnPqLEyAqssQHTCiSAvyY1kFMAdw/+ouXXIgDjBIEf///X+3//6lDsxSCS1YribjyIbmM+iGRO/NarZ4kLI4mxCKZqmh4WRzioClEBhkFUmlJS6Bsaklro0RE3gFhzzCk+4dEjr+elYBucUXc1UwB4nMB8MNBo0phA2qc//u0ZBMB9jVwyCMMSwJDSxgQHAbUWcHHH2exM8D5rqEAcA9RctFUUp033YwfmowWTXWJj4+0qjRIIABXIlBWxEyRMilyMyRl7LUKWBknwA4YSLECMyCZcuZSxbki8YGBtoHxJBGVjMNLeQnIi4qHwYIgSLHV7XFU3Cc6IqgH8e0dVRz2Z2SJkuTlWw2KjTw2cM4kjWem8mjE5FUfKL9Y4bivEUFkKoqQoSFkWbIDDbRpkVyishYIRwgTfrYrZPH3IVqI4KiwFgeeee5hA8VlP///9nc8sx546n/Ptv9bCk/Ffy8ZDtma734/yIhp714iGz+DMmjECkAhFWIPWvE42S2v2QexN1AaEClSwYRLBgHaRqR+ytSlcR2oaWRbYJzMhpSq9rmex2YmW/PF0UDw/MvLqKI2TZxs6XY2fKEbzby9wcl49sqxyaLa4sPH7a0Yobpy0YJi6lVicfCXg9GFj3islNLr1hNgSmLvHR4XBAbLxMwhng1J1ntmBjUnglAggCQVD44GlEKmrTDKE8kSI0kUh4CmATe48wSIZj5GFZREMyW2okrQ+CAn1aSLyDCPYmDQWRuKPGvpOKHB8jQiImRJEqBplgsLoA+eLOGSYmiW5KKydeYhGGUUlzHHTnKf///+7Wopv/b//3787KSGDKkA74CGg6ud7ioddKuJEmAEZmLZBkDFhiaFsQgDEa1fFoW4wqoGaqtgQBTwxAL4J9IWCIoGIC1MK5UoFPC2nhkSC8fo4KuraHSyBcyy6nP4//u0ZBWB9oJwR9sPYaJAC7gwHAPUF/nFIYexLsDoLmAAcA9YX8WUteJWRlqEvXKEA4RHyg6Vqjk3LxgYrDIrnowXsrzmUo+ntylzTigt1agaXOqEi0xi46PmSUYJIU0QkKCNxcJK3YFzZBqtxDPzoz8uRkFpKWjEn1PTNeV1ajT6yNtcvPzS5cTm6yWFcD6xJXTwkzdiOBYVDvrHRSWlmYHmiMxAuXKxyIRZRQLV7ZzczVn60uSsHbRGOXoiA4mfUvmpmeFsjL7iyATFjxqKigvHjY8tXMb9bGN//szr//f/8vn/AxtzVMGQKwSxMyFCPWlFRByBoV6VJ6eYEaOVrw1UKwrpBRJqokpzRypSAf1xClyCTYFchJLhaUuMnpcpkmULUejQLDJkhnXomVR4WTn0QmFRX05ZhdAwcQqMrk9ilF4lXoQXRkiNaRdtlZg01sAytCjJoBxSqhMkCBQo5iLaG9iwiJJtV6MzMyRHUC5RYiSTwuRmUB082hFKqMaWqMlzBkUrOTHfImNNiFuBZlQ+Ttz5Mk+bmEee2AsrBZjHM2grCEiVrBSdguPkKObSxVEcE5EWYPEyIh4be4M2gNEWmlVihOyTIVY9xCYXLsXYpmlAsKCBDqKlLFS7f/9f62cCM8MmZkC3+GKDU4P4kiDI4NqQgvE0kM0+GFmrmbb+GCwt2K1ArnhId1S0RHcdYxf9obiVZc9Kw8JjTEWXAuqKBCiwjFQrKkra0FyZlh2pWiC5VT0WxMUjJ8FhoPPH//u0ZB6D9oVxxyMJY/A66XgAIAKoW43DGQeZhEj1mGAAsBqhzxZ6AZBwDi5qCgkGpkIJKHYpNE4tlZCMz17FLp0XraZmSqxiyxCwsPDpcW3iKWDXF5SP4hpPE7pTeoVmHJOH0GkZwIRidYZJU7LKw8JKaI5ND0c3BCRQRvNYZwumXsIljKJGJPHJfgbKC1YnVV1zDyA6OC2hMNpS+lW39hoiqIXz1gtJKPnqZM6p9CeXKZJkCo6dqJDgAkAWK5VOxvDXmii16ilDFJrYGQUVXcpyoEKT///DOjpl6s2peWhtzFczmBTdM4WN208aU4FPd+fSBfgVaNAM240Dr0clA0F8UoBDNBQOZ6QTU8LBusuoQkNMV+bLRimiSEDzlDKU4suRgOpy4UgGH45DoIb6s0oUTMdVxyHogj4tVF87aMWn7JABheQy2oHZGIFy4WkN4xCmAfC+VR7EMTj9HJaGk7WOnJXJApIxujunuOx0fQQq0tBeQCwTCkPYOo4zIsny87WqUVBtRebBcuXv2JTjGrEdiueplw8HhePzxw7J8pyT6AHR/vkAvJboRaO6WHTmC8tWCQ4++2S4zESD48kEjo8L6tMevk0tkg0fQigvLPNwJ4Jj4Yx5FQv7zRKnvfb+5//mYmjh8FotlBTrYAkEaAHCMBDoCn/6MPLSF1gDfv1f8f91tTzP15zbu1dqCSbhaMAASjCwFQiMTpHbp6GCsHRgP5F4CRZTJh1Kq0e6xEm5XbPHrmHvpXUyZ6kAjnp0//u0ZBmB9p5xxtmYYBA5yOgAJAaoGSXHGow9gwDqI1/AkBqhsJrI+XNlyCdJFTbQdrC0OYlOE0vuHw9XOIGi0XifgscL+xGZWPjtlcdrC54fH0Ck/OC79V3sHZ5c8XFotkwvPNqFCZk6cOcQ0tk54e1N0ZKWmcK9c/hKRIRsW5KTNl3ofLRDchdVNfYkFc9PGnUImF5lOvVNHqpcVV9D51SXIyytLHJT9GWTiKt1pJPa3eYKiTTw6bOVBeWnrKqrikqLWnPxAJS0qPaf6X/tkljb0pKbsvX/7bEzHyHfo49QedjTjwnCcooDy5i1lsbp3KbwfVIYC8yImHIj/q6w23G2RE7QiK0EYwlDDI6U5j1He+bB+uDMBgxHYRjxacuODthkZPXWoa5DPENY61jdE52tf5TV9DhLpwWVhgiZKiIqkQsHJjQ6N2ztmCpabQi/rhVZRlodm4ViXjk+2yQTqrXHEJcsHlMhxJUi1zEEHD9eudX8fr2HI4F6QS1qds9fPDtNC7k0fT0Xn529k3PVS07S0xZAobLSV5FfIeQHdO2lcpDM4NKFR9Q2lbXRsTLilVYr3bgXR1h1K2TliE4+JK+Be5V5pS1woICESI9ICfRkTtT2E1FkKFmcpfxvP//W5KPba9Szub63l44MW3Os7XRA0Mnx8Z9x4LIJFUSi4NhK61UItKJEFoXgOSqCXwRbgeKXYWZJlIXsh6sTJuiABgI4NlxUsSHziAlKyOwjENkzBshJ2CZhQTqkxK0jbTKH//u0ZB6D9mlwxiGPSGJDaFfgPCOOGanBGIelkQjvi6FAYQ5IsChOTnhtgHyZCDxsECdYnPI9YCooPBshI13ig4cLaKAGGWFCBgkErIfWCqwgUFbB0kEUCIKCAuLYKR8TnRAId20ANzYFIS2NYHlh5CukKwTF6xYuIC05CkixQnIR09CSRIrbZq1YzkJyBwhIgs0gRoCMamvE4UJnWQGwoP07AfULa4QtJsNkLLp7NAh8tXWFsp4FxmYNPy1cW46x8YvTNnrBLtIkMZsskNjL/r9////8GPOMJPXdjlavU466u8PYhEdrDMR7DWhMYKnP///f/jvpIPBLTjRKfSgZJ0khcxnCOpBuV5OzofGGXAfsZSrKKcaCqFRESE52sVUITMCQiIdKkFMOhElTJWRI0cEN1uI/LBk+NzyRLPDg+jOFH1dUMrDIeyMfn8MB0rcKzDZ36ZGkE6pquu+c29s0L4ijqlLSTDyqvnFh6+PawzEA8H1KS0h5dI+go0AnSdLmFKUzejIZTiaULz1aeLG3jwzRkM9fmBfLFVVfXrz56JhOVuWP3WGCuFB8jKlamlkJbylSmYQjQmvnxDerQzOVCk6OjnA8QTBlzIvYcwdMWXUvdKhi3sE8OccWLrKdOjBzOCv/9uMtX+l9WxZFhk/3f///ggPHvNSDBgoCKSd4hFDqDaTjKMAEYwawp9FqHbfBp8Avg+i031ZQu8pDQR6IYgFtIBckNjwWiSoQyovWMtpi8hUUHKQ7WHCE0dliE6cU//u0ZB+B5tlxxdk4YFA/gxhiGCMwGtnDFQY9gEESCaCAkwyYJXE9T9ETnobIkCItMMtFsklQxofvqTHFRKOyVQ8TkJCHEei++eJF/CMerakcyWjSdxFmpJfunSn6cfmlXkhqnKjY5L45rB7RllRVm54scV0ZK6+pvcuJXsLp+7Kg5Vi9gSx+MC/lyaWuJXnh1UpxK+Qkx4tHsrxNsMGUFD1gG7KMvHpkymiPlAlHkCVesOj0el5WLSAgqjU4Rnb+gAYhAmhjEjQSsVUHuDzGB2BUtMv6CcGA9JdDWjO6j/+rUVFZFFFn3v8p9+5tmhTv/y0YfUCykgJwnUAlGjYweEqqgANF0SQ/QFwugRBEuG1gdNBZHEQCSXDM6EcWOnKk7MS7yhdVY7ykjnC09OzsQ2Y8KhZIYmEYzJAqJxbU++OJkWie54y8uFhiis6PkM35AcMozwrS4WKkASgaGRJPjFF6EIqzycmsuQS+8oK5gvJqCOBEbJxuU1BS2E5Q8eHI7NC2XztbJ0eUTqtUulQ+EmOsl4mVUkeATYYsL6tsxEgeSDkJv2XcPyyZQlpolnCQjKoSstklnxeJC77J6NagFc/Plh8qNDZcZr3CcnLaQmGKxYkQ0L0LNA/AbXGhqSp4QKPWbOpFxoRrHY2EH1FMcSS3/+rERz/9zl8kUUxqskeidLmSELMRmTrgiSidY4IWxofLGgMLliYgcpUJJuwoYAASQEifBNyGnWfSTUhpjvGQpQHU0TXPw88EgE4EoKIk//u0ZBKBhedxRljPSGBFAkgAMEMwGHnBFoexLQj/iN/ckI2QiYmJjKMjEQmYTcFCYRp0RsOwLFiGJGepI8SmVSxhbTlISqIb5P4ELKSKCZdJnSj+ZgQRTFbUz6ZGUghbfWESqq7ZDBEnNhoo22mjDN1ij0GKlEBaXRIGEIkNJ0uD5ycspYkssmPpxscD6ai7NVkVEagtBDAE9Ycq0Xxc6ZInokaKTSrMzx05pI03aGoMoFb3BsltJb6QdWRPMxUVywbLFQKdBUY0Ndpl61QEZqwUSODh14etYRv////+NsPda7SwO1NlXtGKHMI9JYSJHKHPPHmD3DSICedQNrOiKxoSbUSIVanAIjrA6swuQWB/E9bCzMgcZYwpCUoPHQkPD2rMkh4vaJiVLVaQFyMyiefI7QCCTKx1R5NF+GEJYdEdoRksbmByKZK9s6hXNDpQWRCsyieiJFKPzg4Sl4uHSYhKlzSEdKMoiJMuiImyxIYE+ITcT0u3JRgUnUg8aSXfaJEKozjB4aPcvz8pgiwThqVHRgqYcRURFdpFw+LXVntSQI0wnzvMIyxQVkMxK25ksgDyE+1kCjZ6cCkWJSnI0YGUJjSpAAYmAD/BGAeihPxGgXFKAnexKQJ1TmIW4IGCgENnxiPf/0s7k0Nbu/NfLMb24bUieK1ka2VP91yhdFStSLNI1QhKQACRTQ026g6oPCcGdTRD/UDMnU4LCPNaWTZabl1KuP3XJYPSCdIUBwoQoWimWUy8eaIRgcqD8fDo//u0ZB2DBsZxxEMPYUAxwEgaGMIAHBXHDqw9gkEDmp8IgQ7gw5etSkYpD+PZBekxPjyj5FPoz8mrWnUVTZ8gpiCsOU9PKihcoPxyWIxbZDWVMFa4/w7scvt3LRCyGyAfsmcZaXrgbAzH4aVRVTmw+r0BbQXnkbKGfs8biTcDZAKrhcjhEg+Xp36FPSITndMrEBaLzypORHPFQ34picO9Vi9cXzyI7O3JIhXKx8cBy66PFBn6AVVRgYrfNYg8aMWkKLgFB6AFACbBRo6gYFzhUBR5W9hZ9qCHZ1/3FOhP8gSXXWvb8NK70dB4axq0VJrWi6SUkBEXckJHJyYIMbwtbxRQwfIq4qyODSENZCSCoc0JkJHg5KxTYP4RpL8CEdHQ6jidCWcIRwISokn6axCZiPoRyUviWpZLZ6wOKu0Q7H5gfjQ2PS3oSShmIhH4nGjB4IpSicJhyI6gySmDp8jTAdPYnUI5KT6k0EhIoLTSGJwTjhYjD5J6hjMQ3DMuWMTMsokRc5k/hjP3z4JFP1LPjkMkpw8NThJYQimKuM/48HRcpLy42Tjq+VxBiKsJ+cie4QSGWzAilseQbExaenxXQySOhiYYUzJpIXyCfMl9pZCcwnTvgwCBAoEEl0IJlS7iK29pzCJFVPxXeIJaBGumuX6//wF0YUchwShi1L//riAVQmNgENKEwFO6xXu/7dFev9/+/+sFqZAATyRC8XwtUMxiHGWX08CpDwRH4gFNg2yxPOKpzGGG7Q4cEKxdETj5//u0ZBWDxeVuxMHsSsI7QnfTJQlYGr3HDweliADEiSAMkwxoAySiMqTxiQHmESw8GiCRMfLkwyVeqREpkmikCEiRzTZCjSIFyMkJJCOSSBwqRBsZpq2BtIznlKYUEs2ECxOxJQbHml3CfQ+aGYoCyTe0jUtZpfDJjlycwF8QPZuSJjN6JhyYwjm4jFlkWziHkhDOOlgQaTG0Qyn8UX1o0H1UShAUhNBdYrCcJMJzWuazZxGhFqASgBxohCzSfVNGiUwMojoACFDdhdKdZdlemsqdWJx93LvmGo/OOhtS/6zmgHs5Mav83MnG+m2nWqtqlAqMAH+LcA2oMcQbpPTITQtxoZcREFaDMDMXiMHiGDV1sEj0+Bs9AdpS8dHlYBAVLy46gntS+dGQim4rJDRChUERbo0Gbq1xwvqR7bOT7TtFAfI40r2rzoSkioRTBn0RRFZ8yvk4RONIzqMrawwXSQ7JuViuXYCSCSM9V2IpWLJ6cNMEBJRppWJUKEZCQew92JSwrPSQjfudsnY/LTorUMLnDl3ll0q2koh2KahxHEXCYffxURnpwfVQiIckwpmZuqKp9c5PzN9o4WY+cC4/WLJYdfWGsFAD0jUiqIlDYJlqclU+AOAdBIKrgNwIjC9mGDAEpYf9f1jVvJXt//bFZXb//zO2r/qcERYAAFUGBgay3kZ2sv4nBMtdbnHaJ/4y4qPNLpwBwyK4hxlIIDM4HQSW2D0qLtHGWC0lsaLSshoThaQVQ86cwlUrRGKGeFkg//u0ZCQD9tJwQ8MMS/BDoxegPYYcGbXDDqexMckAlJ8A8A9AMrg6ZsOtV0UKU1TEIGR4LsxZIx4SCU8WRiVkqwApKDaIiI1l2DEDStFUZYjDC4ibUJXmkUlYPbOCpMKBs+E5EQiAZcVC5MwUJlCxO0TixCE1EQIoDYrhCMBOwrEsrawiQhAGng0DR+idw2gRoBXh4DyEGR8ZaaGhQjRNqH0YcHDw0uKEKogDAiWJBWKhlcQoSWePhTgxjiDt9YgNnLdd+E9EEnB4rP0TEFsMJtZzWeCHcAn6SR2cScz/+7GCF7v7v5///LKNmiaQkB97RVQhAQGDpdZkBjcH+nQHE5RuqM7jxPU5EYzoaojvUBYVHHer4pJ5dKsBbJJZaKVztCpEaFwfiTYOTg8UxNE2MlxHKALmufPVZuND5LTpDNagpjK6k6qlZaOaUC1M2pgJJ0heSh6aXu1F6ScHAnjUnhliAXQEAlPojcdQsCRsQiNGiRpI2ZEx4nVI2hUkWkyudLE6kkB8MEZsSSFjJMRnh6RcUoZ8ESM0FzhM2jMkaykSZkHTrI8qTPHlEJK2XOrDIfk87A+4maREbRAKsiJpJLqDcSpBScpzAMQHse8ELQqcGgiCUHarlQr5okkW1oVJItKpXFc/UC9zccfpVwViZVxGeuv//3k0nbLwOX//6IX5p7+r7vt/XRkWQADrMxa0DDikm1KGdu5UXNDeMFOrEWBPrg2VlQLB4EziEUDB0oNnB+DkCwgLSiQg4jehJhUN//u0ZBwD5pFww8MJZGI8YvfzPQZoGfnHDAexK8EDJB6Awo54oScVDDaElOwmgeBZsKwnGlRssPrqFQ4To0MwqyYXgPRMhQkKpkXTREVEq9ijyPomLsrjklF9plGZri2yUislTq9hqsalorKi2HJaWrHj6jaU1RNdidcvHo7SrCgJCI9jEgwWOIEUFkcB4rfIY+vHsTp0lSFVCWKFxfYaqdoJw9bEy6AyJtnyq7A0odP1R8U7tRuo3FR0mOWnxwAAEBWEOTA6lcmlztJMrLBevfsCpUWcgmSgmmf3ZQYHbRIQdLICk/WfRiw9YDnP/9spFmeqEpuNb//W7tDHRgZhMxZClP8p3p0pROpETyQOhLTRMFMRD8vjqiOErRiJyh4zckfD1wmldK+sYSi1gKG3jZEvJohbAtURDZzU0QpHw8fBASrSETxCYWIUB4jE5FA4VcFgprdTEAxshIMCVY4w5Vl5A2mcBJMOFkIanaEIAKIUYrPEi1gaRnwojRCGgqsLiGTOwVIxMK5IhGKwsxAKMtJCxaA/EKLkQBBDbmxnqNQLiEDcGQOMFhOThUiZHdJAcOozQqIGxcmIjAuWRzIicZEaA6yjMoFnI26B8sCWehKZHW9Xth/88SGdmdx6hDPuIhh4HHsIlMHg8yGN/qLgcRCQfEKh5v+pZv/////zB6xscNIhvssV5ZWpBgAGAABXhJi7EmJkaiEIzHBGNl4xGkG4LgiwPQlD06BVQ4DZVQSiU0h6+YiQh2UHS1JsFXjM//u0ZBqDZkpvQ0HpYSI8SNfKJCV6GL3DDKexNcj+IZ5AwBXR4OIlQjmF2hxYOHDohE8gjyY1ZUFuBeqOVtysV+MSr6wxdJRaXlM4QTiN5WIS8uJZLkTDSah0nP/O1iw6XmBmW3jE/Uqz+PFxI1I3xLDq6t+E4Eogo8M8Mk86cngknjlltzlCqg3sgtP0P1xkVHh0aqdHN79eEsH5/l1cUKw4qe7G5ylXXGoscP0FlZj+feKjFvXF+UAIBAIAQBIhpQUhcsz89/KpAysjneX+IZL5WP/qdDB4eJgUOs3/2f//+d/01fyvdFqiN1Ooox0OOcf//oBwWI/ilJ+GzFOI8GdWEpOXBK0siSQpGkJQPmc72JTKhvbFYOJ2wfpD85P2NPiHYuNMJSZsb1Bx1t0zMkBaPZoWDhKnJR0R3EpKQ/hdUQqLI7r1r7ayiGdc9dstKHC+6eCGdO5MRkeLE6u6E1C7hWePzBDMbr16upOjXHScalK0tHQ2SSIkmg2+A+1pskgJQQZRdYlxgiVeN8SkaXxZF8soFZitYuKkXZegJl9JDykoKqW41IUIEC6iGTrOoSBktFdYhwhP2288FACDaI+6Z2cK6NMSoOikMR///1HmEB4ucSQRIPZTLWytf//VxAxwEKbcROcYOYaqNM7kUtVeZzGUYhComUoABgAAdNYpESTEZYlWrUvI9V7kjjHNCUQ8sheDoDhUjCwmUQJEGFyISikSDFkqy4tZhZMVkBMzAIiJISsLtE5IZDa5ZLCw//u0ZCML9npxwsMPSNA8h3dgPAKCWyHDCAfhgMkGHp2A8wkR0tsVkc1FQyQNEIiE9silENTGjkUK2D48Qhs6UUMLFmZTPQVEpGQETQhBYqKGxWmBYgaIk1BpA1SAqaaNB/VBMk0SFxOkCQlJ9YlZcIIIKAiRiNCWBFsFhM88KCbSULzqZc0es0JUpJkSUjCNAKWT5OSKhYNiiRdefLllnthgZJkROESNpChA6euN1KBvoeJrIQNTIRr4fM14eGTO/3FGOqf//ykcUIlXQzqZdSkl//7GEIpzlV7PMKRVcKGmHlk3Cj2QpvhQVAhotdtlBJTPJ1qYqACQsWjUQz0UG56JIOEoTBWfoBeRjWNFWEF0L/I50PRIWhyfE+akIGz5iH49D0IB2eLxoJykRyMwOqgwgOFZJYH4/dK5INjImqTNU6+mcOnTLCSeIZqYIFrR6dF9fGibi85Mmyk6sGmi4nCUS8Bwjozxafp2DsxKZ0TzRmJp8kHi94mkaGBlMyXJRIJL5CshqlfQKROhJLXL1TaV7CSYyjOX3i0OZwZqlTJ9AsEt4wOVwfF5GkLpIXKW3jCMglWi0sH+pi6TCsX1bB5AgVCwLQ0K8v62OATJPpueBA1C1GfU75znfgrMv//4cS5GUzn5b2VHdbv6n2OdQ71yoS6ZiornBO5jHgl3P6zDd7TsagWyBByPSVC4F3IOHYXFHGiLAYHiwFni9x4HYkVfJp7G08oLhNwpny8Z2Hkkp0rZiS2h1qjHI2PikkJ1//u0ZB2D9qZwwgHvYEJEJidQPCmmGTXFCqY9gQERmt1A8A7Ix7OBBOThaN4iArdPC5YsY8vK68eA8QyqcLH2zIpPSVh8PzFM0SRwFhyJJ7e/kgqDqteWKEE9xYvHcnrThMPR8vMH0XiMvOTdWPhTeTPwlBUVjJ52OGSQXD91GOixeUoeJBGSPng+uGJPobY4OSJkw+p2rOfZOztXFd86cLRkjKulwuXUslXVR6cvXLxidRoOrFh4kWt1lhE+W+EPV0KOypwrzSjbmdavBzn/9rcmV7OxW8tf//0Yide8ptfo/kFIFx9JZeeQQpx5CbQIlYkU3g0+Fwamj0P1PdPJNNoUDYyCYFObatUqlPYUshoOcNkQB57lZgrKwNiYegdniVtyUXHyu2NUFDOASCA+eXH4sl83QRKJr5fOF6phaYnaCQLGKdITVqdHA0qjqaxlJQZl2JEhzAsOKMNvv4Znx2e2LCku5AXKsq/ZZWomKMKfWlNauUpmlpMgKy5iU5cV1jWnjzpmdnmWVtqzQbahNq21q1a0e+Zs75eEpJaiJaVi4eOPmkDC6hwechmh+77qVQfPGMSOqdWh00pNnELR4rjLOy/U6LdTNTiplNhUEQIeYC+kIYYAQdgh0hqxtcmSTWM4XOuc1GPev/n/7NldpsYu0YGQZgaDOBBGMcoMim8eh0OgBCzQQId5sKAht6sjARkAAB6DnBmBmlWthIQgiETF9JGwhpjjVpUyQUUqFEkW9cMyEvzsiPXrg9O5kCoM//u0ZBgDhnhxwsHpZeA3wbejPYIaGkHDBqw9isjQgqFoIQwIliMLFSMUICgKiQZcPPFOhXBS0ApCH3RFSBQNFRELKjU9miZBQM4C9rnbI2DTLIVTXCsXECNxjA+xBESk0yBG8QPeyRLk6gSXTm2achQIMWRtTMBpG5tcfobpbmrP3D4qfJdKsUu1hJERkUz1LAvX9jjp4qhRJUiP7jymo5U6fsbsoiScFWknspoTAkF1TZQVU77LxivPjxecc5Q6VqGNRCABcOdSDOO1LkI7mdPpr5fMRpuf6OJBzXf/1eLOZYIVra1650oHnElAsUD4ZQ//RpWb/q+6haTZuGusgPLVcGtKLERTlVhSMTfby5nnITl24WnJiafOcyQR7PBcmLA5rVIiEgRCMEqcQVmPtlRBkDo2L5ED5k9VqWQ/Do7HdUJyEfk6gNlt7Ilok1HgpEy58uf8m0iRlYrLXp0lqqn7nqVksHhWXtK4SHSE6egeu67C5dR4+HUZv6U7jEsvsrFvoQ7S8XobKvJgmwmyEYrYzjUhNPWzkxLMJ0qe0kj2kWq6qT06QvJzEnNIo/hZqrLChamqcQr+WLyqcOrUqRiHC9zqNedFI+aUvlgCBBbaliAMcAqCogEwQeZgnMkUEGyH/l1vIE/9EcIXl79DK/7q2hfMhtko0dq7YuFieKoWAEAIEGioBQuZFdOZhy+FdhzhgSJzS1ycCGp1FPtNp7DAP6GlnMyDCNpWVmVzpEryefMB8H6uWKGmFwxQG5cp//u0ZB+Cho1xQUMPYvA4IWgjJMMkG43C/Kw9j8jwnaAo8who+AVRKZYJIqJQ8oRmUly5xpCVhKUitZhaQlQ9HkRVLpkvCZSTkb5UUrC7DAYmJcb1InTNpFu+VT67iEiT1PfMr0iqfnGbSqHVa4cmmHlF2lnHoPSq1lmi8ycvtpzg+ryd9I3jD6n9aZWwLjnXYGz2Btg/MTmJEplUvpU5cjWwJ7KnkStO6btbR8/Uq35foVXizHFgECJChOJweAyAHeJk1KzlIF8oYWNJcBLWc+H9lQ/ez+w88+6htjGWxAAHP9ddzDr//mq3D7HNArQBAgsmgpiGeMJFcI9i0kGWZvum+2jewC/iypg7GVtAA4agsYejVOV8n55Hhyvj5M58o04b4+WM53M/XNkQ1iRx7OY3j6apWMgbEOVHK5JwkiyLUWgkDEiHjMQtvWmANDMQS8dPqx6OFSUhQI3pZQyEhMnKxC58+4xJLvJDYS1tojaNQ4jOMfBrJy4fHRyeoY+DyfUVVs8ZH1us/EuOjsdYThBLpaP7NPUO0yRDW29ZAnOC0Jo6wMLjpQsXqFtNfVukJ11kSsTxnVVsK1tXhpbuTMU9pUsWmHKrQHOrgaAAAAKmibkmWBQhPZ50ETCR70/qXoyubXtpS3/r3iS7U//6D6Cetvzz9Fc+wOdIMJpIlySDy1o//0kqqkAZ4GCBOwkGoG3jCWaLwUVZkhS6dDA800tvZVOPMGSzkxQtQn8wmChyIYFU2MQfR0M7SrEghbCW//u0ZBwN9mRxPwsvTHBISKfwPAOCGIHC/Eyk3okLpCAAcIn5BiSLGi0s3EJaC/tEFbZUIqe6vhGkuxludI7iEflN7EJseq2jkbb+M2NrU5WclSrpbxYWVULkKBMOOiW2A2SqkMS8myXDhUhQtG1myaB5ZMqrQwakRwlzK9y0o2+7YWIm5RNkLK0jJjUBMgaxZ0ULSjHaQoaWiyhd2WiZFVsIhpcmvoVp+UCQyqXSQrI8VaVS1NC7WKIAM9ZcFK4wt4swuCujQYtvXXrle/sZSat70q9ydTi4bJLPnP//NCOZcPLzLWXrEV7Cc38OPNUlLY5JxwxhGiYUdl+X5zhYIkIBWMh4dxiXZYXTDSUYqMCjIi7meu51i7hlQGX23vsJlR1usPS9/aVeDjLma0yWUNeJCHVpZZaZ5HcGrVYxTum919cU8n/PUMul79TsE2JaxzUOKfmoW/yZk28FLYbrJMJgZF5AjEQk67BdqouRqddg6ZVA/WYVJsqS3BIQNUywT6bRaDEjS6IxqtVJEhiv5JzaDIaNNN0UOqwU1CwuSKtPOB1EOu2IIS3+JlFXE7WSk098YFqf+/R6OXwEn0abdfPCzCIwZBhgiFT0r/X/5hO/5G1+jJSI5MSIhkbErrtpVxrq9KIRiavmVWWyOxjbQdVZVm0xLOR/B0sGtfl+ZvdG7f+tH87LNQKGIZAuihEdTA8GSrJgJZEywjLlLPn/SEsslZvFFeNMQldocS9PFPQZS6VxC2VKPwoBDUMJ+wqN//u0ZBwItmdwPgMvS/JFiRgoHCKuWPnC+qy9MckNgmBAZAARUsLHct7AmVtdiMLkdBamenZgqVOZtshdnMJwcUNikCuQhTJWRBR5udNqrLiMuOxWMCoPQXICZZAiAuDnsbJ5tCeZQxBZQtPCykmkU9IOkfgtkBslvWFm2IIou7UkRZZDbE5MvSiXFKNmiSWwq0zhEvFmmGhtYvrEm4LrrHNwmjRo0TUebMLUvbbn2MNJGO3M7ODisioAQAgAFAegWUsaUPs/Rue+f83KBcBBI2RHuMXrrRTc+S5jxxm28p8QXzjYoqYWlcBmafLKySRcfj3THa/sxDnPw6bqh3rhRAe3Q/PFxdxq4IpCNROtiqR1lF1EWDl8v8oZLomrTgdxzF/TcMty4HMyRlVOgnod20eT5bEhYV88qMEjGq4BA2IU5acXFvDaiGDaOUF0kwaLC/EdTbAoLGJIuoLaTTEZ28cpmmqmUTIq1Yk298dJFQOLpzgSGRmMz6bUaN6VaC715PNs9JnH6bqHdNpi7hU9WXk2+couNpyQ9hzKV+NJzq7pmc21ka6iGpp820pYkWMHWThezqSBqTowXUlBiKZjHxThJWB+bYAEgFY62K/yC/yvZfOvzzepJXznQMZcpv/7muo4u7GXrraa/zaqf7QJB8Ws95vql7h88llafVcmwet0T9Ap8nL6eZ/VRzBQI3qwEu2NOIxB2z5pztmT3nHqY/DQVCc1hjNcRX3qaETVhbclWyqsuE5pzjke1OlSIlVp//u0ZBsPBmFwvgMvTHJDpjgYGCI8WI2+/Ay9L8kck2EIYJoIlvenEsJ1ehBxRC3FovKxIh/yCcyxgwYLGqTulYgO0xdlnRc3slV0TFrNxihP8sjTM4SO9ti2Q7NNCHjBXlAuqhcpGADswkiZw6Tnjzc1UZIuQI1xeUSLyQEpTNRETXqKNthyBhSqRNrVFmBO51eLORbhZwixy59joFk4o29SYaWB51WZaWRpQR2ryrDBVHNN9jtvJk5zgAlKpGiVOafdhbJnhCBegOhYI/Xcj9mrfWt8zeSjJv/rV81Wpipp1khnu1/3FQ0/y2XvRNdO/+k/33d/XYWFfP05y/QsApYdomY4isIrxrSuEE9O6rP3DjkvVXnRoFsOdtQxSle2ODIdqvUaQLZBQ8d0U9Rnwl5zT7AhhK56pc0zdVipOkFIUIBG+Th3vmsN4thBULOxNwBI0mbxKzTIwhB6oeaAMqGDBciUxRILsGDDTSgfP4oVigLn1T6pUnRzGZrkiYITTSe0uvOBLKcWES0mtiiTQwjKMeo275aqu9C76goPRuGXBZ9P2oSzEr9/4g2kDKU2Dkow6+6pnay/LMfJfIRXmVSLUMAIWghEZf9rMj2T2yCrKSN52xoDXjlENhW3cPbZ+0WeTrn8u0J1A9fhBB2kmBCZNCvstL31oBc5cvZfVoTktRC0mq+K7HJVLTc0t8caaIDK1zqolt0q13tvAcchpc6a7RUE4qOD4BW+oyWnXZA5bY1kk2B0gMsU7CoBBGGL//u0ZBsABY08xu1jIAp0R1hxp7wAE21jP7j3gAHJI2l/GNAArSWIuhMRIx91K5FO5yR1LLzKXtu19c9tO5usNKMLkJjFSiRzEQEG4CK7eSBebxKZvRADFb7MH0lbtKeltPfiap5W0B/5RN4cnOW88a9RYfdA/bvtzhpOeSQBdbhCKWYjcP9n5+zJpYxCP0cAhhXuE2H3f//mP2Hh4eP3moBzmWh7PqBNEhqxQH4WBkFsQK25o8YZUJrbezta3I4QNboqDzZz8EkLqHWplLHQ+OyOmBgmgQ15YrEZSfEtAxnIPW9ZKQznhZjUlhw4ThM78SJMztU3+b+IwQ7XVnvfxLw8N8aJj3p64niOAAJm//9ly9HZEI1HJJGZBIJBI5BEGg4n2CtGexMzcTQL8eCu7jckkBiZn+BYhz/MO62hZqHNBs+gqBsLRtnfZri1XOAaaYPo0BhQcQmKC8teBiOSQ5FehauJZFrj4xStNXj9HrlXt7ZZ9Gg+us1/pqJ/uIq2Bta4c7///+v+6a9P77jxHOBSXGLMn+a//H/39//dNb/24QZI00dmf3o8msIlgrqhXhFiHiGg1VW3m4DAsFISA2WxJMysWoF5/Sa1uAIASh0fQDeJpiXXWYaCNJnTL5qX3Vpp0CWIxMNSnUkkjd0GkwchSJM7//p1a1//9NkHdanRpJLRR/+7G6aKzRBRc1z38B5cPk2RUFZ3//6AfDH/+mHVBhUhCoIilksAYDTgsBoNNWqzGSRUVX/hmb4Zq2vl//u0RA8P8/JlvI8lAACBDQdg5KAAQAABpAAAACAAADSAAAAEVVmkkVNKBUAsaUDYPjpJDkGtioNRVYZm2Zr5Vf////9V5Wvho/hrlVtVXhr9fa5JFWlVWmFhamFmvVVuVVagoWWGFhZpDkGtkiorUMzcMzXJIq0qq1BQtRQsdhoROf+Iv4oULNBYEnEQJCpq1UMblVWtmgWFmlYKFjYZm1FRVpJFRU0oFQCxohA2BsdJIqK2SKiptMLC1FCws2qraqq8M3DN/+q1/K81///////s1ezXJIq0qq1AsLUULC1yq16rXxcM2qwULUULWq0ULLszXIqKuKh6bDWSKsCmwgrgUdFBlUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV diff --git a/src/features/ark.js b/src/features/ark.js index 22ba5fd..dbc97f6 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1514,7 +1514,16 @@ export function arkFeature(ctx) { // arkoor send and publishes a receipt referencing the delivered vtxo. // Kinds are provisional until a NIP lands. const ARK_INFO_KIND = 10037; // replaceable: ["ark", <address>], ["network", <net>] - const ARK_ZAP_KIND = 9737; // receipt: ["p", pk], ["e", note?], ["amount", sats], ["vtxo", id], ["network", net] + const ARK_ZAP_KIND = 9737; // receipt: ["p", pk], ["e", note?], ["amount", sats], ["vtxo", id], ["network", net], ["P", sender?] + // The receipt is signed by the wallet key, which has no profile. When the + // wallet is linked to a nostr login, name that identity as the sender (the + // same "P" tag NIP-57 receipts use) so the zap reads as the person, not + // as an npub nobody recognizes. + const zapSenderTag = () => { + const id = ctx.hook('nostrLoginIdentity'); + const me = wallet.nostrPubkey && wallet.nostrPubkey(); + return id && id.pubkey && id.pubkey !== me ? [['P', id.pubkey]] : []; + }; // npub decode without importing the nostr stack (keeps ark-only builds lean) function npubToHex(s) { @@ -1671,7 +1680,7 @@ export function arkFeature(ctx) { kind: ARK_ZAP_KIND, content: (z.comment || '').slice(0, 280), tags: [['p', z.pk], ...(z.eventId ? [['e', z.eventId]] : []), - ['amount', String(sats)], ['vtxo', vtxoId], ['network', getNetwork()]], + ['amount', String(sats)], ['vtxo', vtxoId], ['network', getNetwork()], ...zapSenderTag()], }).catch(() => {}); } @@ -1715,7 +1724,7 @@ export function arkFeature(ctx) { kind: ARK_ZAP_KIND, content: (z.comment || '').slice(0, 280), tags: [['p', z.pk], ...(z.eventId ? [['e', z.eventId]] : []), - ['amount', String(sats)], ['network', getNetwork()], ['gift', locked.url]], + ['amount', String(sats)], ['network', getNetwork()], ['gift', locked.url], ...zapSenderTag()], }).catch(() => {}); const dmText = t('giftDmText', { amount: fmtAmount(sats) + ' ' + unitLabel(), link: locked.url, code: locked.claimCode }); if (wallet.sendNostrDM) { diff --git a/src/features/messages.js b/src/features/messages.js index acde0cb..958cd8d 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -29,13 +29,14 @@ import { packAddr, packNaddr, parsePackRef, parsePackAddr, parseEmojiSet, } from '../emoji.js'; import { saveInbox } from '../dm-inbox.js'; -import { makeSearcher, resultRows, fallbackAvatar, warmSearch } from '../recipient-search.js'; +import { makeSearcher, resultRows, fallbackAvatar, warmSearch, punkImageUrl } from '../recipient-search.js'; import { getNetwork } from '../api.js'; import { decodeBolt11 } from '../ark/lightning.js'; import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import { sha256 } from '@noble/hashes/sha256'; import { base64urlnopad } from '@scure/base'; import { t } from '../i18n.js'; +import { animateZap, warmZapSound } from '../zap-animation.js'; import { SIGNER_SILENT } from '../dm.js'; // The coinos community's join material lives in ../community.js — shared @@ -544,7 +545,7 @@ export function messagesFeature(ctx) { if (!p || (!p.name && !p.picture)) return; const s = wallet.loadFeatureState('profiles', {}); s[pk] = { name: p.name || null, picture: p.picture || null, nip05: p.nip05 || null, lud16: p.lud16 || null, - about: p.about || null, banner: p.banner || null, t: Date.now(), + about: p.about || null, banner: p.banner || null, eventAt: p.eventAt || 0, t: Date.now(), ...(p.thumbFor === p.picture && p.thumb ? { thumb: p.thumb, thumbFor: p.thumbFor, thumbPx: p.thumbPx || 0 } : {}), ...(p.thumbFail ? { thumbFail: p.thumbFail, thumbFailAt: p.thumbFailAt || 0, thumbFails: p.thumbFails || 1, @@ -582,7 +583,7 @@ export function messagesFeature(ctx) { const PUNK_PIC_RE = /^(?:https?:\/\/(?:[a-z0-9-]+\.)*coinos\.io\/)?punks\/(\d{1,2})\.webp$/i; const localPunk = (url, big) => { const m = PUNK_PIC_RE.exec(url || ''); - return m ? (big ? `punks/${m[1]}.webp` : `punks-sm/${m[1]}.webp`) : null; + return m ? punkImageUrl(m[1], !big) : null; }; // A refreshed profile whose picture hasn't changed keeps the thumbnail we @@ -755,7 +756,10 @@ export function messagesFeature(ctx) { function applyProfile(pk, ev) { let m = null; try { m = JSON.parse(ev.content); } catch { m = null; } - const p = m ? { + if (!m || typeof m !== 'object' || Array.isArray(m)) return false; + const prev = profiles.get(pk); + if ((prev?.eventAt || 0) > ev.created_at) return true; + const p = { name: m.display_name || m.name || null, picture: m.picture || null, nip05: m.nip05 || null, @@ -767,14 +771,20 @@ export function messagesFeature(ctx) { // renders anyway. about: typeof m.about === 'string' ? m.about.slice(0, 1000) : null, banner: typeof m.banner === 'string' ? m.banner.slice(0, 400) : null, - } : null; - // An empty answer must never clobber a remembered face with a punk: keep - // what we had and just refresh the clock. - const prev = profiles.get(pk); - const entry = keepThumb(pk, p ? { ...p, t: Date.now(), miss: 0 } : { ...(prev || {}), t: Date.now() }); + }; + const entry = keepThumb(pk, { ...p, eventAt: ev.created_at, t: Date.now(), miss: 0 }); profiles.set(pk, entry); + // A page and the batched avatar lookup must share their answer. A slow + // page miss must never erase a name/photo the batch already found. + if (fullInFlight.has(pk) || fullProfiles.has(pk)) { + fullProfiles.set(pk, m); + fullFetched.add(pk); + fullMisses.delete(pk); + persistPage('full', pk, m); + } persistProfile(pk, entry); preloadPicture(entry); + return true; } async function fetchProfiles(pks) { @@ -789,7 +799,7 @@ export function messagesFeature(ctx) { const c = newest.get(ev.pubkey); if (!c || ev.created_at > c.created_at) newest.set(ev.pubkey, ev); } - for (const [pk, ev] of newest) { applyProfile(pk, ev); found.add(pk); } + for (const [pk, ev] of newest) { if (applyProfile(pk, ev)) found.add(pk); } // paint what this batch found before going after the stragglers — // the outbox fallback below can take seconds, and there's no reason // for a face we already have to wait behind one we don't @@ -808,7 +818,7 @@ export function messagesFeature(ctx) { const c = idxNewest.get(ev.pubkey); if (!c || ev.created_at > c.created_at) idxNewest.set(ev.pubkey, ev); } - for (const [pk, ev] of idxNewest) { applyProfile(pk, ev); found.add(pk); } + for (const [pk, ev] of idxNewest) { if (applyProfile(pk, ev)) found.add(pk); } if (idxNewest.size) scheduleRepaint(); } const stillLeft = pks.filter((pk) => !found.has(pk)); @@ -822,7 +832,7 @@ export function messagesFeature(ctx) { const c = newest.get(ev.pubkey); if (!c || ev.created_at > c.created_at) newest.set(ev.pubkey, ev); } - for (const [pk, ev] of newest) { applyProfile(pk, ev); found.add(pk); } + for (const [pk, ev] of newest) { if (applyProfile(pk, ev)) found.add(pk); } })); } // nobody has one: mark the clock so it's retried in minutes, not asked @@ -1471,7 +1481,7 @@ export function messagesFeature(ctx) { close(); setTimeout(() => document.getElementById('msg-draft')?.focus(), 50); }), - !ui.emojiPick && !mine && canZapPk(m.author) ? item('⚡', t('msgZap'), () => { close(); zapMessage(m.author, m.rumor.id); }) : null, + !ui.emojiPick && canZapPk(m.author) ? item('⚡', t('msgZap'), () => { close(); zapMessage(m.author, m.rumor.id); }) : null, ui.emojiPick ? null : item('⧉', t('copy'), async () => { try { await navigator.clipboard.writeText(msgSnippet(room, m, 100000)); toast(t('copied')); } catch {} close(); @@ -2773,6 +2783,8 @@ export function messagesFeature(ctx) { const fullProfiles = new Map(); // pk -> { raw kind0 content object, fetched_at } const fullFetched = new Set(); // fetched from relays this session + const fullInFlight = new Map(); + const fullMisses = new Set(); // The profile-page cache SURVIVES refreshes: bios and recent posts serve // instantly from storage while a background fetch freshens them. Bounded: @@ -2807,32 +2819,20 @@ export function messagesFeature(ctx) { const cached = pageCache().full[pk]; if (cached) fullProfiles.set(pk, cached.v); } - if (fullFetched.has(pk)) return; - fullFetched.add(pk); - queryOn([...new Set([...PROFILE_RELAYS, ...DM_RELAYS])], { kinds: [0], authors: [pk] }, 3500).then((evs) => { - const newest = evs.sort((a, b) => b.created_at - a.created_at)[0]; - let m = {}; - try { m = newest ? JSON.parse(newest.content) : {}; } catch {} - fullProfiles.set(pk, m); - persistPage('full', pk, m); - // A full fetch is the freshest word on this profile — stamp it into the - // light cache (and persist) so a picture changed elsewhere replaces the - // stale one everywhere within a session, not after the 24h TTL. The - // repaint covers the header/chat avatars, not just an open profile page. - const entry = { - name: m.display_name || m.name || null, picture: m.picture || null, - nip05: m.nip05 || null, lud16: typeof m.lud16 === 'string' ? m.lud16.trim() : null, - about: typeof m.about === 'string' ? m.about.slice(0, 1000) : null, - banner: typeof m.banner === 'string' ? m.banner.slice(0, 400) : null, - t: Date.now(), - }; - keepThumb(pk, entry); - profiles.set(pk, entry); - persistProfile(pk, entry); - preloadPicture(entry); + if (fullFetched.has(pk)) return Promise.resolve(); + if (fullInFlight.has(pk)) return fullInFlight.get(pk); + fullMisses.delete(pk); + // Use the same index-relay + author's-relay fallbacks as feed avatars. + // Only a real kind-0 response marks a page fetched; relay silence is + // retryable and leaves cached profile details intact. + const pending = fetchProfiles([pk]).catch(() => {}).finally(() => { + fullInFlight.delete(pk); + if (!fullFetched.has(pk)) fullMisses.add(pk); if (ui.profilePk === pk) render(); else scheduleRepaint(); - }).catch(() => { if (!fullProfiles.has(pk)) fullProfiles.set(pk, {}); }); + }); + fullInFlight.set(pk, pending); + return pending; } // Warm a profile PAGE (full kind-0 + latest notes + relay list) before @@ -2888,7 +2888,10 @@ export function messagesFeature(ctx) { // ...and, in the same REQ, what else happened to a note: likes (kind 7) // and boosts (kind 6). One round trip for all three. const NOTE_KINDS = [9735, 9737, 7, 6]; - const reacts = new Map(); // note id -> Map(emoji -> Set<pubkey>) + const reacts = new Map(); // note id -> Map([emoji, url] -> { emoji, url, who: Set<pubkey> }) + // Resolve from this reaction's own tag: packs can reuse the same shortcode + // for different pictures, and an unrelated event must not change its face. + const postReactNode = ({ emoji, url }) => url ? emojiImg(shortcodeOf(emoji), url) : emoji; const boosts = new Map(); // note id -> Set<pubkey> const seenNoteEv = new Set(); // event ids already counted const myReactEv = new Map(); // note id -> the id of OUR reaction, so it can be withdrawn @@ -2919,7 +2922,13 @@ export function messagesFeature(ctx) { try { return String((JSON.parse(tagOf(ev, 'description') || 'null') || {}).content || '').slice(0, 200); } catch { return ''; } } function zapperOf(ev) { - if (ev.kind === 9737) return ev.pubkey; + if (ev.kind === 9737) { + // Signed by the wallet key; the sender tag names the person behind it. + // Older receipts of ours have no such tag — read them as us all the same. + const sender = tagOf(ev, 'P') || ev.pubkey; + const id = hook('nostrLoginIdentity'); + return id && wallet.nostr && sender === wallet.nostr.pk ? id.pubkey : sender; + } if (tagOf(ev, 'P')) return tagOf(ev, 'P'); try { return (JSON.parse(tagOf(ev, 'description') || 'null') || {}).pubkey || null; } catch { return null; } } @@ -2938,11 +2947,15 @@ export function messagesFeature(ctx) { boosts.get(id).add(ev.pubkey); } else { // '+' and an empty content both mean a plain like - const emoji = !ev.content || ev.content === '+' ? '\u2764\ufe0f' : ev.content.slice(0, 12); + const code = shortcodeOf(ev.content); + const emoji = !ev.content || ev.content === '+' ? '\u2764\ufe0f' : code ? ev.content : ev.content.slice(0, 12); + const url = code ? emojiTagMap(ev.tags).get(code) || null : null; + const key = JSON.stringify([emoji, url]); + noteEmoji(ev); if (!reacts.has(id)) reacts.set(id, new Map()); const m = reacts.get(id); - if (!m.has(emoji)) m.set(emoji, new Set()); - m.get(emoji).add(ev.pubkey); + if (!m.has(key)) m.set(key, { emoji, url, who: new Set() }); + m.get(key).who.add(ev.pubkey); // ours, wherever it was sent from — so it can be taken back here if (myPubkeys().includes(ev.pubkey)) myReactEv.set(id, ev.id); } @@ -3084,10 +3097,11 @@ export function messagesFeature(ctx) { if (Date.now() - p.at > pendTtl(p)) { zapPending.delete(id); return null; } return p; } - function markZapPending(id, sats) { + function markZapPending(id, sats, origin) { if (!id || !sats) return; zapPending.set(id, { sats, at: Date.now(), state: 'flying' }); - scheduleRepaint(); + animateZap(sats, origin, id); + render(); // repaint when the in-flight chip would expire, so a zap nobody ever // reported on doesn't pulse forever setTimeout(scheduleRepaint, ZAP_FLIGHT_MS + 200); @@ -3118,6 +3132,7 @@ export function messagesFeature(ctx) { // The chip: a little bolt + the sats total. Absent until the first receipt // — or until you zap it yourself, which is its own kind of receipt. function zapChip(id, { onClick, cls = '' } = {}) { + if (onClick) warmZapSound(); const z = zapTotals.get(id) || zapSeeds().get(id); const p = pendingOf(id); const optimistic = p && p.state !== 'void' ? p.sats : 0; @@ -4195,20 +4210,18 @@ export function messagesFeature(ctx) { // asks once and remembers. // Whether a ⚡ makes sense for this author from this wallet: not // ourselves, and an instant path (Ark) or a Lightning fallback exists. - const canZapPk = (pk) => !!pk && !isMe(pk) && !!(hook('arkReady') || hook('canLnZap')); - function zapNote(pk, ev) { + // Your own posts and messages too: a zap to yourself is a harmless + // round-trip, and one bar that behaves the same everywhere beats a special case. + const canZapPk = (pk) => !!pk && !!(hook('arkReady') || hook('canLnZap')); + function zapNote(pk, ev, origin) { + if (pendingOf(ev.id)?.state === 'flying') return; const npubStr = npubOf(pk); const def = ctx.zapDefaultSat ? ctx.zapDefaultSat() : 0; - if (!def) { ui.zapSetup = { pk, npub: npubStr, eventId: ev.id, amount: '21' }; render(); return; } - markZapPending(ev.id, def); // the chip answers the tap; the flow reports back + if (!def) { ui.zapSetup = { pk, npub: npubStr, eventId: ev.id, amount: '21', origin }; render(); return; } + markZapPending(ev.id, def, origin); // strike now; settlement only corrects the tally if (!hook('zapNpub', pk, npubStr, ev.id, def) && !hook('lnZapNpub', pk, npubStr, ev.id, def)) { - // no instant path in this build — the classic form flow settleZap(ev.id, false); - ui.profilePk = null; - ui.chatOpen = false; - ctx.showSend(); - render(); - if (!hook('zapNpub', pk, npubStr, ev.id)) hook('lnZapNpub', pk, npubStr, ev.id); + toast('⚡ ' + t('lnZapFailed'), 4000); } } @@ -4230,8 +4243,11 @@ export function messagesFeature(ctx) { const { pk, npub, eventId } = s; ui.zapSetup = null; render(); - markZapPending(eventId, n); - if (!hook('zapNpub', pk, npub, eventId, n) && !hook('lnZapNpub', pk, npub, eventId, n)) settleZap(eventId, false); + markZapPending(eventId, n, s.origin); + if (!hook('zapNpub', pk, npub, eventId, n) && !hook('lnZapNpub', pk, npub, eventId, n)) { + settleZap(eventId, false); + toast('⚡ ' + t('lnZapFailed'), 4000); + } recheckZap(eventId); } }, t('zapSetupSave'))), h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.zapSetup = null; render(); } }, t('back'))); @@ -4248,7 +4264,7 @@ export function messagesFeature(ctx) { const m = reacts.get(id); if (!m) return null; const mine = myPubkeys(); - for (const [emoji, who] of m) for (const pk of mine) if (who.has(pk)) return emoji; + for (const reaction of m.values()) for (const pk of mine) if (reaction.who.has(pk)) return reaction; return null; }; const iBoosted = (id) => { @@ -4264,10 +4280,22 @@ export function messagesFeature(ctx) { function replyToNote(ev) { if (ui.noteThread && ui.noteThread.rootId === rootIdOf(ev)) { ui.noteThread.focusId = ev.id; - render(); - } else openNoteThread(ev); - if (ui.noteThread) ui.noteThread.refocus = true; - setTimeout(() => document.querySelector('.thread-reply-input')?.focus(), 120); + ui.noteThread.seed = ev; + } else ui.noteThread = { rootId: rootIdOf(ev), focusId: ev.id, seed: ev }; + const s = ui.noteThread; + ui.profOverThread = false; + s.scrollPending = false; // focus the composer, without a competing note scroll + s.refocus = true; + render(); + // Focus in the tap handler so mobile browsers can open the keyboard. + focusThreadReply(s); + } + function focusThreadReply(s) { + if (ui.noteThread !== s || ui.profOverThread) return; + const input = document.querySelector('.thread-reply-input'); + if (!input) return; + input.focus({ preventScroll: true }); + input.scrollIntoView({ block: 'nearest' }); } async function unreact(ev) { @@ -4279,7 +4307,7 @@ export function messagesFeature(ctx) { // that don't go on showing it to other people — worth knowing, not // worth refusing to try. const m = reacts.get(ev.id); - if (m) for (const [emoji, who] of [...m]) { for (const pk of myPubkeys()) who.delete(pk); if (!who.size) m.delete(emoji); } + if (m) for (const [key, { who }] of [...m]) { for (const pk of myPubkeys()) who.delete(pk); if (!who.size) m.delete(key); } render(); const mineEv = myReactEv.get(ev.id); if (!mineEv) return; @@ -4297,7 +4325,7 @@ export function messagesFeature(ctx) { const relays = await noteRelaysFor(ev); const partial = { kind: 7, content: emoji || '❤️', created_at: Math.floor(Date.now() / 1000), - tags: [['e', ev.id], ['p', ev.pubkey], CLIENT_TAG], + tags: [['e', ev.id], ['p', ev.pubkey], ...reactEmojiTags(emoji), CLIENT_TAG], }; const evt = id.signer instanceof Uint8Array ? finalizeEvent(partial, id.signer) : await id.signer.signEvent(partial); myReactEv.set(ev.id, evt.id); @@ -4451,14 +4479,15 @@ export function messagesFeature(ctx) { const I_ZAP = ICON('<path d="M13 2L4 14h7l-1 8 9-12h-7l1-8z"/>', true); function noteActions(pk, ev, { canZap }) { + if (canZap) warmZapSound(); // the clip is decoded before the first tap const mineReact = myReactOn(ev.id); const rm = reacts.get(ev.id); - const likeN = rm ? [...rm.values()].reduce((n, who) => n + who.size, 0) : 0; + const likeN = rm ? [...rm.values()].reduce((n, { who }) => n + who.size, 0) : 0; const boostN = (boosts.get(ev.id) || new Set()).size; const btn = (icon, label, count, on, onClick, cls = '') => h('button', { class: 'note-act' + (on ? ' on' : '') + (cls ? ' ' + cls : ''), title: label, 'aria-label': label, 'aria-disabled': onClick ? undefined : 'true', - onClick: (e) => { e.stopPropagation(); if (onClick) onClick(); }, + onClick: (e) => { e.stopPropagation(); if (onClick) onClick(e); }, }, typeof icon === 'string' && icon.startsWith('<svg') ? h('span', { style: 'display:flex', html: icon }) @@ -4478,23 +4507,24 @@ export function messagesFeature(ctx) { btn(I_BOOST, t('postBoost'), boostN, iBoosted(ev.id), () => boostNote(ev).catch(() => {})), btn(I_QUOTE, t('postQuote'), 0, false, () => quoteNote(ev)), // tap to choose how you feel about it; tap again to take it back - btn(mineReact || I_HEART(false), t('postLike'), likeN, !!mineReact, + btn(mineReact ? postReactNode(mineReact) : I_HEART(false), t('postLike'), likeN, !!mineReact, () => { if (mineReact) unreact(ev).catch(() => {}); else { ui.reactPick = ev; render(); } }), - // shown whenever there is a tally to read, even where zapping is off - // (own post, no wallet) — then it is just a number, not a button - canZap || zapSats - ? btn(I_ZAP, zapLabel, zapSats ? fmtSats(zapSats) : 0, zapMine, - canZap ? () => { zapNote(pk, ev); recheckZap(ev.id); } : null, - 'note-zap' + (zapSats ? ' zapped' : '') + (zapFlying ? ' flying' : '')) - : null, - // who did all that: a small chevron, only once there is anyone to show - whoCount(ev.id) - ? h('button', { - class: 'note-act note-who-toggle' + (whoOpen(ev.id) ? ' on' : ''), - title: t('postWho'), 'aria-label': t('postWho'), 'aria-expanded': whoOpen(ev.id) ? 'true' : 'false', - onClick: (e) => { e.stopPropagation(); toggleWho(ev.id); }, - }, h('span', { class: 'note-who-chev' + (whoOpen(ev.id) ? ' open' : ''), html: I_CHEV })) - : null); + // always in the bar — the same row of buttons on every post, so nothing + // shifts when a wallet comes online; without one it is just the number + btn(I_ZAP, zapLabel, zapSats ? fmtSats(zapSats) : 0, zapMine, + canZap ? (e) => { zapNote(pk, ev, e.currentTarget.getBoundingClientRect()); recheckZap(ev.id); } : null, + 'note-zap' + (zapSats ? ' zapped' : '') + (zapFlying ? ' flying' : '')), + // who did all that: a small chevron, always in place — dimmed until + // there is someone to show, so the first reaction doesn't reflow the bar + (() => { + const n = whoCount(ev.id), open = n && whoOpen(ev.id); + return h('button', { + class: 'note-act note-who-toggle' + (open ? ' on' : ''), + title: t('postWho'), 'aria-label': t('postWho'), 'aria-expanded': open ? 'true' : 'false', + 'aria-disabled': n ? undefined : 'true', + onClick: (e) => { e.stopPropagation(); if (n) toggleWho(ev.id); }, + }, h('span', { class: 'note-who-chev' + (open ? ' open' : ''), html: I_CHEV })); + })()); } // ---- who reacted, boosted and zapped ------------------------------------ @@ -4507,7 +4537,7 @@ export function messagesFeature(ctx) { const toggleWho = (id) => { if (whoOpenIds.has(id)) whoOpenIds.delete(id); else whoOpenIds.add(id); render(); }; function whoCount(id) { const rm = reacts.get(id); - let n = rm ? [...rm.values()].reduce((k, who) => k + who.size, 0) : 0; + let n = rm ? [...rm.values()].reduce((k, { who }) => k + who.size, 0) : 0; n += (boosts.get(id) || new Set()).size; n += (zapWho.get(id) || new Map()).size; return n; @@ -4536,8 +4566,8 @@ export function messagesFeature(ctx) { person(z.pk, h('span', { class: 'note-who-sats' }, fmtSats(z.sats) + ' sats' + (z.text ? ' · ' + z.text : ''))))))); } - for (const [emoji, set] of [...rm.entries()].sort((a, b) => b[1].size - a[1].size)) { - lines.push(line(h('span', { class: 'note-act-emoji' }, emoji), people([...set]))); + for (const reaction of [...rm.values()].sort((a, b) => b.who.size - a.who.size)) { + lines.push(line(h('span', { class: 'note-act-emoji' }, postReactNode(reaction)), people([...reaction.who]))); } if (bs.size) lines.push(line(h('span', { style: 'display:flex', html: I_BOOST }), people([...bs]))); if (!lines.length) return null; @@ -4567,7 +4597,7 @@ export function messagesFeature(ctx) { // profile is actually opened. profileOf(pk); const isReply = ev.tags.some((x) => x[0] === 'e'); - const canZap = !isMe(pk) && !!(hook('arkReady') || hook('canLnZap')); + const canZap = canZapPk(pk); // an optimistic post mid-publish: visible but not yet a real event — // dimmed, and no thread/reply/zap until its signed self takes over const pending = !!ev.pending; @@ -4575,6 +4605,7 @@ export function messagesFeature(ctx) { if (!pending) watchZaps([ev.id]); return h('div', { class: 'row', + 'data-zap-post': ev.id, 'data-focus-note': focus ? '1' : undefined, style: 'gap:10px;align-items:flex-start;padding:10px 0' + (openable ? ';cursor:pointer' : '') @@ -4621,8 +4652,15 @@ export function messagesFeature(ctx) { function threadFor(seed) { const rootId = rootIdOf(seed); let c = threadCache.get(rootId); - if (c) return c; - c = { status: 'loading', rootId, root: seed.id === rootId ? seed : null, replies: [] }; + if (c) { + if (seed.id === rootId) c.root ||= seed; + else if (!c.replies.some((ev) => ev.id === seed.id)) + c.replies = [...c.replies, seed].sort((a, b) => a.created_at - b.created_at); + return c; + } + // We already have the tapped post. Keep it visible and replyable even + // if the root is unavailable or the relay's limited result omits it. + c = { status: 'loading', rootId, root: seed.id === rootId ? seed : null, replies: seed.id === rootId ? [] : [seed] }; threadCache.set(rootId, c); (async () => { // the conversation's home relays are the root author's (NIP-10 outbox); @@ -4634,7 +4672,7 @@ export function messagesFeature(ctx) { ]); if (!c.root) c.root = (roots || [])[0] || null; const seen = new Set([rootId]); - c.replies = (replies || []) + c.replies = [...c.replies, ...(replies || [])] .filter((e) => !seen.has(e.id) && seen.add(e.id)) .sort((a, b) => a.created_at - b.created_at); c.status = 'ready'; @@ -4646,7 +4684,8 @@ export function messagesFeature(ctx) { // button asked for it. if (ui.noteThread.refocus) { ui.noteThread.refocus = false; - setTimeout(() => document.querySelector('.thread-reply-input')?.focus(), 30); + const s = ui.noteThread; + setTimeout(() => focusThreadReply(s), 30); } } })().catch(() => { @@ -5119,6 +5158,9 @@ export function messagesFeature(ctx) { // full header on profiles too — losing the search button here made // finding the NEXT person a trek back home ctx.brandHeader(!ui.pubProf && wallet.loaded), + fullMisses.has(pk) ? h('div', { class: 'row between gap6 small muted' }, + t(full || lp.eventAt || lp.name || lp.picture ? 'profFetchFailed' : 'profNotFound'), + h('button', { class: 'btn-sm', onClick: () => { fetchFullProfile(pk); render(); } }, t('retry'))) : null, h('div', { class: 'card col', style: 'gap:12px' }, bannerUrl ? h('div', { class: 'profile-banner', style: `background-image:url(${JSON.stringify(bannerUrl)})` }) : null, h('div', { class: 'row gap6', style: 'align-items:center' }, @@ -5954,11 +5996,11 @@ export function messagesFeature(ctx) { }, h('div', { class: 'chat-avatar fallback' }, name.slice(0, 2)), h('div', { class: 'col grow', style: 'min-width:0;gap:1px' }, - h('div', { class: 'row between' }, - h('span', { class: 'chat-name' }, name), - unread ? h('i', { class: 'thread-dot' }) : null), + h('span', { class: 'chat-name' }, name), h('div', { class: 'muted small' }, - memberCount ? t('msgMembers', { n: memberCount }) : t('msgEncrypted')))); + memberCount ? t('msgMembers', { n: memberCount }) : t('msgEncrypted'))), + // the row's own centring puts the dot mid-height, off the edge + unread ? h('i', { class: 'thread-dot' }) : null); }))); return h('div', { class: 'card col chat-page', style: 'gap:10px' }, ...kids); @@ -6068,7 +6110,7 @@ export function messagesFeature(ctx) { title: t('msgReact'), onClick: (e) => { e.stopPropagation(); sendReaction(room, chId, m, emoji); }, }, reactNode(emoji), n > 1 ? ' ' + n : ''))) - : null)(zapChip(m.rumor.id, { cls: 'chat-react', onClick: !mine && canZapPk(m.author) ? () => zapMessage(m.author, m.rumor.id) : null })))) + : null)(zapChip(m.rumor.id, { cls: 'chat-react', onClick: canZapPk(m.author) ? () => zapMessage(m.author, m.rumor.id) : null })))) ); }); } @@ -6244,7 +6286,7 @@ export function messagesFeature(ctx) { close(); setTimeout(() => document.getElementById('msg-draft')?.focus(), 50); }), - !ui.emojiPick && !m.mine && canZapPk(m.rumor.pubkey) ? item('⚡', t('msgZap'), () => { close(); zapMessage(m.rumor.pubkey, m.rumor.id); }) : null, + !ui.emojiPick && canZapPk(m.rumor.pubkey) ? item('⚡', t('msgZap'), () => { close(); zapMessage(m.rumor.pubkey, m.rumor.id); }) : null, ui.emojiPick ? null : item('⧉', t('copy'), async () => { try { await navigator.clipboard.writeText(m.rumor.content); toast(t('copied')); } catch {} close(); @@ -6273,7 +6315,7 @@ export function messagesFeature(ctx) { watchZaps(msgs.slice(-150).map((m) => m.rumor.id)); const dmChips = (m) => { const reacts = dmReacts.get(m.rumor.id); - const zc = zapChip(m.rumor.id, { cls: 'chat-react', onClick: !m.mine && canZapPk(m.rumor.pubkey) ? () => zapMessage(m.rumor.pubkey, m.rumor.id) : null }); + const zc = zapChip(m.rumor.id, { cls: 'chat-react', onClick: canZapPk(m.rumor.pubkey) ? () => zapMessage(m.rumor.pubkey, m.rumor.id) : null }); if ((!reacts || !reacts.size) && !zc) return null; const myReact = reacts && my.map((pk) => reacts.get(pk)).find(Boolean); const counts = new Map(); diff --git a/src/features/zaps.js b/src/features/zaps.js index 77f8475..54c8f58 100644 --- a/src/features/zaps.js +++ b/src/features/zaps.js @@ -214,7 +214,7 @@ export function zapsFeature(ctx) { } const invoice = await requestInvoice(p, { amountMsat: msat, zapRequest, lnurlBech32: t2.lnurlBech32 }); await hook('arkPayInvoice', invoice, { maxAmountSat: sats + 50 }); - toast('⚡ ' + t('zapSentShort', { n: fmtAmount(sats) + ' ' + unitLabel() })); + if (!target.eventId) toast('⚡ ' + t('zapSentShort', { n: fmtAmount(sats) + ' ' + unitLabel() })); render(); } @@ -454,10 +454,13 @@ export function zapsFeature(ctx) { // this went, either way autoZap({ kind: 'npub', pk, eventId: eventId || null }, autoSat) .then(() => hook('zapSettled', eventId, true, autoSat)) - .catch((e) => { hook('zapSettled', eventId, false); toast('⚡ ' + e.message); }); + .catch((e) => { hook('zapSettled', eventId, false); toast('⚡ ' + (e.message || t('lnZapFailed')), 4000); }); return true; } - if (autoSat) hook('zapSettled', eventId, false); // one tap couldn't serve it — the form takes over + if (autoSat) { + hook('zapSettled', eventId, false); + if (eventId) { toast('⚡ ' + t('lnZapFailed'), 4000); return true; } + } begin({ kind: 'npub', pk, eventId: eventId || null }, shortNpub(npub || npubOf(pk))); return true; }, diff --git a/src/i18n.js b/src/i18n.js index 0d5f32f..0dd7513 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -102,6 +102,8 @@ const STR = { profUsername: 'Username', profUploadPic: 'Upload photo', profDisplayName: 'Display name', + profFetchFailed: 'Couldn’t load the latest profile.', + profNotFound: 'No profile details found on the available relays.', profUnameInvalid: 'Usernames can only use letters, numbers, dots, dashes and underscores.', lockSaveDesc: 'The accounts already saved on this device are protected by a password. Enter it to save this account with them and lock.', lockAction: 'Lock', diff --git a/src/style.css b/src/style.css index 3fd3de2..3629dfc 100644 --- a/src/style.css +++ b/src/style.css @@ -499,6 +499,43 @@ hr.divider { margin: 16px 0; } .zap-tally { display: inline-flex; align-items: center; gap: 2px; font-size: 11px; font-weight: 650; color: #e6a800; white-space: nowrap; flex-shrink: 0; } .zap-tally.chat-react { font-size: 12px; padding: 1px 7px 1px 5px; } .zap-tally.clickable { cursor: pointer; } +/* A tap-local lightning strike, followed by a floating amount and embers. */ +.zap-fx { position: fixed; inset: 0; z-index: 10000; pointer-events: none; overflow: hidden; contain: strict; } +.zap-fx-impact { position: absolute; left: var(--zap-x); top: var(--zap-y); width: 0; height: 0; color: #ffc83d; } +.zap-fx-bolt { position: absolute; width: 82px; height: 180px; left: -41px; bottom: 0; fill: #fff5c2; filter: drop-shadow(0 0 5px #fff3a1) drop-shadow(0 0 13px #ff9d00); transform-origin: 50% 100%; animation: zap-strike 430ms ease-out both; } +.zap-fx-flash, .zap-fx-ring { position: absolute; left: -28px; top: -28px; width: 56px; height: 56px; border-radius: 50%; animation: zap-impact 550ms 60ms ease-out both; } +.zap-fx-flash { background: radial-gradient(circle, #fffce0, #ffca45 25%, #ff980066 50%, transparent 70%); } +.zap-fx-ring { border: 2px solid #ffd76a; box-shadow: 0 0 12px #ffad32, inset 0 0 10px #ffad3266; animation-name: zap-ring; } +.zap-fx-amount { position: absolute; left: var(--zap-label-x); top: -60px; display: flex; align-items: baseline; gap: 6px; white-space: nowrap; color: #fff2ad; text-shadow: 0 2px 2px #713500, 0 0 9px #ff9c00, 0 0 22px #ff9500aa; animation: zap-amount 1450ms cubic-bezier(.16, 1, .3, 1) both; } +.zap-fx-amount strong { font-size: clamp(34px, 9vw, 48px); font-weight: 900; line-height: 1; letter-spacing: -.045em; } +.zap-fx-amount span { font-size: 13px; font-weight: 800; letter-spacing: .04em; } +.zap-fx-spark { position: absolute; width: 10px; height: 3px; left: -5px; top: -2px; border-radius: 3px; background: #ffe997; box-shadow: 0 0 6px #ffaf20; animation: zap-spark 720ms var(--delay) ease-out both; } +.zap-fx-spark.ember { left: calc(var(--zap-label-x) - 2px); top: -80px; width: 3px; height: 3px; background: #fff4c2; animation-duration: 750ms; } +@keyframes zap-strike { + 0% { opacity: 0; transform: scaleY(.15); } + 16%, 38% { opacity: 1; transform: scaleY(1); } + 65% { opacity: .8; } + 100% { opacity: 0; transform: scaleY(1.04); } +} +@keyframes zap-impact { 0% { opacity: 1; transform: scale(.2); } 100% { opacity: 0; transform: scale(2.4); } } +@keyframes zap-ring { 0% { opacity: .9; transform: scale(.25); } 100% { opacity: 0; transform: scale(2); } } +@keyframes zap-amount { + 0% { opacity: 0; transform: translate(-50%, 35px) scale(.35) rotate(-9deg); } + 16% { opacity: 1; transform: translate(-50%, -8px) scale(1.2) rotate(-5deg); } + 30% { opacity: 1; transform: translate(-50%, -4px) scale(1) rotate(-3deg); } + 58% { opacity: 1; filter: blur(0); } + 100% { opacity: 0; transform: translate(-50%, -65px) scale(1.08) rotate(2deg); filter: blur(3px); } +} +@keyframes zap-spark { + 0% { opacity: 0; transform: translate(0, 0) rotate(var(--angle)) scale(.4); } + 12% { opacity: 1; } + 100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--angle)) scale(.1); } +} +@media (prefers-reduced-motion: reduce) { + .zap-fx-bolt, .zap-fx-flash, .zap-fx-ring, .zap-fx-spark { display: none; } + .zap-fx-amount { animation: zap-amount-reduced 1200ms ease-out both; } + @keyframes zap-amount-reduced { 0%, 65% { opacity: 1; transform: translateX(-50%); } 100% { opacity: 0; transform: translateX(-50%); } } +} /* A zap in flight: the chip lands the moment you tap and breathes until the payment settles — the bolt carries the pulse, the number stays readable. */ .zap-tally.flying { animation: zap-breathe 1.15s ease-in-out infinite; } @@ -529,6 +566,7 @@ hr.divider { margin: 16px 0; } .chat-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .thread-when { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; } .thread-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--red); flex-shrink: 0; } +.chat-thread-row > .thread-dot { margin-right: 8px; } .chat-thread-row.unread .chat-name { font-weight: 700; } .chat-thread-row.unread .chat-preview { color: var(--ink); } .chat-invite { padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--surface2); flex: 0 0 auto; max-height: 40vh; overflow-y: auto; } @@ -574,6 +612,8 @@ hr.divider { margin: 16px 0; } .note-act-emoji { font-size: 17px; line-height: 1; } .note-act-n { font-size: 12px; font-weight: 600; color: var(--muted); } .note-who-toggle.on { color: var(--muted); } +.note-who-toggle[aria-disabled] { opacity: .3; cursor: default; } +.note-who-toggle[aria-disabled]:hover { background: none; color: var(--faint); } .note-who-chev { display: flex; transition: transform .2s; } .note-who-chev.open { transform: rotate(180deg); } /* who reacted, boosted and zapped: opened by the chevron on the bar */ diff --git a/src/zap-animation.js b/src/zap-animation.js new file mode 100644 index 0000000..20a6b60 --- /dev/null +++ b/src/zap-animation.js @@ -0,0 +1,83 @@ +// Outside the app's morphed tree so relay updates cannot restart the effect. +// This is tap feedback only: payment settlement never plays it again. + +// The clip is 62KB of base64 — messages.js can't be deferred (it's the boot +// shell), so the sound is its own chunk instead, fetched once a zappable +// post or chat message is on screen. By the first tap it's decoded; and if +// it isn't, the sound keeps the tap's own clock and stays silent when late. +let soundModule = null, sound = null; +export function warmZapSound() { + if (!soundModule) { + soundModule = import('./zap-sound.js') + .then((m) => (sound = m)) + .catch(() => { soundModule = null; return null; }); + } + return soundModule; +} + +const trembles = new WeakMap(); +function tremblePost(id) { + if (!id || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + for (const post of document.querySelectorAll(`[data-zap-post="${CSS.escape(id)}"]`)) { + // Web Animations survive the app's DOM morphs without adding a class + // that a balance update could remove or accidentally restart. + trembles.get(post)?.cancel(); + const animation = post.animate([ + { transform: 'none' }, + { transform: 'translate(-3px, 1px) rotate(-.3deg)' }, + { transform: 'translate(3px, -1px) rotate(.3deg)' }, + { transform: 'translate(-2.5px, 0) rotate(-.2deg)' }, + { transform: 'translate(2px, 1px) rotate(.2deg)' }, + { transform: 'translate(-1.5px, -1px)' }, + { transform: 'translate(1px, 0)' }, + { transform: 'none' }, + ], { duration: 360, easing: 'ease-out' }); + trembles.set(post, animation); + animation.onfinish = () => { if (trembles.get(post) === animation) trembles.delete(post); }; + } +} + +export function animateZap(sats, origin, postId) { + // Warm: play inside the tap itself (the audio context resumes on a + // gesture). Cold: play when the chunk lands, if that's still soon enough. + if (sound) sound.playZapSound(); + else { const tappedAt = performance.now(); warmZapSound().then((m) => { if (m) m.playZapSound(tappedAt); }); } + try { tremblePost(postId); } catch {} // Optional motion must never block a zap. + const layer = document.createElement('div'); + layer.className = 'zap-fx'; + layer.setAttribute('aria-hidden', 'true'); + const x = origin ? origin.left + origin.width / 2 : innerWidth / 2; + const y = origin ? origin.top + origin.height / 2 : innerHeight / 2; + layer.style.setProperty('--zap-x', `${x}px`); + layer.style.setProperty('--zap-y', `${Math.max(110, y)}px`); + layer.innerHTML = '<div class="zap-fx-impact">' + + '<svg class="zap-fx-bolt" viewBox="0 0 100 180"><path d="M62 0 25 65 52 59 15 122 43 110 50 180 72 91 48 100 88 35 59 43Z"/></svg>' + + '<i class="zap-fx-flash"></i><i class="zap-fx-ring"></i>' + + '<div class="zap-fx-amount"><strong></strong><span>sats</span></div></div>'; + layer.querySelector('strong').textContent = '+' + sats.toLocaleString(); + const impact = layer.firstElementChild; + for (let i = 0; i < 26; i++) { + const spark = document.createElement('i'); + const angle = i * 2.39996; + const distance = 38 + (i % 6) * 15; + spark.className = 'zap-fx-spark' + (i >= 16 ? ' ember' : ''); + spark.style.setProperty('--dx', `${Math.cos(angle) * distance}px`); + spark.style.setProperty('--dy', `${Math.sin(angle) * distance - 35}px`); + spark.style.setProperty('--angle', `${angle}rad`); + spark.style.setProperty('--delay', `${i >= 16 ? 600 + (i % 5) * 55 : 60 + (i % 4) * 20}ms`); + impact.append(spark); + } + document.body.append(layer); + // Leave room for the pop's 1.2x overshoot, even for long amounts at an edge. + const label = layer.querySelector('.zap-fx-amount'); + if (label.offsetWidth > (innerWidth - 32) / 1.2) { + const strong = label.querySelector('strong'); + strong.style.fontSize = `${parseFloat(getComputedStyle(strong).fontSize) * (innerWidth - 80) / (label.offsetWidth * 1.2)}px`; + } + const margin = label.offsetWidth * .6 + 12; + const labelX = Math.max(margin, Math.min(innerWidth - margin, x)); + layer.style.setProperty('--zap-label-x', `${labelX - x}px`); + const clean = () => { layer.remove(); window.removeEventListener('pagehide', clean); }; + window.addEventListener('pagehide', clean, { once: true }); + setTimeout(clean, 1700); +} diff --git a/src/zap-sound.js b/src/zap-sound.js new file mode 100644 index 0000000..fcf6d1f --- /dev/null +++ b/src/zap-sound.js @@ -0,0 +1,59 @@ +// The supplied zap.mp3, encoded verbatim so hosted and standalone builds +// both include it. Decode ahead of the tap; no network request is needed. +import mp3 from './assets/zap.mp3.base64.txt' with { type: 'text' }; + +let context, buffer, loading, playing, sequence = 0; + +function prepareZap() { + const AudioCtx = window.AudioContext || window.webkitAudioContext; + if (!AudioCtx) return null; + if (!context || context.state === 'closed') { + context = new AudioCtx({ latencyHint: 'interactive' }); + buffer = null; + loading = null; + } + if (!loading) { + const ctx = context; + const bytes = Uint8Array.from(atob(mp3.trim()), c => c.charCodeAt(0)); + loading = ctx.decodeAudioData(bytes.buffer).then(decoded => { + if (context === ctx) buffer = decoded; + return decoded; + }); + loading.catch(() => { if (context === ctx) loading = null; }); + } + return loading; +} + +// Decoding is allowed while the audio context is suspended; the tap resumes +// playback. A browser without audio must still be able to load the wallet. +try { prepareZap(); } catch {} + +// tappedAt: when the finger landed — the caller's clock, since this module +// may still have been loading when it did. +export function playZapSound(tappedAt = performance.now()) { + try { + const ready = prepareZap(); + if (!ready) return; + const ctx = context, token = ++sequence; + if (ctx.state !== 'running') ctx.resume().catch(() => {}); + const start = decoded => { + // A very early tap may beat decoding. Never queue an old sound behind + // a later zap or surprise the user after a stalled decoder recovers. + if (token !== sequence || context !== ctx || performance.now() - tappedAt > 250) return; + if (playing) { playing.stop(); playing.disconnect(); } + const source = ctx.createBufferSource(), gain = ctx.createGain(); + source.buffer = decoded; + gain.gain.value = .65; + source.connect(gain); + gain.connect(ctx.destination); + source.onended = () => { + source.disconnect(); gain.disconnect(); + if (playing === source) playing = null; + }; + playing = source; + source.start(); + }; + if (buffer) start(buffer); + else ready.then(start).catch(() => {}); + } catch {} // Unavailable audio never interrupts a payment or its animation. +} |
