Compare commits

...
268 Commits
Author SHA1 Message Date
Vitor Pamplona f73c9b5773 v0.85.3 2024-03-04 18:09:03 -05:00
Vitor Pamplona 5848255e72 Displaying issues and Patches in the Notification 2024-03-04 18:01:12 -05:00
Vitor Pamplona 1cf828b165 v0.85.2 2024-03-04 17:52:08 -05:00
Vitor PamplonaandGitHub 2e1f7d6587 Merge pull request #799 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-04 17:00:26 -05:00
Crowdin Bot c6977d97d3 New Crowdin translations by GitHub Action 2024-03-04 21:54:12 +00:00
Vitor Pamplona d12e886e9e Bringing GitIssues in the Notification 2024-03-04 16:52:38 -05:00
Vitor Pamplona 8299f4cfca Cleans up the fork information on new posts has been canceled or posted. 2024-03-04 16:20:59 -05:00
Vitor Pamplona 432b1e4902 Fixes forking information kept from a previous post. 2024-03-04 16:15:09 -05:00
Vitor Pamplona e9fd62dc26 Displaying correct edits on the new edit proposal 2024-03-04 15:37:29 -05:00
Vitor Pamplona f42ec3c149 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-04 15:25:43 -05:00
Vitor PamplonaandGitHub 43e1e0f23e Merge pull request #798 from greenart7c3/multiple-results-crash
fix crash parsing multiple results from amber
2024-03-04 15:17:52 -05:00
Vitor Pamplona 7bc393143c Adds link to the version notes from the drawer 2024-03-04 15:12:17 -05:00
greenart7c3 23718f51dd fix crash parsing multiple results from amber 2024-03-04 17:11:01 -03:00
Vitor Pamplona 8b2efecfbe Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-04 14:13:32 -05:00
Vitor Pamplona 9081d5a54b Adds nostr git for issue management software. 2024-03-04 14:13:23 -05:00
Vitor PamplonaandGitHub 709e3bdd3a Merge pull request #797 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-04 13:22:09 -05:00
Crowdin Bot 1f66ef717b New Crowdin translations by GitHub Action 2024-03-04 18:18:38 +00:00
Vitor Pamplona cd2b5d78a1 Don't show button to edit the post if the author of the original post is not the logged in user 2024-03-04 13:16:34 -05:00
Vitor Pamplona 2f45067949 v0.85.1 2024-03-04 12:00:02 -05:00
Vitor Pamplona 5a680beabb Fixes missing Fhir Classes on Release 2024-03-04 11:58:24 -05:00
Vitor Pamplona 779ce110ba Adding the most recent release features to the Readme. 2024-03-04 11:26:22 -05:00
Vitor Pamplona 43bcee5642 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-04 11:03:55 -05:00
Vitor Pamplona 64c4f7dc95 Removes extra new line 2024-03-04 10:29:19 -05:00
Vitor PamplonaandGitHub 2b6cd07805 Merge pull request #796 from davotoula/accessibility_updates
Accessibility updates
2024-03-04 10:29:05 -05:00
David Kaspar 875d538581 added accessibility translations for CS, DE, SV, PT 2024-03-04 15:03:36 +00:00
David Kaspar f0c53efebd app logo 2024-03-04 14:56:59 +00:00
David Kaspar 8c5aea46f1 refactor incognito button icon 2024-03-04 14:56:59 +00:00
David Kaspar e507e0bb6a update Play text 2024-03-04 14:56:59 +00:00
David Kaspar 2f3df616f3 QR code icon 2024-03-04 14:56:59 +00:00
David Kaspar 75934b5b98 Relay list screeen 2024-03-04 14:56:59 +00:00
David Kaspar 4cbdf11ecb Send message button 2024-03-04 14:56:59 +00:00
David Kaspar bbf0d36cdf Sealed message button on send message 2024-03-04 14:56:59 +00:00
David Kaspar 2bef5f015d change ArrowForwardIos to AutoMirrored 2024-03-04 14:56:59 +00:00
David Kaspar 708bd00d0d updated vertical dots with content descriptions 2024-03-04 14:56:59 +00:00
David Kaspar 2158e36032 Correct spelling 2024-03-04 14:56:59 +00:00
David Kaspar 08a103d332 updated some icons with content descriptions 2024-03-04 14:56:59 +00:00
Vitor PamplonaandGitHub 57bcae6a0d Merge pull request #795 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-04 09:43:33 -05:00
Crowdin Bot 4897c28ed3 New Crowdin translations by GitHub Action 2024-03-04 14:38:48 +00:00
Vitor Pamplona b3211cbb35 Removes the release drafter plugin on actions. Too buggy 2024-03-04 09:37:32 -05:00
Vitor Pamplona fa6adcde75 v0.85.0 2024-03-04 09:10:11 -05:00
Vitor Pamplona ad65e02fc9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-04 09:09:21 -05:00
Vitor Pamplona 249ac4e039 Revert "v0.85.0"
This reverts commit a92ef7a375.
2024-03-04 09:08:20 -05:00
Vitor Pamplona a92ef7a375 v0.85.0 2024-03-04 09:08:06 -05:00
Vitor PamplonaandGitHub e65faa2076 Merge pull request #794 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-03 11:14:10 -05:00
Crowdin Bot 5f53fc6aa9 New Crowdin translations by GitHub Action 2024-03-03 16:07:20 +00:00
Vitor PamplonaandGitHub 218e133857 Merge pull request #793 from davotoula/update_translations
Updated translations. DE, SV, PT
2024-03-03 11:06:06 -05:00
David Kaspar fcd61d87be Updated translations. DE, SV, PT 2024-03-03 09:28:10 +00:00
Vitor Pamplona fb280f4880 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-02 16:18:33 -05:00
Vitor Pamplona 5342c4cfba updates mockk 2024-03-02 16:18:11 -05:00
Vitor PamplonaandGitHub dd106f315f Merge pull request #792 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-02 16:14:02 -05:00
Crowdin Bot 3ddd06fafc New Crowdin translations by GitHub Action 2024-03-02 21:13:28 +00:00
Vitor Pamplona 4fb68dd014 Fixes content title for the video playback notification 2024-03-02 16:11:01 -05:00
Vitor Pamplona a301421915 Adds Home-only empty feed screen. 2024-03-02 15:57:49 -05:00
Vitor Pamplona 16c171ec40 Fixes quote-notes and reposted notes partially disappearing when they contain hidden users or words. 2024-03-02 12:37:02 -05:00
Vitor Pamplona 6725114b48 Avoids changing the showhidden value all the time, triggering recompositions. 2024-03-02 12:35:30 -05:00
Vitor Pamplona 861ad218c8 Fixes bug with NIP-11s with null kinds 2024-03-02 12:03:16 -05:00
Vitor Pamplona b694ac7259 Visualizing proposed edits from other people 2024-03-01 21:15:43 -05:00
Vitor Pamplona 81cc985e3b Adds the ability to see and reply to Git Issues and Patches. 2024-03-01 18:09:15 -05:00
Vitor Pamplona 786802b708 Presents notifications when a person forks your content. 2024-03-01 17:07:19 -05:00
Vitor Pamplona c53fc03157 Cache must update before updating the screen. 2024-03-01 16:52:18 -05:00
Vitor Pamplona 48a6923537 Forces edits to be from the same user. 2024-03-01 16:15:18 -05:00
Vitor Pamplona c34811e05b Tries to highlight that there are two states here for Compose to observe. 2024-03-01 16:15:06 -05:00
Vitor Pamplona 3f500ef5b3 Make sure to add the forked author to the p-tag of a note. 2024-03-01 16:04:26 -05:00
Vitor Pamplona cbce22c994 Fixes the alt tag 2024-03-01 15:46:58 -05:00
Vitor Pamplona bc7a578cfe Making forks work with revisions. 2024-03-01 15:39:24 -05:00
Vitor Pamplona af784a5bda Editting the current version of the post instead of only the original post. 2024-03-01 14:50:28 -05:00
Vitor Pamplona b1a355691a Fixes another caching issue when multiple posts include the same poll. 2024-03-01 13:41:43 -05:00
Vitor PamplonaandGitHub 4f2b2992e0 Merge pull request #791 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-01 12:41:09 -05:00
Crowdin Bot 77d30b6e7c New Crowdin translations by GitHub Action 2024-03-01 17:30:38 +00:00
Vitor Pamplona 228de99ccf Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-01 12:28:48 -05:00
Vitor Pamplona dfec7ae7f5 Better aligns header elements 2024-03-01 12:28:42 -05:00
Vitor PamplonaandGitHub f8c4242843 Merge pull request #790 from davotoula/adding_translations
Updated translations. DE, SV, PT, CS
2024-03-01 12:17:18 -05:00
Vitor PamplonaandGitHub 34150b6725 Merge branch 'main' into adding_translations 2024-03-01 12:17:12 -05:00
Vitor Pamplona 21a18cfa38 Rotates all versions of a note 2024-03-01 12:15:48 -05:00
David Kaspar 8fa6d1d490 Updated translations. DE, SV, PT, CS 2024-03-01 17:12:06 +00:00
Vitor Pamplona c23510e05e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-01 10:41:57 -05:00
Vitor Pamplona 1cc4fd5183 Closes popup when finished editing. 2024-03-01 10:40:43 -05:00
Vitor Pamplona 7468352795 Removes warnings from the quartz library. 2024-03-01 10:40:12 -05:00
Vitor Pamplona 238e577b93 avoids changing and reading the state at the same time 2024-03-01 10:39:49 -05:00
Vitor Pamplona 0653a69bbd Replaces deprecated put method 2024-03-01 10:39:32 -05:00
Vitor Pamplona 87fcba02b3 Move project to version catalogs 2024-03-01 10:38:03 -05:00
Vitor Pamplona 3fb8a5938f Updates to Android Studio Iguana 2024-03-01 08:03:58 -05:00
Vitor PamplonaandGitHub f2a9c2b34b Merge pull request #789 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-01 07:59:24 -05:00
Crowdin Bot cf1e5f952d New Crowdin translations by GitHub Action 2024-03-01 12:57:52 +00:00
Vitor Pamplona b6e096ec96 Only display edit option when looking at a text note. 2024-02-29 20:00:19 -05:00
Vitor Pamplona ee4e20bac8 Fixing alt of git reply 2024-02-29 19:58:18 -05:00
Vitor Pamplona 4d2a7bbc32 Fixes Poll Caching problems. 2024-02-29 18:51:39 -05:00
Vitor Pamplona 600aab5e8d Reverts the ReactionRow change due to misalignments 2024-02-29 18:51:27 -05:00
Vitor Pamplona 1efef5887a Reduces the cache size for parsed posts. 2024-02-29 18:50:55 -05:00
Vitor Pamplona 3495866017 Improving cache of cashu tokens 2024-02-29 18:03:07 -05:00
Vitor Pamplona 0755bd2474 Improving the cache of LnInvoices. 2024-02-29 18:02:52 -05:00
Vitor Pamplona d257f7e253 Fixes not being able to create an invoice because of an old cache of the lud16
Fixes inserting invoice in the cursor.
2024-02-29 17:35:44 -05:00
Vitor Pamplona 7497237763 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-29 17:06:22 -05:00
Vitor Pamplona d506cf13a9 Adjustments to padding of the last element and the size of the relay box in kind 1 2024-02-29 17:05:41 -05:00
Vitor PamplonaandGitHub 05454f5420 Merge pull request #788 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-29 15:10:18 -05:00
Crowdin Bot 8d8990899b New Crowdin translations by GitHub Action 2024-02-29 19:55:30 +00:00
Vitor Pamplona fd37826663 Refactoring to improve legibility and compose stability of FhirResources 2024-02-29 14:52:56 -05:00
Vitor Pamplona 0084c2b532 Improves the speed of Location services. 2024-02-29 14:19:50 -05:00
Vitor Pamplona c686e775bf Deletes unused code 2024-02-29 10:09:19 -05:00
Vitor Pamplona 6e0701b65d Removes unused kotlin serialization 2024-02-29 09:22:15 -05:00
Vitor Pamplona 417c29bd72 Updates Compose UI Version 2024-02-29 09:18:56 -05:00
Vitor Pamplona 3eb6983a94 Support for Kind 1 Edits 2024-02-28 17:42:06 -05:00
Vitor Pamplona d4ba135368 Second embed bundle tests with nsecs 2024-02-28 16:54:28 -05:00
Vitor Pamplona 7f35d7d416 Improves rendering of prescriptions 2024-02-28 16:52:33 -05:00
Vitor Pamplona 9f4a30657b Fixes Ots Observation check 2024-02-28 16:51:14 -05:00
Vitor Pamplona 86aab2e842 Fixes DM chatroom edit button 2024-02-28 16:50:58 -05:00
Vitor Pamplona 73e3b60721 Fixes missing nsec processing 2024-02-28 16:50:23 -05:00
Vitor Pamplona fdb0830d1b Refactors extra chars for Bech Links 2024-02-28 11:23:56 -05:00
Vitor Pamplona bc833fadcf Fixes event loading for nembeds 2024-02-28 11:23:05 -05:00
Vitor Pamplona d36efd6a2c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-27 20:56:31 -05:00
Vitor Pamplona 0d92384d8c Fixes OTS test cases 2024-02-27 19:16:06 -05:00
Vitor PamplonaandGitHub 466929a0f7 Merge pull request #787 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-27 18:56:30 -05:00
Crowdin Bot 85059d775b New Crowdin translations by GitHub Action 2024-02-27 23:52:02 +00:00
Vitor Pamplona 328ed226c6 Basic support for FHIR payloads.
Support for nembed
2024-02-27 18:50:20 -05:00
Vitor Pamplona e305f3c198 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-27 15:40:49 -05:00
Vitor Pamplona f78a252c8f - Adds nembed
- Redesigns NIP-19 returns to allow nembeds and give more nuance to the UI.
2024-02-27 15:39:15 -05:00
Vitor PamplonaandGitHub 6e65bdbb91 Merge pull request #786 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-27 10:40:49 -05:00
Crowdin Bot 9639514a82 New Crowdin translations by GitHub Action 2024-02-27 15:37:25 +00:00
Vitor Pamplona 87fabd54de Completed OTS Support 2024-02-27 10:34:49 -05:00
Vitor Pamplona 529403d92f Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-26 20:52:24 -05:00
Vitor Pamplona e2d86c1727 Adds a new report option as other 2024-02-26 20:52:15 -05:00
Vitor Pamplona 11ff0736e4 Speeding up the loop through local cache. 2024-02-26 20:51:59 -05:00
Vitor PamplonaandGitHub fbc63a6df1 Merge pull request #784 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-26 19:04:10 -05:00
Crowdin Bot 7dccac41e9 New Crowdin translations by GitHub Action 2024-02-26 23:53:45 +00:00
Vitor Pamplona 8ebad524ff Adds local cache to wait for OTS confirmations
Presents OTS pending and confirmations on the screen
Adds Menu item to OTS any post.
2024-02-26 18:51:23 -05:00
Vitor Pamplona bf67ff64b4 Moves the image preview on new posts to the top of additional options for the post. 2024-02-26 15:47:44 -05:00
Vitor Pamplona 85ecaf18cc Sets split zaps for all quotes and forks 2024-02-26 15:47:22 -05:00
Vitor Pamplona 3c36f52baf Visual support for Kind1 forks. 2024-02-26 15:17:17 -05:00
Vitor Pamplona 782926fbea Separates the q filter from the e filter to avoid and moves q to the last position since it can be cut off and won't affect the app. 2024-02-25 18:24:50 -05:00
Vitor Pamplona 21d141c5c2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-25 17:53:23 -05:00
Vitor Pamplona de17aab7ab Fixing stack overflow with more than 200 zaps in a single note. 2024-02-25 17:53:15 -05:00
Vitor PamplonaandGitHub d1b0707b54 Merge pull request #783 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-25 17:13:49 -05:00
Crowdin Bot 2ea80272e3 New Crowdin translations by GitHub Action 2024-02-25 22:06:59 +00:00
Vitor Pamplona 6a7d33e58f - Generalizing OTS Calendar access
- Developing OTS access points with OkHttp
2024-02-25 17:04:51 -05:00
Vitor Pamplona 880915e702 Better explains why the fork of javaOTS was needed. 2024-02-25 15:43:59 -05:00
Vitor Pamplona d6fd407632 Moves external signer from org.json to jackson parser 2024-02-25 15:43:42 -05:00
Vitor Pamplona 1c0854eeaa Moves from org.json to jackson in OTS replies 2024-02-25 15:43:18 -05:00
Vitor Pamplona 9eefb65047 Generalizes the interface for block explorers in OTS 2024-02-25 15:43:01 -05:00
Vitor Pamplona 31c9041461 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-25 11:50:01 -05:00
Vitor Pamplona c4d6b1a5d1 Adds support for OpenTimestamps in Quartz 2024-02-25 11:49:51 -05:00
Vitor Pamplona ad03d355b0 Adds internet for testing of OTS attestations 2024-02-25 11:45:59 -05:00
Vitor Pamplona 5ad86e5604 Adds support for the q tag together with the support for the mention marker. 2024-02-25 11:40:39 -05:00
Vitor Pamplona 6afd9a0191 Adds note on why the libscrypt was removed. 2024-02-25 11:39:48 -05:00
Vitor PamplonaandGitHub 58ff0b5500 Merge pull request #782 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-24 09:18:08 -05:00
Crowdin Bot 3f33871c9c New Crowdin translations by GitHub Action 2024-02-24 14:16:45 +00:00
Vitor PamplonaandGitHub ca4cc93b31 Fix Quartz use 2024-02-24 09:15:41 -05:00
Vitor Pamplona 49c2d4f486 Adding Basic Git Rendering support. 2024-02-23 22:09:06 -05:00
Vitor Pamplona b9c3123f18 Faster Longform tag filters 2024-02-23 18:08:55 -05:00
Vitor Pamplona 452911bf27 Removes the need to specify nulls for new TextNote creations 2024-02-23 18:08:30 -05:00
Vitor Pamplona 173e82d236 Adds Wikipage Kind 2024-02-23 17:55:39 -05:00
Vitor Pamplona 2819c71e85 Updates Coil and Google Service dependencies 2024-02-23 16:59:47 -05:00
Vitor PamplonaandGitHub 3eac579638 Fixing Format 2024-02-23 14:29:14 -05:00
Vitor PamplonaandGitHub d606cef90e Adds Jitpack instructions for Quartz 2024-02-23 14:27:20 -05:00
Vitor PamplonaandGitHub de8870f240 Merge pull request #780 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-23 13:45:11 -05:00
Crowdin Bot 13031223a4 New Crowdin translations by GitHub Action 2024-02-23 18:22:53 +00:00
Vitor PamplonaandGitHub 553500460a Merge pull request #779 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-23 13:21:41 -05:00
Vitor Pamplona a7e601beee Keeping signing and util classes in the final jar 2024-02-23 13:21:25 -05:00
Crowdin Bot d21792a46f New Crowdin translations by GitHub Action 2024-02-23 17:17:02 +00:00
Vitor Pamplona b8b9aaf4aa Adds time utils tests to see if jitpack exports it 2024-02-23 12:15:20 -05:00
Vitor Pamplona dd160ecb71 Improves relay list NIP-11 caching, updates NIP-11 properties in the document and adds icon rendering from the NIP11 document. 2024-02-22 18:43:46 -05:00
Vitor Pamplona 2ee4b24c9b Moves Relay information to Quartz
Renames Nip19 object for better readability
2024-02-22 16:08:36 -05:00
Vitor Pamplona 606c483d09 Moves Wallet Connect to Quartz 2024-02-22 15:40:07 -05:00
Vitor Pamplona b1d1c4089c Speeds up some of image calculations 2024-02-22 14:46:18 -05:00
Vitor Pamplona 5740b75a64 Adds a few more benchmarks 2024-02-22 14:05:47 -05:00
Vitor Pamplona 8c9971d9f2 Moves benchmark tests to the benchmark release type 2024-02-22 13:59:59 -05:00
Vitor Pamplona 860c3773c3 Moves emoji parsers to commons and 2024-02-22 13:56:56 -05:00
Vitor Pamplona 8ff4ac3709 Inserts uploaded URLs where the cursor is and not at the end of the file. 2024-02-22 09:54:21 -05:00
Vitor Pamplona 5b77e39c8b Improves the speed of the text parser. 2024-02-21 19:18:30 -05:00
Vitor Pamplona 5886c866d3 Makes the name of the test follow the refactored name 2024-02-21 16:24:06 -05:00
Vitor Pamplona 96f29fc5ca Refactoring some names for the parsers. 2024-02-21 16:22:17 -05:00
Vitor Pamplona 90175198f0 Preparing to move Kind1 content parsers to commons. 2024-02-21 15:25:40 -05:00
Vitor Pamplona 4b9a55e178 Increasing the speed of the Robohash SVG to buffer function 2024-02-21 13:51:27 -05:00
Vitor Pamplona 0429c4dc3a Faster comparisons on GiftWrap Benchmarks 2024-02-21 11:25:08 -05:00
Vitor Pamplona e3f3350c85 Fixes Robohash test 2024-02-21 11:24:51 -05:00
Vitor Pamplona 5cd1c5ab90 Faster hex validation 2024-02-21 11:23:32 -05:00
Vitor Pamplona d982f029e4 Fixes Ukrainian translation 2024-02-21 10:22:20 -05:00
Vitor Pamplona f44bc02767 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-21 10:13:18 -05:00
Vitor PamplonaandGitHub 2e096069b9 Merge pull request #778 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-21 10:04:53 -05:00
Vitor Pamplona f10dd38126 refactoring on method names to make them clearer 2024-02-21 10:04:40 -05:00
Crowdin Bot 74b1cad578 New Crowdin translations by GitHub Action 2024-02-21 15:02:57 +00:00
Vitor Pamplona dbcf59779e Fix benchmark test 2024-02-21 09:59:39 -05:00
Vitor Pamplona f4cee92702 Fixes the crash on images not being present in the image pager. 2024-02-20 16:51:18 -05:00
Vitor Pamplona b70407f447 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-20 15:52:32 -05:00
Vitor Pamplona 444857ad95 Updating Vico 2024-02-20 15:52:25 -05:00
Vitor Pamplona ae291d278f Fixes text's vertical alignment when emoji's are present. 2024-02-20 12:28:46 -05:00
Vitor Pamplona 0d44c44c6e Adds release note ID to gradle. 2024-02-20 11:30:27 -05:00
Vitor Pamplona a03f9d2b98 Fixes position of the notification dot 2024-02-20 11:30:11 -05:00
Vitor PamplonaandGitHub 49a090da00 Merge pull request #777 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-20 11:00:50 -05:00
Crowdin Bot c8d11fdcac New Crowdin translations by GitHub Action 2024-02-20 15:59:43 +00:00
Vitor PamplonaandGitHub 33342b85d1 Merge pull request #776 from davotoula/update_translations
Updated translations. DE, SV, PT, CS
2024-02-20 10:58:17 -05:00
David Kaspar a148da4b7b Updated translations. DE, SV, PT, CS 2024-02-20 11:40:16 +00:00
Vitor Pamplona 1b93fc57cb Creates a way to ask for donations in the Notification page. 2024-02-19 18:57:21 -05:00
Vitor Pamplona 2514b74883 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-19 18:55:38 -05:00
Vitor Pamplona a97ade41fa adds a release drafter to test 2024-02-19 18:55:29 -05:00
Vitor PamplonaandGitHub e44ee1e549 Merge pull request #775 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-19 08:31:44 -05:00
Crowdin Bot b170d04d5e New Crowdin translations by GitHub Action 2024-02-19 13:20:13 +00:00
Vitor Pamplona dc55d71486 Removing the need to recreate modifiers on BottomBar 2024-02-19 08:17:44 -05:00
Vitor Pamplona dd071dc65a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-17 13:54:45 -05:00
Vitor Pamplona dd7427971c refactoring 2024-02-17 13:53:53 -05:00
Vitor PamplonaandGitHub d715ea44df Typo 2024-02-17 12:39:48 -05:00
Vitor PamplonaandGitHub 7af2fcdb27 Adds playstore badge 2024-02-17 11:51:42 -05:00
Vitor PamplonaandGitHub 879f7cf8e3 Smaller download badges 2024-02-17 11:29:18 -05:00
Vitor PamplonaandGitHub 88033c2acd Update README.md 2024-02-17 11:16:45 -05:00
Vitor PamplonaandGitHub e7fc5545ba Update README.md 2024-02-17 11:15:19 -05:00
Vitor PamplonaandGitHub e7240c0acf Using an SVG as readme logo 2024-02-17 11:09:40 -05:00
Vitor PamplonaandGitHub 1fa8cd95dc Improves Layout of Readme 2024-02-17 11:05:40 -05:00
Vitor Pamplona 9424147a08 v0.84.3 2024-02-16 16:00:00 -05:00
Vitor Pamplona 26356d6060 simple refactoring 2024-02-16 15:51:43 -05:00
Vitor Pamplona 56bc10b513 Speeds up set building 2024-02-16 15:49:57 -05:00
Vitor Pamplona 67a1aae1d0 Avoids the need to tag a cited user to count as a citation. 2024-02-16 15:49:42 -05:00
Vitor Pamplona b88ab06154 Counts quotes as retweets in the notification stats bar 2024-02-16 15:48:54 -05:00
Vitor Pamplona 7d9784ee44 Updates secp256k1 2024-02-16 15:40:53 -05:00
Vitor Pamplona 8034fd0975 Adds normalizing test for emoji passwords in NIP49 2024-02-16 13:53:06 -05:00
Vitor Pamplona 42cc04c194 updates license notice year 2024-02-15 18:31:26 -05:00
Vitor Pamplona 52c6ce4849 Updates scrypt lib to support empty keys. 2024-02-15 17:42:19 -05:00
Vitor Pamplona 6553f3bec5 Normalizes passwords to NFKC in NIP49 2024-02-15 16:05:53 -05:00
Vitor Pamplona 03a9ec8ed2 Refactors clickable text 2024-02-15 11:03:01 -05:00
Vitor Pamplona 133d7d5ce0 v0.84.2 2024-02-15 11:00:23 -05:00
Vitor Pamplona 215989108b Fixes misalignment of Clickable Texts 2024-02-15 11:00:15 -05:00
Vitor Pamplona ed7dcf94f8 Fixes extra spaces after urls 2024-02-15 10:59:52 -05:00
Vitor PamplonaandGitHub b2c7f150ec Merge pull request #774 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-15 08:46:02 -05:00
Crowdin Bot c85d74a688 New Crowdin translations by GitHub Action 2024-02-15 13:41:21 +00:00
Vitor PamplonaandGitHub cbf23fedf1 Merge pull request #773 from davotoula/update_translations
Update translations
2024-02-15 08:39:53 -05:00
David Kaspar 5ed64bd5b5 Change to ellipsis character 2024-02-15 12:19:12 +00:00
David Kaspar b79f9c8395 Updated translations. DE, SV, PT, CS 2024-02-15 12:18:19 +00:00
Vitor Pamplona 02da606461 v0.84.1 2024-02-14 17:49:07 -05:00
Vitor Pamplona 7074446cae Removing scrypt native library that doesn't work on Android 2024-02-14 17:48:57 -05:00
Vitor Pamplona 4746b5b18a removes unneeded translation keys 2024-02-14 17:40:50 -05:00
fiatjafandVitor Pamplona 103a61f35c useless pt-BR translation changes. 2024-02-14 17:29:28 -05:00
Vitor Pamplona d9c48d4039 v0.84.0 2024-02-14 17:09:10 -05:00
Vitor Pamplona 9479e86b66 Enabled citation via @ on chats 2024-02-14 16:52:25 -05:00
Vitor PamplonaandGitHub bb7ec1ee4b Merge pull request #772 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-14 12:59:57 -05:00
Crowdin Bot 7a213545a2 New Crowdin translations by GitHub Action 2024-02-14 17:56:37 +00:00
Vitor Pamplona 48f27f656d Adds NIP49 to login and back up key screens 2024-02-14 12:54:08 -05:00
Vitor Pamplona 47176421e0 Adds copyright sentence. 2024-02-14 09:17:39 -05:00
Vitor Pamplona 9c84a538d6 Removes the NSEC form nip19 2024-02-13 20:49:21 -05:00
Vitor Pamplona 8bc1c2ec68 Removing nip19 test with ncryptosec 2024-02-13 20:44:00 -05:00
Vitor Pamplona b94c1e2792 Quartz support for NIP-49 encrypted nsecs 2024-02-13 19:51:32 -05:00
Vitor Pamplona 74f0393706 Adds "₿itcoin" to the set of custom hashtags 2024-02-13 12:42:46 -05:00
Vitor PamplonaandGitHub 0ec4b9b59f Merge pull request #771 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-13 11:58:43 -05:00
Crowdin Bot 1592d55db5 New Crowdin translations by GitHub Action 2024-02-13 16:43:27 +00:00
Vitor Pamplona 629c8b8167 Avoids the memory use of the flatten operation 2024-02-13 11:40:42 -05:00
Vitor Pamplona 9d8f70e04b No need to remember this param. 2024-02-13 11:39:54 -05:00
Vitor Pamplona f75104e616 Updates Compose dependencies 2024-02-13 11:38:55 -05:00
Vitor Pamplona c9d59fd5ac Adds a check for the main thread when pulling opengraph tags. 2024-02-13 11:38:28 -05:00
Vitor Pamplona d41eb4ce31 No need to crossfade when expanding content 2024-02-12 18:11:28 -05:00
Vitor Pamplona 60e87d943a v0.83.13 2024-02-12 16:09:03 -05:00
Vitor Pamplona 1deed59d5b Fixes: https://github.com/vitorpamplona/amethyst/issues/740
Displays error messages when the URI sent to Amethyst is invalid
2024-02-12 16:03:37 -05:00
Vitor Pamplona bbc4ec2625 Fixes thread rendering when "mention" events are added with mentioning the event. 2024-02-12 15:10:48 -05:00
Vitor Pamplona 5109b65e37 Fixes crash when MuteList has e tags that are not valid hexes 2024-02-12 14:21:32 -05:00
Vitor Pamplona c0a9ce8e21 Small refactoring to focus the encrypted storage procedures to the application context. 2024-02-12 11:52:18 -05:00
Vitor Pamplona bdff8c6c0d Moves language translation cleanup to the IO thread since it clears disk files as well 2024-02-12 11:15:59 -05:00
Vitor PamplonaandGitHub a3ac8a445d Merge pull request #768 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-10 10:22:09 -05:00
Crowdin Bot 84d4d68116 New Crowdin translations by GitHub Action 2024-02-10 15:00:43 +00:00
Vitor Pamplona 27248bff17 Invert the order of the hidden users in the screen. 2024-02-10 09:59:01 -05:00
Vitor PamplonaandGitHub dc9a9f288b Merge pull request #767 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-10 08:40:44 -05:00
Crowdin Bot 88662a4e55 New Crowdin translations by GitHub Action 2024-02-10 13:27:57 +00:00
Vitor PamplonaandGitHub 9d269f7cba Merge pull request #766 from davotoula/update_translations
Updated translations. DE, SV, PT
2024-02-10 08:26:53 -05:00
David Kaspar 04fe439e0b Updated translations. DE, SV, PT 2024-02-10 09:45:17 +00:00
Vitor Pamplona 72503b9061 Adds community and public chat list events from nip51 2024-02-09 15:54:56 -05:00
Vitor Pamplona f7ab963db9 Small fixes in the use of modifiers 2024-02-09 14:37:49 -05:00
Vitor Pamplona f97128e11c Fixing accessibility issues raised on https://github.com/vitorpamplona/amethyst/issues/765 2024-02-08 17:16:54 -05:00
Vitor Pamplona e2d4107297 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-08 14:54:04 -05:00
Vitor Pamplona 18709a1b22 Disables the notification dot in the discovery tab 2024-02-08 14:53:53 -05:00
Vitor Pamplona ddfe2abb42 Fixes send to Top in marketplaces. 2024-02-08 14:53:35 -05:00
Vitor PamplonaandGitHub 0cf82f7edd Merge pull request #764 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-07 16:37:18 -05:00
Crowdin Bot d1c40efd90 New Crowdin translations by GitHub Action 2024-02-07 21:16:44 +00:00
Vitor Pamplona 68e90ce3ed Unwrapps the reply message if the GiftWrap was tagged and not the inner message id. 2024-02-07 16:14:48 -05:00
552 changed files with 20091 additions and 4179 deletions
+1 -1
View File
@@ -227,4 +227,4 @@ jobs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
asset_content_type: application/zip
asset_content_type: application/zip
+86 -43
View File
@@ -1,26 +1,46 @@
# Amethyst: Nostr client for Android
<div align="center">
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px"/>
<a href="https://amethyst.social">
<img src="./docs/design/3rd%20Logo%20-%20Zitron/amethyst.svg" alt="Amethyst Logo" title="Amethyst logo" width="80"/>
</a>
Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.
# Amethyst
## Nostr Client for Android
Join the social network you control.
[![GitHub downloads](https://img.shields.io/github/downloads/vitorpamplona/amethyst/total?label=Downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/vitorpamplona/amethyst/releases)
[![PlayStore downloads](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dcom.vitorpamplona.amethyst%26gl%3DUS%26hl%3Den%26l%3DPlayStore%26m%3D%24shortinstalls)](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
[![Last Version](https://img.shields.io/github/release/vitorpamplona/amethyst.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/vitorpamplona/amethyst)
[![JitPack version](https://jitpack.io/v/vitorpamplona/amethyst.svg)](https://jitpack.io/#vitorpamplona/amethyst)
[![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE)
## Download and Install
[<img src="./docs/design/obtainium.png"
alt="Get it on Obtaininum"
height="80">](https://github.com/ImranR98/Obtainium)
height="70">](https://github.com/ImranR98/Obtainium)
[<img src="https://github.com/machiav3lli/oandbackupx/raw/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub"
height="80">](https://github.com/vitorpamplona/amethyst/releases)
height="70">](https://github.com/vitorpamplona/amethyst/releases)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
height="70">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
# Current Features
</div>
## Supported Features
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px">
- [x] Events / Relay Subscriptions (NIP-01)
- [x] Follow List (NIP-02)
- [ ] OpenTimestamps Attestations (NIP-03)
- [x] OpenTimestamps Attestations (NIP-03)
- [x] Private Messages (NIP-04)
- [x] DNS Address (NIP-05)
- [ ] Mnemonic seed phrase (NIP-06)
@@ -50,7 +70,9 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
- [x] Event kind summaries (NIP-31)
- [ ] Labeling (NIP-32)
- [x] Parameterized Replaceable Events (NIP-33)
- [x] Git Stuff (NIP-34/Draft)
- [x] Sensitive Content (NIP-36)
- [x] Note Edits (NIP-37/Draft)
- [x] User Status Event (NIP-38)
- [x] External Identities (NIP-39)
- [x] Expiration Support (NIP-40)
@@ -59,7 +81,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
- [ ] Nostr Connect (NIP-46)
- [x] Wallet Connect API (NIP-47)
- [ ] Proxy Tags (NIP-48, Not applicable)
- [ ] Private key encryption for import/export (NIP-49)
- [x] Private key encryption for import/export (NIP-49)
- [x] Online Relay Search (NIP-50)
- [x] Lists (NIP-51)
- [ ] Calendar Events (NIP-52)
@@ -89,7 +111,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
- [x] Classifieds (NIP-99)
- [x] Private Messages and Small Groups (NIP-24/Draft)
- [x] Versioned Encrypted Payloads (NIP-44/Draft)
- [x] Audio Tracks (zapstr.live) (Kind:31337)
- [x] Audio Tracks (zapstr.live) (kind:31337)
- [x] Push Notifications (Google and Unified Push)
- [x] In-Device Automatic Translations
- [x] Hashtag Following and Custom Hashtags
@@ -98,36 +120,49 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
- [x] De-googled F-Droid flavor
- [x] Multiple Accounts
- [x] Markdown Support
- [x] FHIR Payloads (kind:82)
- [ ] Decentralized Wiki (kind:30818)
- [ ] Embed events
- [ ] Image/Video Capture in the app
- [ ] Local Database
- [ ] Workspaces
- [ ] Infinity Scroll
## Privacy and Information Permanence
Relays know your IP address, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) except for Private Zaps and Private DMs. While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN or Tor) from trackers online.
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
Information shared on Nostr can be re-broadcasted to other servers and should be assumed permanent for privacy purposes. There is no way to guarantee the deletion of any content once posted.
# Development Overview
## Overall Architecture
This repository is split between Amethyst and Quartz:
- Amethyst is a native Android app made with Kotlin and Jetpack Compose.
- Quartz is our own Nostr-commons library to host classes that are of interest to other Nostr Clients.
This is a native Android app made with Kotlin and Jetpack Compose.
The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays.
The overall architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
The app architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData and Flow objects to
allow the UI and other parts of the app to subscribe to each Note/User and receive updates when they happen.
They are also responsible for updating viewModels when needed. Filters react to changes in the screen. As the user
sees different Events, the Datasource classes are used to receive more information about those particular Events.
They are also responsible for updating viewModels when needed. As the user scrolls through Events, the Datasource classes
are updated to receive more information about those particular Events.
Most of the UI is reactive to changes in the repository classes. The service layer assembles Nostr filters for each need of the app,
receives the data from the Relay, and sends it to the repository. Connection with relays is never closed during the use of the app.
The UI receives a notification that objects have been updated. Instances of User and Notes are mutable directly.
There will never be two Notes with the same ID or two User instances with the same pubkey.
Lastly, the user's account information (priv key/pub key) is stored in the Android KeyStore for security.
Lastly, the user's account information (private key/pub key) is stored in the Android KeyStore for security.
## Setup
Make sure to have the following pre-requisites installed:
1. Java 17
1. Java 17+
2. Android Studio
3. Android 8.0+ Phone or Emulation setup
@@ -136,7 +171,7 @@ Fork and clone this repository and import it into Android Studio
git clone https://github.com/vitorpamplona/amethyst.git
```
Use one of the Android Studio builds to install and run the app in your device or a simulator.
Use an Android Studio build action to install and run the app on your device or a simulator.
## Building
Build the app:
@@ -168,15 +203,7 @@ For the Play build:
./gradlew installPlayDebug
```
## Contributing
[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also very welcome.
Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social)
You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac)
## How to Deploy
## Deploying
1. Generate a new signing key
```
@@ -195,23 +222,37 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
7. Add your CHANGE LOG to the description of the new release
8. Download the `aab` file and upload it to the PlayStore.
# Privacy on Relays & nostr
Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
## Using the Quartz library
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
Setup [JitPack.io](https://jitpack.io/#vitorpamplona/amethyst/v0.84.3) to your build file
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of Private Zaps and Private DMs.
Add `maven { url 'https://jitpack.io' }` to settings.gradle at the end of repositories:
# DM Privacy #
While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
```gradle
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
```
# Visibility & Permanence of Your Content on nostr
## Information Visibility ##
Content that you share can be shared to other relays.
Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
Add the dependency
## Information Permanence ##
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted.
```gradle
implementation('com.github.vitorpamplona.amethyst:quartz:v0.85.1')
```
## Contributing
Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst)
[GitHub issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also welcome. Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social)
You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac)
By contributing to this repository, you agree to license your work under the MIT license. Any work contributed where you are not the original author must contain its license header with the original author(s) and source.
# Screenshots
@@ -219,7 +260,7 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th
|-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------|
| ![Home Feed](./docs/screenshots/home.png) | ![Messages](./docs/screenshots/messages.png) | ![Live Streams](./docs/screenshots/replies.png) | ![Notifications](./docs/screenshots/notifications.png) |
## Contributors
# Contributors
<a align="center" href="https://github.com/vitorpamplona/amethyst/graphs/contributors">
<img src="https://contrib.rocks/image?repo=vitorpamplona/amethyst" />
@@ -227,6 +268,7 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th
# MIT License
<pre>
Copyright (c) 2023 Vitor Pamplona
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -246,3 +288,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</pre>
+68 -65
View File
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.gms.google-services'
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.googleServices)
}
android {
@@ -12,8 +12,9 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 353
versionName "0.83.12"
versionCode 362
versionName "0.85.3"
buildConfigField "String", "RELEASE_NOTES_ID", "\"d8da33fd13d129d86c53564aedefafbe3716f007c520431be4a8e488d3925afb\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -144,13 +145,13 @@ android {
// Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin
kotlinCompilerExtensionVersion "1.5.8"
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
excludes += ['/META-INF/{AL2.0,LGPL2.1}', '**/libscrypt.dylib']
}
}
lint {
disable 'MissingTranslation'
}
@@ -163,75 +164,78 @@ android {
dependencies {
implementation project(path: ':quartz')
implementation project(path: ':commons')
implementation "androidx.core:core-ktx:$core_ktx_version"
implementation 'androidx.activity:activity-compose:1.8.2'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation libs.androidx.core.ktx
implementation libs.androidx.activity.compose
implementation platform(libs.androidx.compose.bom)
implementation libs.androidx.ui
implementation libs.androidx.ui.graphics
implementation libs.androidx.ui.tooling.preview
// Needs this to open gallery / image upload
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation libs.androidx.fragment.ktx
// Navigation
implementation "androidx.navigation:navigation-compose:$nav_version"
implementation libs.androidx.navigation.compose
// Observe Live data as State
implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version"
implementation libs.androidx.runtime.livedata
// Material 3 Design
implementation "androidx.compose.material3:material3:${material3_version}"
implementation "androidx.compose.material:material-icons-extended:$compose_ui_version"
implementation libs.androidx.material3
implementation libs.androidx.material.icons
// Adaptive Layout / Two Pane
implementation "androidx.compose.material3:material3-window-size-class:${material3_version}"
implementation 'com.google.accompanist:accompanist-adaptive:0.34.0'
implementation libs.androidx.material3.windowSize
implementation libs.accompanist.adaptive
// Lifecycle
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.lifecycle.runtime.compose
implementation libs.androidx.lifecycle.viewmodel.compose
implementation libs.androidx.lifecycle.livedata.ktx
// Zoomable images
implementation 'net.engawapg.lib:zoomable:1.6.0'
implementation libs.zoomable
// Biometrics
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
implementation libs.androidx.biometric.ktx
// Websockets API
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
implementation libs.okhttp
// HTML Parsing for Link Preview
implementation 'org.jsoup:jsoup:1.17.2'
implementation libs.jsoup
// Encrypted Key Storage
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06'
implementation libs.androidx.security.crypto.ktx
// view videos
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
implementation "androidx.media3:media3-ui:$media3_version"
implementation "androidx.media3:media3-session:$media3_version"
implementation libs.androidx.media3.exoplayer
implementation libs.androidx.media3.exoplayer.hls
implementation libs.androidx.media3.ui
implementation libs.androidx.media3.session
// important for proxy / tor
implementation "androidx.media3:media3-datasource-okhttp:$media3_version"
implementation libs.androidx.media3.datasource.okhttp
// Load images from the web.
implementation "io.coil-kt:coil-compose:$coil_version"
implementation libs.coil.compose
// view gifs
implementation "io.coil-kt:coil-gif:$coil_version"
implementation libs.coil.gif
// view svgs
implementation "io.coil-kt:coil-svg:$coil_version"
implementation libs.coil.svg
// create blurhash
implementation group: 'io.trbl', name: 'blurhash', version: '1.0.0'
implementation libs.trbl.blurhash
// Permission to upload pictures:
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
implementation libs.accompanist.permissions
// For QR generation
implementation 'com.google.zxing:core:3.5.3'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation libs.zxing
implementation libs.zxing.embedded
// Markdown
//implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
@@ -239,51 +243,50 @@ dependencies {
//implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
// Markdown (With fix for full-image bleeds)
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:48702a8ced')
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material3:48702a8ced')
implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:48702a8ced')
implementation libs.markdown.ui
implementation libs.markdown.ui.material3
implementation libs.markdown.commonmark
// Language picker and Theme chooser
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation libs.androidx.appcompat
// Local model for language identification
playImplementation 'com.google.mlkit:language-id:17.0.4'
playImplementation libs.google.mlkit.language.id
// Google services model the translate text
playImplementation 'com.google.mlkit:translate:17.0.2'
playImplementation libs.google.mlkit.translate
// PushNotifications
playImplementation platform('com.google.firebase:firebase-bom:32.7.1')
playImplementation 'com.google.firebase:firebase-messaging-ktx'
playImplementation platform(libs.firebase.bom)
playImplementation libs.firebase.messaging
//PushNotifications(FDroid)
fdroidImplementation 'com.github.UnifiedPush:android-connector:2.2.0'
fdroidImplementation libs.unifiedpush
// Charts
implementation "com.patrykandpatrick.vico:core:${vico_version}"
implementation "com.patrykandpatrick.vico:compose:${vico_version}"
implementation "com.patrykandpatrick.vico:views:${vico_version}"
implementation "com.patrykandpatrick.vico:compose-m2:${vico_version}"
implementation libs.vico.charts.core
implementation libs.vico.charts.compose
implementation libs.vico.charts.views
implementation libs.vico.charts.m3
// GeoHash
implementation 'com.github.drfonfon:android-kotlin-geohash:1.0'
implementation libs.drfonfon.geohash
// Waveform visualizer
implementation 'com.github.lincollincol:compose-audiowaveform:1.1.1'
implementation libs.audiowaveform
// Video compression lib
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.2'
implementation libs.abedElazizShe.image.compressor
// Image compression lib
implementation 'id.zelory:compressor:3.0.1'
implementation libs.zelory.video.compressor
testImplementation 'junit:junit:4.13.2'
testImplementation 'io.mockk:mockk:1.13.9'
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha03'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha03'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
testImplementation libs.junit
testImplementation libs.mockk
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.junit.ktx
androidTestImplementation libs.androidx.espresso.core
debugImplementation libs.androidx.ui.tooling
debugImplementation libs.androidx.ui.test.manifest
}
// https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds#differing-assetsdexoptbaselineprofm-easy-to-fix
+5
View File
@@ -33,6 +33,11 @@
# JNA For Libsodium
-keep class com.goterl.lazysodium.** { *; }
# libscrypt
-keep class com.lambdaworks.codec.** { *; }
-keep class com.lambdaworks.crypto.** { *; }
-keep class com.lambdaworks.jni.** { *; }
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
-keep class com.sun.jna.ToNativeConverter { *; }
-keep class com.sun.jna.NativeMapped { *; }
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.ots.OpenTimestamps
import com.vitorpamplona.quartz.signers.NostrSignerInternal
import junit.framework.TestCase.assertEquals
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class OkHttpOtsTest {
val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}"
val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}"
val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}"
val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6"
@Before
fun setup() {
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
}
@Test
fun verifyNostrEvent() {
val ots = Event.fromJson(otsEvent) as OtsEvent
println(ots.info())
assertEquals(1707688818L, ots.verify())
}
@Test
fun verifyNostrEvent2() {
val ots = Event.fromJson(otsEvent2) as OtsEvent
println(ots.info())
assertEquals(1706322179L, ots.verify())
}
@Test
fun verifyNostrPendingEvent() {
val ots = Event.fromJson(otsPendingEvent) as OtsEvent
println(ots.info())
assertEquals(null, ots.verify())
}
@Test
fun createOTSEventAndVerify() {
val signer = NostrSignerInternal(KeyPair())
var ots: OtsEvent? = null
val countDownLatch = CountDownLatch(1)
val otsFile = OtsEvent.stamp(otsEvent2Digest)
OtsEvent.create(otsEvent2Digest, otsFile, signer) {
ots = it
countDownLatch.countDown()
}
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
println(ots!!.toJson())
println(ots!!.info())
// Should not be valid because we need to wait for confirmations
assertEquals(null, ots!!.verify())
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -26,9 +26,14 @@ import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import coil.ImageLoader
import coil.disk.DiskCache
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.amethyst.service.playback.VideoCache
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.ots.OpenTimestamps
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -52,7 +57,7 @@ class Amethyst : Application() {
newCache
}
private val imageCache: DiskCache by lazy {
val coilCache: DiskCache by lazy {
DiskCache.Builder()
.directory(applicationContext.safeCacheDir.resolve("image_cache"))
.maxSizePercent(0.2)
@@ -64,6 +69,8 @@ class Amethyst : Application() {
super.onCreate()
instance = this
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
ThreadPolicy.Builder().detectAll().penaltyLog().build(),
@@ -84,7 +91,11 @@ class Amethyst : Application() {
}
fun imageLoaderBuilder(): ImageLoader.Builder {
return ImageLoader.Builder(applicationContext).diskCache { imageCache }
return ImageLoader.Builder(applicationContext).diskCache { coilCache }
}
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences {
return EncryptedStorage.preferences(instance, npub)
}
companion object {
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -20,32 +20,35 @@
*/
package com.vitorpamplona.amethyst
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
object EncryptedStorage {
private const val PREFERENCES_NAME = "secret_keeper"
class EncryptedStorage {
companion object {
private const val PREFERENCES_NAME = "secret_keeper"
// returns the preferences for each account or a global file if null.
fun prefsFileName(npub: String? = null): String {
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
}
// returns the preferences for each account or a global file if null.
fun prefsFileName(npub: String? = null): String {
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
}
fun preferences(npub: String? = null): EncryptedSharedPreferences {
val context = Amethyst.instance
val masterKey: MasterKey =
MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
fun preferences(
applicationContext: Context,
npub: String? = null,
): EncryptedSharedPreferences {
val masterKey: MasterKey =
MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val preferencesName = prefsFileName(npub)
return EncryptedSharedPreferences.create(
context,
preferencesName,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
) as EncryptedSharedPreferences
return EncryptedSharedPreferences.create(
applicationContext,
prefsFileName(npub),
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
) as EncryptedSharedPreferences
}
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -33,7 +33,6 @@ import com.vitorpamplona.amethyst.model.DefaultReactions
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.Nip47URI
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.model.ThemeType
@@ -44,6 +43,8 @@ import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
@@ -112,6 +113,8 @@ private object PrefKeys {
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
const val SIGNER_PACKAGE_NAME = "signer_package_name"
const val HAS_DONATED_IN_VERSION = "has_donated_in_version"
const val PENDING_ATTESTATIONS = "pending_attestations"
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
const val SHARED_SETTINGS = "shared_settings"
@@ -233,7 +236,7 @@ object LocalPreferences {
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
} else {
return EncryptedStorage.preferences(npub)
return Amethyst.instance.encryptedStorage(npub)
}
}
@@ -331,12 +334,18 @@ object LocalPreferences {
PrefKeys.LAST_READ_PER_ROUTE,
Event.mapper.writeValueAsString(account.lastReadPerRoute),
)
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
if (account.showSensitiveContent == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
}
putString(
PrefKeys.PENDING_ATTESTATIONS,
Event.mapper.writeValueAsString(account.pendingAttestations),
)
}
.apply()
}
@@ -521,7 +530,7 @@ object LocalPreferences {
val zapPaymentRequestServer =
try {
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
Event.mapper.readValue<Nip47URI?>(it)
Event.mapper.readValue<Nip47WalletConnect.Nip47URI?>(it)
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
@@ -554,6 +563,26 @@ object LocalPreferences {
null
}
val pendingAttestations =
try {
getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let {
println("Decoding Attestation List: " + it)
if (it != null) {
Event.mapper.readValue<Map<HexKey, String>>(it)
} else {
null
}
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}",
e,
)
null
}
val languagePreferences =
try {
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
@@ -617,6 +646,8 @@ object LocalPreferences {
NostrSignerInternal(keyPair)
}
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
val account =
Account(
keyPair = keyPair,
@@ -644,6 +675,8 @@ object LocalPreferences {
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,
hasDonatedInVersion = hasDonatedInVersion,
pendingAttestations = pendingAttestations ?: emptyMap(),
)
// Loads from DB
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -31,6 +31,7 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
@@ -43,6 +44,7 @@ import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.BookmarkListEvent
@@ -65,6 +67,7 @@ import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GeneralListEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LnZapEvent
@@ -74,6 +77,7 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.NIP24Factory
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.Price
@@ -86,6 +90,7 @@ import com.vitorpamplona.quartz.events.Response
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
import com.vitorpamplona.quartz.events.WrappedEvent
import com.vitorpamplona.quartz.events.ZapSplitSetup
import com.vitorpamplona.quartz.signers.NostrSigner
@@ -96,6 +101,7 @@ import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -170,7 +176,7 @@ class Account(
var defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
var defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
var defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
var zapPaymentRequest: Nip47URI? = null,
var zapPaymentRequest: Nip47WalletConnect.Nip47URI? = null,
var hideDeleteRequestDialog: Boolean = false,
var hideBlockAlertDialog: Boolean = false,
var hideNIP24WarningDialog: Boolean = false,
@@ -181,10 +187,10 @@ class Account(
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true,
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
var hasDonatedInVersion: Set<String> = setOf<String>(),
var pendingAttestations: Map<HexKey, String> = mapOf<HexKey, String>(),
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
) {
// Uses a single scope for the entire application.
val scope = Amethyst.instance.applicationIOScope
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
data class PaymentRequest(
@@ -727,7 +733,7 @@ class Account(
zapResponseEvent.response(signer, onReady)
}
fun calculateIfNoteWasZappedByAccount(
suspend fun calculateIfNoteWasZappedByAccount(
zappedNote: Note?,
onWasZapped: () -> Unit,
) {
@@ -891,6 +897,38 @@ class Account(
}
}
suspend fun updateAttestations() {
Log.d("Pending Attestations", "Updating ${pendingAttestations.size} pending attestations")
pendingAttestations.toMap().forEach { pair ->
val newAttestation = OtsEvent.upgrade(pair.value, pair.key)
if (pair.value != newAttestation) {
OtsEvent.create(pair.key, newAttestation, signer) {
LocalCache.justConsume(it, null)
Client.send(it)
pendingAttestations = pendingAttestations - pair.key
}
}
}
}
fun hasPendingAttestations(note: Note): Boolean {
val id = note.event?.id() ?: note.idHex
return pendingAttestations.get(id) != null
}
fun timestamp(note: Note) {
if (!isWriteable()) return
val id = note.event?.id() ?: note.idHex
pendingAttestations = pendingAttestations + Pair(id, OtsEvent.stamp(id))
saveable.invalidateData()
}
fun follow(user: User) {
if (!isWriteable()) return
@@ -1166,7 +1204,7 @@ class Account(
Client.send(signedEvent, relayList = relayList)
LocalCache.consume(signedEvent, null)
return LocalCache.notes[signedEvent.id]
return LocalCache.getNoteIfExists(signedEvent.id)
}
fun consumeNip95(
@@ -1176,7 +1214,7 @@ class Account(
LocalCache.consume(data, null)
LocalCache.consume(signedEvent, null)
return LocalCache.notes[signedEvent.id]
return LocalCache.getNoteIfExists(signedEvent.id)
}
fun sendNip95(
@@ -1196,7 +1234,7 @@ class Account(
Client.send(signedEvent, relayList = relayList)
LocalCache.consume(signedEvent, null)
LocalCache.notes[signedEvent.id]?.let { onReady(it) }
LocalCache.getNoteIfExists(signedEvent.id)?.let { onReady(it) }
}
fun createHeader(
@@ -1312,6 +1350,63 @@ class Account(
}
}
fun sendGitReply(
message: String,
replyTo: List<Note>?,
mentions: List<User>?,
repository: ATag?,
zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean,
zapRaiserAmount: Long? = null,
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
forkedFrom: Event?,
relayList: List<Relay>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
) {
if (!isWriteable()) return
val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex }
val mentionsHex = mentions?.map { it.pubkeyHex }
val addresses = listOfNotNull(repository) + (replyTo?.mapNotNull { it.address() } ?: emptyList())
GitReplyEvent.create(
msg = message,
replyTos = repliesToHex,
mentions = mentionsHex,
addresses = addresses,
extraTags = null,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
replyingTo = replyingTo,
root = root,
directMentions = directMentions,
geohash = geohash,
nip94attachments = nip94attachments,
forkedFrom = forkedFrom,
signer = signer,
) {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
// broadcast replied notes
replyingTo?.let {
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
Client.send(it, relayList = relayList)
}
}
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList)
}
}
}
}
fun sendPost(
message: String,
replyTo: List<Note>?,
@@ -1323,6 +1418,7 @@ class Account(
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
forkedFrom: Event?,
relayList: List<Relay>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
@@ -1347,6 +1443,7 @@ class Account(
directMentions = directMentions,
geohash = geohash,
nip94attachments = nip94attachments,
forkedFrom = forkedFrom,
signer = signer,
) {
Client.send(it, relayList = relayList)
@@ -1367,6 +1464,29 @@ class Account(
}
}
fun sendEdit(
message: String,
originalNote: Note,
notify: HexKey?,
summary: String? = null,
relayList: List<Relay>? = null,
) {
if (!isWriteable()) return
val idHex = originalNote.event?.id() ?: return
TextNoteModificationEvent.create(
content = message,
eventId = idHex,
notify = notify,
summary = summary,
signer = signer,
) {
LocalCache.justConsume(it, null)
Client.send(it, relayList = relayList)
}
}
fun sendPoll(
message: String,
replyTo: List<Note>?,
@@ -2037,7 +2157,7 @@ class Account(
saveable.invalidateData()
}
fun changeZapPaymentRequest(newServer: Nip47URI?) {
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?) {
zapPaymentRequest = newServer
live.invalidateData()
saveable.invalidateData()
@@ -2354,6 +2474,16 @@ class Account(
return lastReadPerRoute[route] ?: 0
}
fun hasDonatedInThisVersion(): Boolean {
return hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
}
fun markDonatedInThisVersion() {
hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME
saveable.invalidateData()
live.invalidateData()
}
suspend fun registerObservers() =
withContext(Dispatchers.Main) {
// saves contact list for the next time.
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.events.Event
import kotlinx.coroutines.Dispatchers
@@ -58,7 +58,7 @@ class AntiSpamFilter {
// really long, make it ok.
// The idea here is to avoid considering repeated "@Bot, command" messages spam, while still
// blocking repeated "lnbc..." invoices or fishing urls
if (event.content.length < 180 && Nip19.nip19regex.matcher(event.content).find()) return false
if (event.content.length < 180 && Nip19Bech32.nip19regex.matcher(event.content).find()) return false
// double list strategy:
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
@@ -71,7 +71,7 @@ class AntiSpamFilter {
) {
Log.w(
"Potential SPAM Message for sharing",
"${Nip19.createNEvent(event.id, event.pubKey, event.kind, null)}",
"${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, null)}",
)
Log.w(
"Potential SPAM Message",
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -32,6 +32,7 @@ fun checkForHashtagWithIcon(
primary: Color,
): HashtagIcon? {
return when (tag.lowercase()) {
"₿itcoin",
"bitcoin",
"btc",
"timechain",
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -21,18 +21,17 @@
package com.vitorpamplona.amethyst.model
import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledInsert
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.HexValidator
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.decodeEventIdAsHexOrNull
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppDefinitionEvent
@@ -50,6 +49,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelHideMessageEvent
import com.vitorpamplona.quartz.events.ChannelListEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChannelMuteUserEvent
@@ -57,18 +57,24 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityListEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FhirResourceEvent
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileServersEvent
import com.vitorpamplona.quartz.events.FileStorageEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.GitIssueEvent
import com.vitorpamplona.quartz.events.GitPatchEvent
import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.GitRepositoryEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
@@ -80,6 +86,7 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.NNSEvent
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
@@ -92,8 +99,11 @@ import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
@@ -110,18 +120,31 @@ import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.measureTimedValue
object LocalCache {
val antiSpam = AntiSpamFilter()
val users = ConcurrentHashMap<HexKey, User>(5000)
val notes = ConcurrentHashMap<HexKey, Note>(5000)
private val users = ConcurrentHashMap<HexKey, User>(5000)
private val notes = ConcurrentHashMap<HexKey, Note>(5000)
val channels = ConcurrentHashMap<HexKey, Channel>()
val addressables = ConcurrentHashMap<String, AddressableNote>(100)
val awaitingPaymentRequests =
ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
var noteListCache: List<Note> = emptyList()
var userListCache: List<User> = emptyList()
fun updateListCache() {
val (value, elapsed) =
measureTimedValue {
noteListCache = ArrayList(notes.values)
userListCache = ArrayList(users.values)
}
Log.d("LocalCache", "UpdateListCache $elapsed")
}
fun checkGetOrCreateUser(key: String): User? {
// checkNotInMainThread()
@@ -358,6 +381,94 @@ object LocalCache {
refreshObservers(note)
}
fun consume(
event: GitPatchEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume(
event: GitIssueEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume(
event: GitReplyEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
val repository = event.repository()?.toTag()
val replyTo =
event
.tagsWithoutCitations()
.filter { it != repository }
.mapNotNull { checkGetOrCreateNote(it) }
// println("New GitReply ${event.id} for ${replyTo.firstOrNull()?.event?.id()} ${event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.firstOrNull()}")
note.loadEvent(event, author, replyTo)
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume(
event: LongTextNoteEvent,
relay: Relay?,
@@ -393,6 +504,41 @@ object LocalCache {
}
}
fun consume(
event: WikiNoteEvent,
relay: Relay?,
) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, replyTo)
refreshObservers(note)
}
}
fun consume(
event: PollNoteEvent,
relay: Relay? = null,
@@ -463,6 +609,27 @@ object LocalCache {
consumeBaseReplaceable(event, relay)
}
fun consume(
event: CommunityListEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
fun consume(
event: GitRepositoryEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
fun consume(
event: ChannelListEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
fun consume(
event: FileServersEvent,
relay: Relay?,
@@ -572,6 +739,30 @@ object LocalCache {
}
}
fun consume(
event: OtsEvent,
relay: Relay?,
) {
val version = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
// Already processed this event.
if (version.event?.id() == event.id()) return
// makes sure the OTS has a valid certificate
if (event.cacheVerify() == null) return // no valid OTS
if (version.event == null) {
version.loadEvent(event, author, emptyList())
if (version.liveSet != null) {
updateListCache()
}
version.liveSet?.innerOts?.invalidateData()
}
refreshObservers(version)
}
fun consume(
event: BadgeDefinitionEvent,
relay: Relay?,
@@ -743,7 +934,7 @@ object LocalCache {
event
.deleteEvents()
.mapNotNull { notes[it] }
.mapNotNull { getNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) {
@@ -1184,6 +1375,57 @@ object LocalCache {
refreshObservers(note)
}
fun consume(
event: FhirResourceEvent,
relay: Relay?,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume(
event: TextNoteModificationEvent,
relay: Relay?,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
event.editedNote()?.let {
checkGetOrCreateNote(it)?.let { editedNote ->
modificationCache.remove(editedNote.idHex)
// must update list of Notes to quickly update the user.
if (editedNote.liveSet != null) {
updateListCache()
}
editedNote.liveSet?.innerModifications?.invalidateData()
}
}
refreshObservers(note)
}
fun consume(
event: HighlightEvent,
relay: Relay?,
@@ -1380,11 +1622,14 @@ object LocalCache {
val key = decodePublicKeyAsHexOrNull(username)
if (key != null && users[key] != null) {
return listOfNotNull(users[key])
if (key != null) {
val user = getUserIfExists(key)
if (user != null) {
return listOfNotNull(user)
}
}
return users.values.filter {
return userListCache.filter {
(it.anyNameStartsWith(username)) ||
it.pubkeyHex.startsWith(username, true) ||
it.pubkeyNpub().startsWith(username, true)
@@ -1394,19 +1639,16 @@ object LocalCache {
fun findNotesStartingWith(text: String): List<Note> {
checkNotInMainThread()
val key =
try {
Nip19.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey()
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
val key = decodeEventIdAsHexOrNull(text)
if (key != null && (notes[key] ?: addressables[key]) != null) {
return listOfNotNull(notes[key] ?: addressables[key])
if (key != null) {
val note = getNoteIfExists(key)
if (note != null) {
return listOfNotNull(note)
}
}
return notes.values.filter {
return noteListCache.filter {
(
it.event !is GenericRepostEvent &&
it.event !is RepostEvent &&
@@ -1444,14 +1686,7 @@ object LocalCache {
fun findChannelsStartingWith(text: String): List<Channel> {
checkNotInMainThread()
val key =
try {
Nip19.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey()
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
val key = decodeEventIdAsHexOrNull(text)
if (key != null && channels[key] != null) {
return listOfNotNull(channels[key])
}
@@ -1482,12 +1717,61 @@ object LocalCache {
.toImmutableList()
}
suspend fun findEarliestOtsForNote(note: Note): Long? {
checkNotInMainThread()
var minTime: Long? = null
val time = TimeUtils.now()
noteListCache.forEach { item ->
val noteEvent = item.event
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
noteEvent.verifiedTime?.let { stampedTime ->
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
minTime = stampedTime
}
}
}
}
return minTime
}
val modificationCache = LruCache<HexKey, List<Note>>(20)
fun cachedModificationEventsForNote(note: Note): List<Note>? {
return modificationCache[note.idHex]
}
suspend fun findLatestModificationForNote(note: Note): List<Note> {
checkNotInMainThread()
val originalAuthor = note.author?.pubkeyHex ?: return emptyList()
modificationCache[note.idHex]?.let {
return it
}
val time = TimeUtils.now()
val newNotes =
noteListCache.filter { item ->
val noteEvent = item.event
noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
}.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
modificationCache.put(note.idHex, newNotes)
return newNotes
}
fun cleanObservers() {
notes.forEach { it.value.clearLive() }
noteListCache.forEach { it.clearLive() }
addressables.forEach { it.value.clearLive() }
users.forEach { it.value.clearLive() }
userListCache.forEach { it.clearLive() }
}
fun pruneOldAndHiddenMessages(account: Account) {
@@ -1513,8 +1797,8 @@ object LocalCache {
}
}
users.forEach { userPair ->
userPair.value.privateChatrooms.values.map {
userListCache.forEach { userPair ->
userPair.privateChatrooms.values.map {
val toBeRemoved = it.pruneMessagesToTheLatestOnly()
val childrenToBeRemoved = mutableListOf<Note>()
@@ -1529,7 +1813,7 @@ object LocalCache {
if (toBeRemoved.size > 1) {
println(
"PRUNE: ${toBeRemoved.size} private messages with ${userPair.value.toBestDisplayName()} removed. ${it.roomMessages.size} kept",
"PRUNE: ${toBeRemoved.size} private messages with ${userPair.toBestDisplayName()} removed. ${it.roomMessages.size} kept",
)
}
}
@@ -1538,9 +1822,9 @@ object LocalCache {
fun prunePastVersionsOfReplaceables() {
val toBeRemoved =
notes
noteListCache
.filter {
val noteEvent = it.value.event
val noteEvent = it.event
if (noteEvent is AddressableEvent) {
noteEvent.createdAt() <
(addressables[noteEvent.address().toTag()]?.event?.createdAt() ?: 0)
@@ -1548,7 +1832,6 @@ object LocalCache {
false
}
}
.values
val childrenToBeRemoved = mutableListOf<Note>()
@@ -1573,24 +1856,23 @@ object LocalCache {
checkNotInMainThread()
val toBeRemoved =
notes
noteListCache
.filter {
(
(it.value.event is TextNoteEvent && !it.value.isNewThread()) ||
it.value.event is ReactionEvent ||
it.value.event is LnZapEvent ||
it.value.event is LnZapRequestEvent ||
it.value.event is ReportEvent ||
it.value.event is GenericRepostEvent
(it.event is TextNoteEvent && !it.isNewThread()) ||
it.event is ReactionEvent ||
it.event is LnZapEvent ||
it.event is LnZapRequestEvent ||
it.event is ReportEvent ||
it.event is GenericRepostEvent
) &&
it.value.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
it.value.liveSet?.isInUse() != true && // don't delete if observing.
it.value.author?.pubkeyHex !in
it.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
it.liveSet?.isInUse() != true && // don't delete if observing.
it.author?.pubkeyHex !in
accounts && // don't delete if it is the logged in account
it.value.event?.isTaggedUsers(accounts) !=
it.event?.isTaggedUsers(accounts) !=
true // don't delete if it's a notification to the logged in user
}
.values
val childrenToBeRemoved = mutableListOf<Note>()
@@ -1656,7 +1938,8 @@ object LocalCache {
fun pruneExpiredEvents() {
checkNotInMainThread()
val toBeRemoved = notes.filter { it.value.event?.isExpired() == true }.values
val now = TimeUtils.now()
val toBeRemoved = noteListCache.filter { it.event?.isExpirationBefore(now) == true }
val childrenToBeRemoved = mutableListOf<Note>()
@@ -1682,7 +1965,7 @@ object LocalCache {
?.hiddenUsers
?.map { userHex ->
(
notes.values.filter { it.event?.pubKey() == userHex } +
noteListCache.filter { it.event?.pubKey() == userHex } +
addressables.values.filter { it.event?.pubKey() == userHex }
)
.toSet()
@@ -1704,7 +1987,7 @@ object LocalCache {
checkNotInMainThread()
var removingContactList = 0
users.values.forEach {
userListCache.forEach {
if (
it.pubkeyHex !in loggedIn &&
(it.liveSet == null || it.liveSet?.isInUse() == false) &&
@@ -1772,6 +2055,7 @@ object LocalCache {
is CalendarTimeSlotEvent -> consume(event, relay)
is CalendarRSVPEvent -> consume(event, relay)
is ChannelCreateEvent -> consume(event)
is ChannelListEvent -> consume(event, relay)
is ChannelHideMessageEvent -> consume(event)
is ChannelMessageEvent -> consume(event, relay)
is ChannelMetadataEvent -> consume(event)
@@ -1779,6 +2063,7 @@ object LocalCache {
is ChatMessageEvent -> consume(event, relay)
is ClassifiedsEvent -> consume(event, relay)
is CommunityDefinitionEvent -> consume(event, relay)
is CommunityListEvent -> consume(event, relay)
is CommunityPostApprovalEvent -> {
event.containedPost()?.let { verifyAndConsume(it, relay) }
consume(event)
@@ -1788,11 +2073,16 @@ object LocalCache {
is EmojiPackEvent -> consume(event, relay)
is EmojiPackSelectionEvent -> consume(event, relay)
is SealedGossipEvent -> consume(event, relay)
is FhirResourceEvent -> consume(event, relay)
is FileHeaderEvent -> consume(event, relay)
is FileServersEvent -> consume(event, relay)
is FileStorageEvent -> consume(event, relay)
is FileStorageHeaderEvent -> consume(event, relay)
is GiftWrapEvent -> consume(event, relay)
is GitIssueEvent -> consume(event, relay)
is GitReplyEvent -> consume(event, relay)
is GitPatchEvent -> consume(event, relay)
is GitRepositoryEvent -> consume(event, relay)
is HighlightEvent -> consume(event, relay)
is LiveActivitiesEvent -> consume(event, relay)
is LiveActivitiesChatMessageEvent -> consume(event, relay)
@@ -1810,6 +2100,7 @@ object LocalCache {
is MetadataEvent -> consume(event)
is MuteListEvent -> consume(event, relay)
is NNSEvent -> comsume(event, relay)
is OtsEvent -> consume(event, relay)
is PrivateDmEvent -> consume(event, relay)
is PinListEvent -> consume(event, relay)
is PeopleListEvent -> consume(event, relay)
@@ -1828,8 +2119,10 @@ object LocalCache {
}
is StatusEvent -> consume(event, relay)
is TextNoteEvent -> consume(event, relay)
is TextNoteModificationEvent -> consume(event, relay)
is VideoHorizontalEvent -> consume(event, relay)
is VideoVerticalEvent -> consume(event, relay)
is WikiNoteEvent -> consume(event, relay)
else -> {
Log.w("Event Not Supported", event.toJson())
}
@@ -1850,6 +2143,10 @@ class LocalCacheLiveData {
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
fun invalidateData(newNote: Note) {
bundler.invalidateList(newNote) { bundledNewNotes -> _newEventBundles.emit(bundledNewNotes) }
bundler.invalidateList(newNote) {
bundledNewNotes ->
LocalCache.updateListCache()
_newEventBundles.emit(bundledNewNotes)
}
}
}
@@ -0,0 +1,161 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import android.util.Log
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "resourceType",
)
@JsonSubTypes(
JsonSubTypes.Type(value = Practitioner::class, name = "Practitioner"),
JsonSubTypes.Type(value = Patient::class, name = "Patient"),
JsonSubTypes.Type(value = Bundle::class, name = "Bundle"),
JsonSubTypes.Type(value = VisionPrescription::class, name = "VisionPrescription"),
)
open class Resource(
var resourceType: String? = null,
var id: String = "",
)
class Practitioner(
resourceType: String? = null,
id: String = "",
var active: Boolean? = null,
var name: ArrayList<HumanName> = arrayListOf(),
var gender: String? = null,
) : Resource(resourceType, id)
class Patient(
resourceType: String? = null,
id: String = "",
var active: Boolean? = null,
var name: ArrayList<HumanName> = arrayListOf(),
var gender: String? = null,
) : Resource(resourceType, id)
class HumanName(
var use: String? = null,
var family: String? = null,
var given: ArrayList<String> = arrayListOf(),
) {
fun assembleName(): String {
return given.joinToString(" ") + " " + family
}
}
class Bundle(
resourceType: String? = null,
id: String = "",
var type: String? = null,
var created: String? = null,
var entry: List<Resource> = arrayListOf(),
) : Resource(resourceType, id)
class VisionPrescription(
resourceType: String? = null,
id: String = "",
var status: String? = null,
var created: String? = null,
var patient: Reference? = Reference(),
var encounter: Reference? = Reference(),
var dateWritten: String? = null,
var prescriber: Reference? = Reference(),
var lensSpecification: List<LensSpecification> = arrayListOf(),
) : Resource(resourceType, id)
class LensSpecification(
var eye: String? = null,
var sphere: Double? = null,
var cylinder: Double? = null,
var axis: Double? = null,
var add: Double? = null,
var prism: List<Prism> = arrayListOf(),
// contact lenses
var power: Double? = null,
var backCurve: Double? = null,
var diameter: Double? = null,
var color: String? = null,
var brand: String? = null,
var note: String? = null,
)
class Prism(
var amount: Double? = null,
var base: String? = null,
)
class Reference(
var reference: String? = null,
)
data class FhirElementDatabase(
var baseResource: Resource? = null,
var localDb: ImmutableMap<String, Resource> = persistentMapOf(),
)
fun findReferenceInDb(
it: String,
db: Map<String, Resource>,
): Resource? {
val parts = it.split("/")
return if (parts.size == 2) {
db.get(parts[1].removePrefix("#"))
} else {
db.get(it.removePrefix("#"))
}
}
fun parseResourceBundleOrNull(json: String): FhirElementDatabase? {
val mapper =
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return try {
val resource = mapper.readValue(json, Resource::class.java)
val db =
when (resource) {
is Bundle -> {
resource.entry.associateBy { it.id }.toImmutableMap()
}
else -> {
persistentMapOf(resource.id to resource)
}
}
FhirElementDatabase(
localDb = db,
baseResource = resource,
)
} catch (e: Exception) {
Log.e("RenderEyeGlassesPrescription", "Parser error", e)
null
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
@@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PayInvoiceSuccessResponse
import com.vitorpamplona.quartz.events.RepostEvent
@@ -66,10 +67,13 @@ import com.vitorpamplona.quartz.utils.containsAny
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import java.math.BigDecimal
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.coroutines.resume
@Stable
class AddressableNote(val address: ATag) : Note(address.toTag()) {
@@ -138,17 +142,17 @@ open class Note(val idHex: String) {
return if (myEvent is WrappedEvent) {
val host = myEvent.host
if (host != null) {
Nip19.createNEvent(
Nip19Bech32.createNEvent(
host.id,
host.pubKey,
host.kind(),
relays.firstOrNull()?.url,
)
} else {
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
Nip19Bech32.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
}
} else {
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
Nip19Bech32.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
}
}
@@ -521,7 +525,7 @@ open class Note(val idHex: String) {
}
}
private fun recursiveIsZappedByCalculation(
private suspend fun isZappedByCalculation(
option: Int?,
user: User,
account: Account,
@@ -532,48 +536,65 @@ open class Note(val idHex: String) {
return
}
val next = remainingZapEvents.first()
remainingZapEvents.forEach { next ->
val zapRequest = next.first.event as LnZapRequestEvent
val zapEvent = next.second?.event as? LnZapEvent
if (next.first.author?.pubkeyHex == user.pubkeyHex) {
onWasZappedByAuthor()
} else {
account.decryptZapContentAuthor(next.first) {
if (
it.pubKey == user.pubkeyHex &&
(option == null || option == (it as? LnZapEvent)?.zappedPollOption())
) {
if (!zapRequest.isPrivateZap()) {
// public events
if (zapRequest.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
onWasZappedByAuthor()
return
}
} else {
// private events
// if has already decrypted
val privateZap = zapRequest.cachedPrivateZap()
if (privateZap != null) {
if (privateZap.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
onWasZappedByAuthor()
return
}
} else {
recursiveIsZappedByCalculation(
option,
user,
account,
remainingZapEvents.minus(next),
onWasZappedByAuthor,
)
if (account.isWriteable()) {
val result =
withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
zapRequest.decryptPrivateZap(account.signer) {
continuation.resume(it)
}
}
}
if (result?.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
onWasZappedByAuthor()
return
}
}
}
}
}
}
fun isZappedBy(
suspend fun isZappedBy(
user: User,
account: Account,
onWasZappedByAuthor: () -> Unit,
) {
recursiveIsZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor)
isZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor)
if (account.userProfile() == user) {
recursiveIsPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor)
}
}
fun isZappedBy(
suspend fun isZappedBy(
option: Int?,
user: User,
account: Account,
onWasZappedByAuthor: () -> Unit,
) {
recursiveIsZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor)
isZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor)
}
fun getReactionBy(user: User): String? {
@@ -926,6 +947,8 @@ class NoteLiveSet(u: Note) {
val innerReports = NoteBundledRefresherLiveData(u)
val innerRelays = NoteBundledRefresherLiveData(u)
val innerZaps = NoteBundledRefresherLiveData(u)
val innerOts = NoteBundledRefresherLiveData(u)
val innerModifications = NoteBundledRefresherLiveData(u)
val metadata = innerMetadata.map { it }
val reactions = innerReactions.map { it }
@@ -979,7 +1002,9 @@ class NoteLiveSet(u: Note) {
hasReactions.hasObservers() ||
replyCount.hasObservers() ||
reactionCount.hasObservers() ||
boostCount.hasObservers()
boostCount.hasObservers() ||
innerOts.hasObservers() ||
innerModifications.hasObservers()
}
fun destroy() {
@@ -990,6 +1015,8 @@ class NoteLiveSet(u: Note) {
innerReports.destroy()
innerRelays.destroy()
innerZaps.destroy()
innerOts.destroy()
innerModifications.destroy()
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -139,10 +139,10 @@ class User(val pubkeyHex: String) {
// Update Followers of the past user list
// Update Followers of the new contact list
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
}
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
}
liveSet?.innerRelays?.invalidateData()
@@ -363,7 +363,7 @@ class User(val pubkeyHex: String) {
}
suspend fun transientFollowerCount(): Int {
return LocalCache.users.count { it.value.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
return LocalCache.userListCache.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
fun cachedFollowingKeySet(): Set<HexKey> {
@@ -387,7 +387,7 @@ class User(val pubkeyHex: String) {
}
suspend fun cachedFollowerCount(): Int {
return LocalCache.users.count { it.value.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
return LocalCache.userListCache.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -20,48 +20,13 @@
*/
package com.vitorpamplona.amethyst.service
import android.util.Log
import android.util.LruCache
import android.util.Patterns
import androidx.compose.runtime.Immutable
import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlContent
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
import com.vitorpamplona.amethyst.ui.components.hashTagsPattern
import com.vitorpamplona.amethyst.ui.components.imageExtensions
import com.vitorpamplona.amethyst.ui.components.removeQueryParamsForExtensionComparison
import com.vitorpamplona.amethyst.ui.components.tagIndex
import com.vitorpamplona.amethyst.ui.components.videoExtensions
import com.vitorpamplona.quartz.encoders.Nip54
import com.vitorpamplona.quartz.encoders.Nip92
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.amethyst.commons.RichTextParser
import com.vitorpamplona.amethyst.commons.RichTextViewerState
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CancellationException
import java.util.regex.Pattern
@Immutable
data class RichTextViewerState(
val urlSet: ImmutableSet<String>,
val imagesForPager: ImmutableMap<String, ZoomableUrlContent>,
val imageList: ImmutableList<ZoomableUrlContent>,
val customEmoji: ImmutableMap<String, String>,
val paragraphs: ImmutableList<ParagraphState>,
)
data class ParagraphState(val words: ImmutableList<Segment>, val isRTL: Boolean)
object CachedRichTextParser {
val richTextCache = LruCache<String, RichTextViewerState>(200)
val richTextCache = LruCache<String, RichTextViewerState>(50)
fun parseText(
content: String,
@@ -76,298 +41,3 @@ object CachedRichTextParser {
}
}
}
// Group 1 = url, group 4 additional chars
// val noProtocolUrlValidator =
// Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
// Android9 seems to have an issue starting this regex.
val noProtocolUrlValidator =
try {
Pattern.compile(
"(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+[^\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}])*\\/?)(.*)",
)
} catch (e: Exception) {
Pattern.compile(
"(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)",
)
}
val HTTPRegex =
"^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?"
.toRegex(RegexOption.IGNORE_CASE)
class RichTextParser() {
fun parseMediaUrl(
fullUrl: String,
eventTags: ImmutableListOfLists<String>,
): ZoomableUrlContent? {
val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl)
return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
val frags = Nip54().parse(fullUrl)
val tags = Nip92().parse(fullUrl, eventTags.lists)
ZoomableUrlImage(
url = fullUrl,
description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
contentWarning = frags["content-warning"] ?: tags["content-warning"],
)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
val frags = Nip54().parse(fullUrl)
val tags = Nip92().parse(fullUrl, eventTags.lists)
ZoomableUrlVideo(
url = fullUrl,
description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
contentWarning = frags["content-warning"] ?: tags["content-warning"],
)
} else {
null
}
}
fun parseText(
content: String,
tags: ImmutableListOfLists<String>,
): RichTextViewerState {
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
val urlSet =
urls.mapNotNullTo(LinkedHashSet(urls.size)) {
// removes e-mails
if (Patterns.EMAIL_ADDRESS.matcher(it.originalUrl).matches()) {
null
} else if (isNumber(it.originalUrl)) {
null
} else if (it.originalUrl.contains("")) {
null
} else {
if (HTTPRegex.matches(it.originalUrl)) {
it.originalUrl
} else {
null
}
}
}
val imagesForPager =
urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl, tags) }.associateBy { it.url }
val imageList = imagesForPager.values.toList()
val emojiMap =
tags.lists.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] }
val segments = findTextSegments(content, imagesForPager.keys, urlSet, emojiMap, tags)
return RichTextViewerState(
urlSet.toImmutableSet(),
imagesForPager.toImmutableMap(),
imageList.toImmutableList(),
emojiMap.toImmutableMap(),
segments,
)
}
private fun findTextSegments(
content: String,
images: Set<String>,
urls: Set<String>,
emojis: Map<String, String>,
tags: ImmutableListOfLists<String>,
): ImmutableList<ParagraphState> {
val lines = content.split('\n')
val paragraphSegments = ArrayList<ParagraphState>(lines.size)
lines.forEach { paragraph ->
var isDirty = false
val isRTL = isArabic(paragraph)
val wordList = paragraph.trimEnd().split(' ')
val segments = ArrayList<Segment>(wordList.size)
wordList.forEach { word ->
val wordSegment = wordIdentifier(word, images, urls, emojis, tags)
if (wordSegment !is RegularTextSegment) {
isDirty = true
}
segments.add(wordSegment)
}
val newSegments =
if (isDirty) {
ParagraphState(segments.toPersistentList(), isRTL)
} else {
ParagraphState(persistentListOf<Segment>(RegularTextSegment(paragraph)), isRTL)
}
paragraphSegments.add(newSegments)
}
return paragraphSegments.toImmutableList()
}
fun isNumber(word: String): Boolean {
return numberPattern.matcher(word).matches()
}
fun isDate(word: String): Boolean {
return shortDatePattern.matcher(word).matches() || longDatePattern.matcher(word).matches()
}
private fun isArabic(text: String): Boolean {
return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' }
}
private fun wordIdentifier(
word: String,
images: Set<String>,
urls: Set<String>,
emojis: Map<String, String>,
tags: ImmutableListOfLists<String>,
): Segment {
val emailMatcher = Patterns.EMAIL_ADDRESS.matcher(word)
val phoneMatcher = Patterns.PHONE.matcher(word)
val schemelessMatcher = noProtocolUrlValidator.matcher(word)
return if (word.isEmpty()) {
RegularTextSegment(word)
} else if (images.contains(word)) {
ImageSegment(word)
} else if (urls.contains(word)) {
LinkSegment(word)
} else if (emojis.any { word.contains(it.key) }) {
EmojiSegment(word)
} else if (word.startsWith("lnbc", true)) {
InvoiceSegment(word)
} else if (word.startsWith("lnurl", true)) {
WithdrawSegment(word)
} else if (word.startsWith("cashuA", true)) {
CashuSegment(word)
} else if (emailMatcher.matches()) {
EmailSegment(word)
} else if (word.length in 7..14 && !isDate(word) && phoneMatcher.matches()) {
PhoneSegment(word)
} else if (startsWithNIP19Scheme(word)) {
BechSegment(word)
} else if (word.startsWith("#")) {
parseHash(word, tags)
} else if (word.contains(".") && schemelessMatcher.find()) {
val url = schemelessMatcher.group(1) // url
val additionalChars = schemelessMatcher.group(4) // additional chars
val pattern =
"""^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?"""
.toRegex(RegexOption.IGNORE_CASE)
if (pattern.find(word) != null) {
SchemelessUrlSegment(word, url, additionalChars)
} else {
RegularTextSegment(word)
}
} else {
RegularTextSegment(word)
}
}
private fun parseHash(
word: String,
tags: ImmutableListOfLists<String>,
): Segment {
// First #[n]
val matcher = tagIndex.matcher(word)
try {
if (matcher.find()) {
val index = matcher.group(1)?.toInt()
val suffix = matcher.group(2)
if (index != null && index >= 0 && index < tags.lists.size) {
val tag = tags.lists[index]
if (tag.size > 1) {
if (tag[0] == "p") {
return HashIndexUserSegment(word, tag[1], suffix)
} else if (tag[0] == "e" || tag[0] == "a") {
return HashIndexEventSegment(word, tag[1], suffix)
}
}
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.w("Tag Parser", "Couldn't link tag $word", e)
}
// Second #Amethyst
val hashtagMatcher = hashTagsPattern.matcher(word)
try {
if (hashtagMatcher.find()) {
val hashtag = hashtagMatcher.group(1)
if (hashtag != null) {
return HashTagSegment(word, hashtag, hashtagMatcher.group(2))
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
}
return RegularTextSegment(word)
}
companion object {
val longDatePattern: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$")
val shortDatePattern: Pattern = Pattern.compile("^\\d{2}-\\d{2}-\\d{2}$")
val numberPattern: Pattern = Pattern.compile("^(-?[\\d.]+)([a-zA-Z%]*)$")
}
}
@Immutable open class Segment(val segmentText: String)
@Immutable class ImageSegment(segment: String) : Segment(segment)
@Immutable class LinkSegment(segment: String) : Segment(segment)
@Immutable class EmojiSegment(segment: String) : Segment(segment)
@Immutable class InvoiceSegment(segment: String) : Segment(segment)
@Immutable class WithdrawSegment(segment: String) : Segment(segment)
@Immutable class CashuSegment(segment: String) : Segment(segment)
@Immutable class EmailSegment(segment: String) : Segment(segment)
@Immutable class PhoneSegment(segment: String) : Segment(segment)
@Immutable class BechSegment(segment: String) : Segment(segment)
@Immutable
open class HashIndexSegment(segment: String, val hex: String, val extras: String?) :
Segment(segment)
@Immutable
class HashIndexUserSegment(segment: String, hex: String, extras: String?) :
HashIndexSegment(segment, hex, extras)
@Immutable
class HashIndexEventSegment(segment: String, hex: String, extras: String?) :
HashIndexSegment(segment, hex, extras)
@Immutable
class HashTagSegment(segment: String, val hashtag: String, val extras: String?) : Segment(segment)
@Immutable
class SchemelessUrlSegment(segment: String, val url: String, val extras: String?) :
Segment(segment)
@Immutable class RegularTextSegment(segment: String) : Segment(segment)
fun startsWithNIP19Scheme(word: String): Boolean {
val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@")
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service
import android.content.Context
import android.util.LruCache
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@@ -42,6 +43,24 @@ data class CashuToken(
val proofs: JsonNode,
)
object CachedCashuProcessor {
val cashuCache = LruCache<String, GenericLoadable<CashuToken>>(20)
fun cached(token: String): GenericLoadable<CashuToken> {
return cashuCache[token] ?: GenericLoadable.Loading()
}
fun parse(token: String): GenericLoadable<CashuToken> {
if (cashuCache[token] !is GenericLoadable.Loaded) {
val newCachuData = CashuProcessor().parse(token)
cashuCache.put(token, newCachuData)
}
return cashuCache[token]
}
}
class CashuProcessor {
fun parse(cashuToken: String): GenericLoadable<CashuToken> {
checkNotInMainThread()
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -27,6 +27,7 @@ import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.HandlerThread
import android.util.LruCache
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CancellationException
@@ -92,7 +93,33 @@ class LocationUtil(context: Context) {
}
}
class ReverseGeoLocationUtil {
object CachedGeoLocations {
val locationNames = LruCache<String, String>(20)
fun cached(geoHashStr: String): String? {
return locationNames[geoHashStr]
}
suspend fun geoLocate(
geoHashStr: String,
location: Location,
context: Context,
): String? {
locationNames[geoHashStr]?.let {
return it
}
val name = ReverseGeoLocationUtil().execute(location, context)?.ifBlank { null }
if (name != null) {
locationNames.put(geoHashStr, name)
}
return name
}
}
private class ReverseGeoLocationUtil {
suspend fun execute(
location: Location,
context: Context,
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -22,7 +22,8 @@ package com.vitorpamplona.amethyst.service
import android.util.Log
import android.util.LruCache
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CancellationException
import okhttp3.Call
import okhttp3.Callback
@@ -31,29 +32,64 @@ import okhttp3.Response
import java.io.IOException
object Nip11CachedRetriever {
val relayInformationDocumentCache = LruCache<String, RelayInformation>(100)
open class RetrieveResult(val time: Long)
class RetrieveResultError(val error: Nip11Retriever.ErrorCode, val msg: String? = null) : RetrieveResult(TimeUtils.now())
class RetrieveResultSuccess(val data: Nip11RelayInformation) : RetrieveResult(TimeUtils.now())
val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
val retriever = Nip11Retriever()
fun getFromCache(dirtyUrl: String): Nip11RelayInformation? {
val result = relayInformationDocumentCache.get(retriever.cleanUrl(dirtyUrl)) ?: return null
if (result is RetrieveResultSuccess) return result.data
return null
}
suspend fun loadRelayInfo(
dirtyUrl: String,
onInfo: (RelayInformation) -> Unit,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
) {
val url = retriever.cleanUrl(dirtyUrl)
val doc = relayInformationDocumentCache.get(url)
if (doc != null) {
onInfo(doc)
if (doc is RetrieveResultSuccess) {
onInfo(doc.data)
} else if (doc is RetrieveResultError) {
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
onError(dirtyUrl, doc.error, null)
} else {
Nip11Retriever()
.loadRelayInfo(
url = url,
dirtyUrl = dirtyUrl,
onInfo = {
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
onInfo(it)
},
onError = { dirtyUrl, code, errorMsg ->
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
onError(url, code, errorMsg)
},
)
}
}
} else {
Nip11Retriever()
.loadRelayInfo(
url,
dirtyUrl,
url = url,
dirtyUrl = dirtyUrl,
onInfo = {
relayInformationDocumentCache.put(url, it)
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
onInfo(it)
},
onError,
onError = { dirtyUrl, code, errorMsg ->
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
onError(url, code, errorMsg)
},
)
}
}
@@ -78,9 +114,10 @@ class Nip11Retriever {
suspend fun loadRelayInfo(
url: String,
dirtyUrl: String,
onInfo: (RelayInformation) -> Unit,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (String, ErrorCode, String?) -> Unit,
) {
checkNotInMainThread()
try {
val request: Request =
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
@@ -98,7 +135,7 @@ class Nip11Retriever {
val body = it.body.string()
try {
if (it.isSuccessful) {
onInfo(RelayInformation.fromJson(body))
onInfo(Nip11RelayInformation.fromJson(body))
} else {
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -35,6 +35,9 @@ import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.CalendarDateSlotEvent
import com.vitorpamplona.quartz.events.CalendarRSVPEvent
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
@@ -42,6 +45,10 @@ import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.GitIssueEvent
import com.vitorpamplona.quartz.events.GitPatchEvent
import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.MetadataEvent
@@ -120,24 +127,12 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
)
}
fun createAccountAcceptedAwardsFilter(): TypedFilter {
fun createAccountSettingsFilter(): TypedFilter {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds = listOf(BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 10,
),
)
}
fun createAccountBookmarkListFilter(): TypedFilter {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND),
kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 100,
),
@@ -204,6 +199,36 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
)
}
fun createNotificationFilter2(): TypedFilter {
val since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultNotificationFollowList.value)
?.relayList
?: account.activeRelays()?.associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
?: account.convertLocalRelays().associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds =
listOf(
GitReplyEvent.KIND,
GitIssueEvent.KIND,
GitPatchEvent.KIND,
HighlightEvent.KIND,
CalendarDateSlotEvent.KIND,
CalendarTimeSlotEvent.KIND,
CalendarRSVPEvent.KIND,
),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
limit = 400,
since = since,
),
)
}
fun createGiftWrapsToMeFilter() =
TypedFilter(
types = COMMON_FEED_TYPES,
@@ -297,10 +322,10 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountContactListFilter(),
createAccountRelayListFilter(),
createNotificationFilter(),
createNotificationFilter2(),
createGiftWrapsToMeFilter(),
createAccountReportsFilter(),
createAccountAcceptedAwardsFilter(),
createAccountBookmarkListFilter(),
createAccountSettingsFilter(),
createAccountLastPostsListFilter(),
createOtherAccountsBaseFilter(),
)
@@ -312,7 +337,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createAccountBookmarkListFilter(),
createAccountSettingsFilter(),
)
.ifEmpty { null }
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -24,9 +24,12 @@ import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexValidator
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
@@ -66,83 +69,118 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
null
}
Nip19.uriToRoute(mySearchString)?.hex ?: isAStraightHex
when (val parsed = Nip19Bech32.uriToRoute(mySearchString)?.entity) {
is Nip19Bech32.NSec -> KeyPair(privKey = parsed.hex.bechToBytes()).pubKey.toHexKey()
is Nip19Bech32.NPub -> parsed.hex
is Nip19Bech32.NProfile -> parsed.hex
is Nip19Bech32.Note -> parsed.hex
is Nip19Bech32.NEvent -> parsed.hex
is Nip19Bech32.NEmbed -> parsed.event.id
is Nip19Bech32.NRelay -> null
is Nip19Bech32.NAddress -> parsed.atag
else -> isAStraightHex
}
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
// downloads all the reactions to a given event.
return listOfNotNull(
val directReferenceFilters =
hexToWatch?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
ids = listOfNotNull(hexToWatch),
if (it.contains(":")) {
// naddr
listOfNotNull(
ATag.parse(it, null)?.let { aTag ->
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND, aTag.kind),
authors = listOfNotNull(aTag.pubKeyHex),
// just to be sure
limit = 5,
),
)
},
)
} else {
// event ids
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
ids = listOfNotNull(hexToWatch),
),
),
)
},
hexToWatch?.let {
// authors
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND),
authors = listOfNotNull(hexToWatch),
// just to be sure
limit = 5,
),
),
)
}
} ?: emptyList()
// downloads all the reactions to a given event.
return directReferenceFilters +
listOfNotNull(
TypedFilter(
types = COMMON_FEED_TYPES,
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND),
authors = listOfNotNull(hexToWatch),
search = mySearchString,
limit = 100,
),
)
},
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND),
search = mySearchString,
limit = 100,
),
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
kinds =
listOf(
TextNoteEvent.KIND,
LongTextNoteEvent.KIND,
BadgeDefinitionEvent.KIND,
PeopleListEvent.KIND,
BookmarkListEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
PollNoteEvent.KIND,
ChannelCreateEvent.KIND,
),
search = mySearchString,
limit = 100,
),
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
kinds =
listOf(
ChannelMetadataEvent.KIND,
ClassifiedsEvent.KIND,
CommunityDefinitionEvent.KIND,
EmojiPackEvent.KIND,
HighlightEvent.KIND,
LiveActivitiesEvent.KIND,
PollNoteEvent.KIND,
NNSEvent.KIND,
),
search = mySearchString,
limit = 100,
),
),
)
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
kinds =
listOf(
TextNoteEvent.KIND,
LongTextNoteEvent.KIND,
BadgeDefinitionEvent.KIND,
PeopleListEvent.KIND,
BookmarkListEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
PollNoteEvent.KIND,
ChannelCreateEvent.KIND,
),
search = mySearchString,
limit = 100,
),
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
kinds =
listOf(
ChannelMetadataEvent.KIND,
ClassifiedsEvent.KIND,
CommunityDefinitionEvent.KIND,
EmojiPackEvent.KIND,
HighlightEvent.KIND,
LiveActivitiesEvent.KIND,
PollNoteEvent.KIND,
NNSEvent.KIND,
),
search = mySearchString,
limit = 100,
),
),
)
}
val searchChannel = requestNewChannel()
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -29,13 +29,16 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
private var eventsToWatch = setOf<Note>()
@@ -120,27 +123,53 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
}
return groupByEOSEPresence(eventsToWatch).map {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds =
listOf(
TextNoteEvent.KIND,
ReactionEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ReportEvent.KIND,
LnZapEvent.KIND,
PollNoteEvent.KIND,
),
tags = mapOf("e" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 1000,
),
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds =
listOf(
TextNoteEvent.KIND,
ReactionEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ReportEvent.KIND,
LnZapEvent.KIND,
PollNoteEvent.KIND,
OtsEvent.KIND,
TextNoteModificationEvent.KIND,
GitReplyEvent.KIND,
),
tags = mapOf("e" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 1000,
),
),
)
}.flatten()
}
private fun createQuotesFilter(): List<TypedFilter>? {
if (eventsToWatch.isEmpty()) {
return null
}
return groupByEOSEPresence(eventsToWatch).map {
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
tags = mapOf("q" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 1000,
),
),
)
}.flatten()
}
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
@@ -205,9 +234,10 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
val missing = createLoadEventsIfNotLoadedFilter()
val addresses = createAddressFilter()
val addressReactions = createReactionsToWatchInAddressFilter()
val quotes = createQuotesFilter()
singleEventChannel.typedFilters =
listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null }
listOfNotNull(missing, addresses, reactions, addressReactions, quotes).flatten().ifEmpty { null }
}
fun add(eventId: Note) {
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.lnurl
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import kotlinx.coroutines.CancellationException
import java.text.NumberFormat
@Stable
data class InvoiceAmount(val invoice: String, val amount: String?)
object CachedLnInvoiceParser {
val lnInvoicesCache = LruCache<String, InvoiceAmount>(20)
fun cached(lnurl: String): InvoiceAmount? {
return lnInvoicesCache[lnurl]
}
fun parse(lnbcWord: String): InvoiceAmount? {
val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord)
if (myInvoice != null) {
val myInvoiceAmount =
try {
NumberFormat.getInstance().format(LnInvoiceUtil.getAmountInSats(myInvoice))
} catch (e: Exception) {
if (e is CancellationException) throw e
e.printStackTrace()
null
}
val lnInvoice = InvoiceAmount(myInvoice, myInvoiceAmount)
lnInvoicesCache.put(lnbcWord, lnInvoice)
return lnInvoice
}
return null
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -112,7 +112,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
event.pubKey != acc.userProfile().pubkeyHex
) { // from the user
val chatNote = LocalCache.notes[event.id] ?: return
val chatNote = LocalCache.getNoteIfExists(event.id) ?: return
val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey())
val followingKeySet = acc.followingKeySet()
@@ -145,7 +145,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
event: PrivateDmEvent,
acc: Account,
) {
val note = LocalCache.notes[event.id] ?: return
val note = LocalCache.getNoteIfExists(event.id) ?: return
// old event being re-broadcast
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
@@ -184,7 +184,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
event: LnZapEvent,
acc: Account,
) {
val noteZapEvent = LocalCache.notes[event.id] ?: return
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
// old event being re-broadcast
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.ots
import android.util.Log
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.quartz.ots.BitcoinExplorer
import com.vitorpamplona.quartz.ots.BlockHeader
import com.vitorpamplona.quartz.ots.exceptions.UrlException
import okhttp3.Request
class OkHttpBlockstreamExplorer : BitcoinExplorer {
/**
* Retrieve the block information from the block hash.
*
* @param hash Hash of the block.
* @return the blockheader of the hash
* @throws Exception desc
*/
override fun block(hash: String): BlockHeader {
val client = HttpClientManager.getHttpClient()
val url = "$BLOCKSTREAM_API_URL/block/$hash"
val request =
Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/json")
.url(url)
.get()
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val jsonObject = jacksonObjectMapper().readTree(it.body.string())
val blockHeader = BlockHeader()
blockHeader.merkleroot = jsonObject["merkle_root"].asText()
blockHeader.setTime(jsonObject["timestamp"].asInt().toString())
blockHeader.blockHash = hash
Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash")
return blockHeader
} else {
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
}
}
}
/**
* Retrieve the block hash from the block height.
*
* @param height Height of the block.
* @return the hash of the block at height height
* @throws Exception desc
*/
@Throws(Exception::class)
override fun blockHash(height: Int): String {
val client = HttpClientManager.getHttpClient()
val url = "$BLOCKSTREAM_API_URL/block-height/$height"
val request =
Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val blockHash = it.body.string()
Log.d("OkHttpBlockstreamExplorer", "$url $blockHash")
return blockHash
} else {
throw UrlException(it.message)
}
}
}
companion object {
private const val BLOCKSTREAM_API_URL = "https://blockstream.info/api"
}
}
@@ -0,0 +1,133 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.ots.ICalendar
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
import com.vitorpamplona.quartz.ots.Timestamp
import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException
import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException
import com.vitorpamplona.quartz.ots.exceptions.UrlException
import com.vitorpamplona.quartz.ots.http.Request
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
/**
* Class representing remote calendar server interface.
*/
class OkHttpCalendar(val url: String) : ICalendar {
/**
* Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest.
*
* @param digest The digest hash to send.
* @return the Timestamp received from the calendar.
* @throws ExceededSizeException if response is too big.
* @throws UrlException if url is not reachable.
* @throws DeserializationException if the data is corrupt
*/
@Throws(ExceededSizeException::class, UrlException::class, DeserializationException::class)
override fun submit(digest: ByteArray): Timestamp {
try {
val client = HttpClientManager.getHttpClient()
val url = "$url/digest"
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
val requestBody = digest.toRequestBody(mediaType)
val request =
okhttp3.Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val ctx = StreamDeserializationContext(it.body.bytes())
return Timestamp.deserialize(ctx, digest)
} else {
throw UrlException("Failed to open $url")
}
}
} catch (e: ExceededSizeException) {
throw e
} catch (e: DeserializationException) {
throw e
} catch (e: Exception) {
throw UrlException(e.message)
}
}
/**
* Get a timestamp for a given commitment.
*
* @param commitment The digest hash to send.
* @return the Timestamp from the calendar server (with blockchain information if already written).
* @throws ExceededSizeException if response is too big.
* @throws UrlException if url is not reachable.
* @throws CommitmentNotFoundException if commit is not found.
* @throws DeserializationException if the data is corrupt
*/
@Throws(
DeserializationException::class,
ExceededSizeException::class,
CommitmentNotFoundException::class,
UrlException::class,
)
override fun getTimestamp(commitment: ByteArray): Timestamp {
try {
val client = HttpClientManager.getHttpClient()
val url = url + "/timestamp/" + Hex.encode(commitment)
val request =
okhttp3.Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
.url(url)
.get()
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val ctx = StreamDeserializationContext(it.body.bytes())
return Timestamp.deserialize(ctx, commitment)
} else {
throw CommitmentNotFoundException("Calendar response a status code != 200: " + it.code)
}
}
} catch (e: DeserializationException) {
throw e
} catch (e: ExceededSizeException) {
throw e
} catch (e: CommitmentNotFoundException) {
throw e
} catch (e: Exception) {
throw UrlException(e.message)
}
}
}
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
import com.vitorpamplona.quartz.ots.Timestamp
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Optional
import java.util.concurrent.BlockingQueue
/**
* For making async calls to a calendar server
*/
class OkHttpCalendarAsyncSubmit(private val url: String, private val digest: ByteArray) : ICalendarAsyncSubmit {
private var queue: BlockingQueue<Optional<Timestamp>>? = null
fun setQueue(queue: BlockingQueue<Optional<Timestamp>>?) {
this.queue = queue
}
@Throws(Exception::class)
override fun call(): Optional<Timestamp> {
val client = HttpClientManager.getHttpClient()
val url = "$url/digest"
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
val requestBody = digest.toRequestBody(mediaType)
val request =
okhttp3.Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val ctx = StreamDeserializationContext(it.body.bytes())
val timestamp = Timestamp.deserialize(ctx, digest)
val of = Optional.of(timestamp)
queue!!.add(of)
return of
} else {
queue!!.add(Optional.empty())
return Optional.empty()
}
}
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -18,8 +18,21 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.ots.CalendarBuilder
import com.vitorpamplona.quartz.ots.ICalendar
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
data class Nip47URI(val pubKeyHex: HexKey, val relayUri: String?, val secret: HexKey?)
class OkHttpCalendarBuilder : CalendarBuilder {
override fun newSyncCalendar(url: String): ICalendar {
return OkHttpCalendar(url)
}
override fun newAsyncCalendar(
url: String,
digest: ByteArray,
): ICalendarAsyncSubmit {
return OkHttpCalendarAsyncSubmit(url, digest)
}
}
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service.previews
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType
@@ -91,6 +92,7 @@ suspend fun getDocument(
withContext(Dispatchers.IO) {
val request: Request = Request.Builder().url(url).get().build()
HttpClientManager.getHttpClient().newCall(request).execute().use {
checkNotInMainThread()
if (it.isSuccessful) {
val mimeType =
it.headers.get("Content-Type")?.toMediaType()
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -108,7 +108,7 @@ object Client : RelayPool.Listener {
checkNotInMainThread()
subscriptions = subscriptions + Pair(subscriptionId, filters)
RelayPool.sendFilterOnlyIfDisconnected(subscriptionId)
RelayPool.connectAndSendFiltersIfDisconnected()
}
fun send(
@@ -292,7 +292,7 @@ object Client : RelayPool.Listener {
open fun onRelayStateChange(
type: Relay.StateType,
relay: Relay,
channel: String?,
subscriptionId: String?,
) = Unit
/** When an relay saves or rejects a new event. */
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -37,26 +37,26 @@ class JsonFilter(
val filter =
factory.objectNode().apply {
ids?.run {
put(
replace(
"ids",
factory.arrayNode(ids.size).apply { ids.forEach { add(it) } },
)
}
authors?.run {
put(
replace(
"authors",
factory.arrayNode(authors.size).apply { authors.forEach { add(it) } },
)
}
kinds?.run {
put(
replace(
"kinds",
factory.arrayNode(kinds.size).apply { kinds.forEach { add(it) } },
)
}
tags?.run {
entries.forEach { kv ->
put(
replace(
"#${kv.key}",
factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } },
)
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -409,7 +409,7 @@ class Relay(
return buffer.toString()
}
fun sendFilterOnlyIfDisconnected(subscriptionId: String) {
fun connectAndSendFiltersIfDisconnected() {
checkNotInMainThread()
if (socket == null) {
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -81,8 +81,8 @@ object RelayPool : Relay.Listener {
relays.forEach { it.sendFilter(subscriptionId) }
}
fun sendFilterOnlyIfDisconnected(subscriptionId: String) {
relays.forEach { it.sendFilterOnlyIfDisconnected(subscriptionId) }
fun connectAndSendFiltersIfDisconnected() {
relays.forEach { it.connectAndSendFiltersIfDisconnected() }
}
fun sendToSelectedRelays(
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -47,7 +47,7 @@ data class Subscription(
return factory.objectNode().apply {
put("id", id)
typedFilters?.also { filters ->
put(
replace(
"typedFilters",
factory.arrayNode(filters.size).apply {
filters.forEach { filter -> add(filter.toJsonObject()) }
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -36,8 +36,8 @@ class TypedFilter(
val factory = Event.mapper.nodeFactory
return factory.objectNode().apply {
put("types", typesToJson(types))
put("filter", filterToJson(filter))
replace("types", typesToJson(types))
replace("filter", filterToJson(filter))
}
}
@@ -50,26 +50,26 @@ class TypedFilter(
val factory = Event.mapper.nodeFactory
return factory.objectNode().apply {
filter.ids?.run {
put(
replace(
"ids",
factory.arrayNode(filter.ids.size).apply { filter.ids.forEach { add(it) } },
)
}
filter.authors?.run {
put(
replace(
"authors",
factory.arrayNode(filter.authors.size).apply { filter.authors.forEach { add(it) } },
)
}
filter.kinds?.run {
put(
replace(
"kinds",
factory.arrayNode(filter.kinds.size).apply { filter.kinds.forEach { add(it) } },
)
}
filter.tags?.run {
entries.forEach { kv ->
put(
replace(
"#${kv.key}",
factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } },
)
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Vitor Pamplona
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in

Some files were not shown because too many files have changed in this diff Show More