Compare commits

...

899 Commits

Author SHA1 Message Date
Vitor Pamplona
a86f64fce5 Fixes release Id for the zap the devs button 2025-11-15 12:01:17 -05:00
Vitor Pamplona
075a92a709 Version 1.04 2025-11-15 11:45:26 -05:00
Vitor Pamplona
67d6b9e350 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2025-11-15 11:34:05 -05:00
Vitor Pamplona
f7fd1e79f0 Improves creation of lists for users that had none 2025-11-15 11:26:35 -05:00
Vitor Pamplona
01afb0aeeb Merge pull request #1568 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-15 11:11:35 -05:00
Crowdin Bot
d777e94aad New Crowdin translations by GitHub Action 2025-11-15 16:09:40 +00:00
Vitor Pamplona
1bd2b2168f Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  updated cz, de, pt, sv
  New Crowdin translations by GitHub Action
2025-11-15 11:08:00 -05:00
Vitor Pamplona
719e34ea8d Less jarring delete buttons on lists 2025-11-15 11:04:13 -05:00
Vitor Pamplona
78c13a7d29 Minor changes to the change log 2025-11-15 10:36:44 -05:00
David Kaspar
61d4f3000f Merge pull request #1567 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-15 13:21:09 +01:00
Crowdin Bot
22c8ecefed New Crowdin translations by GitHub Action 2025-11-15 12:15:13 +00:00
David Kaspar
8995c7a51d Merge pull request #1566 from davotoula/update-translations
updated cz, de, pt, sv
2025-11-15 13:14:06 +01:00
David Kaspar
ef267acee2 Merge pull request #1565 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-15 13:13:49 +01:00
davotoula
cbf9effcf4 updated cz, de, pt, sv 2025-11-15 09:38:00 +01:00
Crowdin Bot
007f512cc4 New Crowdin translations by GitHub Action 2025-11-15 00:20:39 +00:00
Vitor Pamplona
96ee6baa8f Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2025-11-14 19:18:33 -05:00
Vitor Pamplona
f400cc6088 Adds a change log 2025-11-14 19:07:52 -05:00
Vitor Pamplona
23299b5ef3 half-way contrast for placeholder texts 2025-11-13 18:25:49 -05:00
David Kaspar
88a3e2a74b Merge pull request #1563 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-13 19:17:56 +01:00
Crowdin Bot
e5fe63d630 New Crowdin translations by GitHub Action 2025-11-13 15:53:22 +00:00
Vitor Pamplona
e6a10e07c4 Correctly adding relays to the filters so that the Global feed can filter only events that came from the relays listed in the Global filter. 2025-11-13 10:50:13 -05:00
Vitor Pamplona
19846e7ff3 Improves the composition of NIP-05 lines 2025-11-13 10:49:29 -05:00
Vitor Pamplona
05af3a78e4 Increases the contrast of placeholder text 2025-11-13 10:45:58 -05:00
Vitor Pamplona
c9370f8d02 Better parse multiple imetas with multiple urls into appropriate properties 2025-11-13 10:45:28 -05:00
Vitor Pamplona
f186b78ca9 Adds test case for empty d-tags in naddr1 2025-11-13 10:30:26 -05:00
Vitor Pamplona
abcaf19d2b removes zapstr.live 2025-11-13 09:11:20 -05:00
Vitor Pamplona
43fbf4f40f Removes filestr 2025-11-13 09:11:09 -05:00
Vitor Pamplona
9f66cd0660 Opens Follow Packs on following.space 2025-11-13 09:10:56 -05:00
Vitor Pamplona
b80f24b587 Upgrades dependencies 2025-11-13 08:42:07 -05:00
Vitor Pamplona
ee52f10192 removes unecessary list of icons option from drawer 2025-11-12 18:19:28 -05:00
Vitor Pamplona
8d41b58167 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2025-11-12 17:26:17 -05:00
Vitor Pamplona
ac291c996c Adds on click to the UserProfile selections 2025-11-12 17:21:46 -05:00
Vitor Pamplona
29d11c8281 Moves the List/Pack creation Dialog to a screen and adds an Image upload option 2025-11-12 17:17:22 -05:00
Vitor Pamplona
e2c50847d0 Merge pull request #1562 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-12 14:21:30 -05:00
Crowdin Bot
b339e3a245 New Crowdin translations by GitHub Action 2025-11-12 19:20:01 +00:00
Vitor Pamplona
8963024a95 Fixes Jump on FollowPack screens 2025-11-12 14:18:01 -05:00
Vitor Pamplona
27bc950afb Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2025-11-12 14:12:26 -05:00
Vitor Pamplona
0ddbb4616b Adds creation, edits and member update for Follow Packs 2025-11-12 13:50:57 -05:00
Vitor Pamplona
adb484bf56 Improves descriptions of lists and packs 2025-11-12 13:18:18 -05:00
Vitor Pamplona
bd30b526a9 Adds a @Stable marker on UserSuggestionState s 2025-11-12 12:34:49 -05:00
Vitor Pamplona
ce49725771 Allows customization of the Create new list dialog 2025-11-12 12:34:11 -05:00
Vitor Pamplona
6c64297415 Adds an update method for changes in current Event 2025-11-12 12:33:35 -05:00
Vitor Pamplona
90330165d2 Prepares to create follow packs 2025-11-12 12:33:10 -05:00
David Kaspar
fe876de4c9 Merge pull request #1561 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-12 15:13:05 +01:00
Crowdin Bot
b17d709696 New Crowdin translations by GitHub Action 2025-11-12 13:32:00 +00:00
Vitor Pamplona
8554257329 Change reqResultsInOrderAsFlow to reqAsFlow 2025-11-12 08:29:05 -05:00
Vitor Pamplona
9eee5d9e1e slight performance improvement on the reqflow 2025-11-12 08:27:59 -05:00
Vitor Pamplona
b4810fd12c Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2025-11-11 19:29:10 -05:00
Vitor Pamplona
bf803384a8 This early decryption doesn't seem needed anymore. The event consumer in the account will take care of decrypting the new DM 2025-11-11 19:01:52 -05:00
Vitor Pamplona
e8eb4c688c Modifies the flow to only output changes in the relay piece that has modified. 2025-11-11 19:01:20 -05:00
Vitor Pamplona
a5207c3f0a adds recursive markAsSeen to mark the inner chat element of NIP-17 DMs for each relay that that message was received. 2025-11-11 19:00:49 -05:00
Vitor Pamplona
ce073553ee Makes sure the edit is Stable 2025-11-11 18:59:34 -05:00
Vitor Pamplona
dd97dfc67f Merge pull request #1560 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-11 17:08:06 -05:00
Vitor Pamplona
1cfe953a65 Refactors the code to avoid using callback lambdas 2025-11-11 16:37:15 -05:00
Vitor Pamplona
d4b91845d5 rename runIOCatching to launchSigner 2025-11-11 16:10:49 -05:00
Crowdin Bot
a114bf9dfa New Crowdin translations by GitHub Action 2025-11-11 21:05:02 +00:00
Vitor Pamplona
b14c588a34 Moves the dispatcher of the save event for server models from each of the viewmodels to the account view model since the scope gets cancelled when the screen closes. 2025-11-11 16:01:19 -05:00
Vitor Pamplona
1589b646a8 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  updated cz, de, pt, sv
2025-11-11 16:00:15 -05:00
Vitor Pamplona
8c3ab80e47 More correctly parses null and default values from NIP-55 using Jackson 2025-11-11 14:00:17 -05:00
David Kaspar
4690f59922 Merge pull request #1556 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-11 09:12:24 +01:00
Crowdin Bot
255389aa3c New Crowdin translations by GitHub Action 2025-11-11 07:58:56 +00:00
David Kaspar
cf756669e5 Merge pull request #1557 from davotoula/update-translations
updated cz, de, pt, sv
2025-11-11 08:57:39 +01:00
davotoula
d7ca9d5a82 updated cz, de, pt, sv 2025-11-11 08:53:04 +01:00
Vitor Pamplona
5f88249be6 Creates a feed filter option for only kind 3 users 2025-11-10 19:45:39 -05:00
Vitor Pamplona
db354b646f Adds context to the highlights 2025-11-10 18:42:17 -05:00
Vitor Pamplona
17fdf05847 Fixes the author of the highlight 2025-11-10 18:00:32 -05:00
Vitor Pamplona
ddfe61eb9f adds description to the follow card ui 2025-11-10 17:36:07 -05:00
Vitor Pamplona
8bdd9d0979 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  updated cz, de, pt, sv
  New Crowdin translations by GitHub Action
  Update strings.xml

# Conflicts:
#	amethyst/src/main/res/values/strings.xml
2025-11-10 17:12:11 -05:00
Vitor Pamplona
bd1a70039a Creates a feed for follow packs 2025-11-10 17:02:21 -05:00
David Kaspar
87599b7e59 Merge pull request #1554 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-10 18:22:12 +01:00
Crowdin Bot
5762490098 New Crowdin translations by GitHub Action 2025-11-10 17:03:42 +00:00
David Kaspar
e634040504 Merge pull request #1553 from davotoula/update-translations
updated cz, de, pt, sv
2025-11-10 18:02:10 +01:00
davotoula
15e7909222 updated cz, de, pt, sv 2025-11-10 18:01:16 +01:00
Vitor Pamplona
ffaa06fed7 Adds an option to render a user gallery from HexKeys instead of full User objects 2025-11-10 11:18:18 -05:00
Vitor Pamplona
58128d9ad0 Making Channel metadata view model stable for performance 2025-11-10 11:17:22 -05:00
Vitor Pamplona
011aebfc27 Moving preview EmptyNav to a class to avoid Nav auto-imports in production code. 2025-11-10 11:17:04 -05:00
Vitor Pamplona
7230f67782 Merge pull request #1552 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-10 08:02:31 -05:00
Crowdin Bot
93e80dda83 New Crowdin translations by GitHub Action 2025-11-10 12:54:37 +00:00
Vitor Pamplona
0505011eab Merge pull request #1551 from maxblake2015/main
Update strings.xml
2025-11-10 07:53:07 -05:00
Max Blake
ea83dc3fd7 Update strings.xml
a small logical correction
2025-11-08 12:22:45 +00:00
Vitor Pamplona
948180157f pushes exception upward if triggered 2025-11-07 17:37:34 -05:00
Vitor Pamplona
a996065c05 Creates new observer for events with null option 2025-11-07 17:27:41 -05:00
Vitor Pamplona
c328a3aefe Refactors event observer name to a more precise one 2025-11-07 17:26:20 -05:00
Vitor Pamplona
6ccec0681a Marks address as stable 2025-11-07 17:23:53 -05:00
Vitor Pamplona
26a6b5d219 - Inverts public and private tabs
- adds number of users in the tab title
2025-11-06 16:19:43 -05:00
Vitor Pamplona
d17f39e85b Fixes naming cropping the menu icon 2025-11-06 16:18:29 -05:00
Vitor Pamplona
2ac5a57efe Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  optimise imports
  synchronize all cache mutations and supply sinceRelaySet so callers get snapshot copies rework groupByRelayPresence to build relay snapshots via sinceRelaySet,     filtering with immutable lists to prevent ConcurrentModificationException when relays update mid-iteration
  Hardened EOSEAccountFast against concurrent access so callers no longer iterate over live mutable maps
  Adjusted subscription cleanup to avoid mutating the watcher map while iterating it, preventing the ConcurrentModificationException when accounts switch

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt
2025-11-06 15:15:34 -05:00
Vitor Pamplona
6727bd0430 Improves padding of the ListItem interfaces 2025-11-06 14:15:59 -05:00
Vitor Pamplona
34aca458ed migrates to njump.to 2025-11-06 13:49:15 -05:00
Vitor Pamplona
ba082d86e2 Merge pull request #1550 from davotoula/reduce-errors-ConcurrentModificationException
Reduce errors in log: concurrent modification exception
2025-11-06 09:50:22 -05:00
davotoula
409b3b43f7 optimise imports 2025-11-06 10:19:32 +01:00
davotoula
545cd2ff6d synchronize all cache mutations and supply sinceRelaySet so callers get snapshot copies
rework groupByRelayPresence to build relay snapshots via sinceRelaySet,
    filtering with immutable lists to prevent ConcurrentModificationException when relays update mid-iteration
2025-11-06 10:12:07 +01:00
davotoula
a82d6565fa Hardened EOSEAccountFast against concurrent access so callers no longer iterate over live mutable maps 2025-11-06 10:12:07 +01:00
davotoula
c07202944d Adjusted subscription cleanup to avoid mutating the watcher map while iterating it, preventing the ConcurrentModificationException when accounts switch 2025-11-06 10:12:07 +01:00
Vitor Pamplona
178582f3c4 Showing the list option in all Follow Buttons 2025-11-05 19:03:01 -05:00
Vitor Pamplona
c2c3442b74 Fixes showing the list of people button when the user is blocked 2025-11-05 19:02:45 -05:00
Vitor Pamplona
43942891b7 Changes the following marker to be a follow from all lists and follow sets 2025-11-05 18:02:49 -05:00
Vitor Pamplona
fc488064b7 Changes animations for profile buttons to come from the bottom of the screen 2025-11-05 17:58:56 -05:00
Vitor Pamplona
4535966c32 Refactoring of class names and breaking down bigger files into many 2025-11-05 17:48:16 -05:00
Vitor Pamplona
83f460c01d Selects the full text when adding users to list 2025-11-05 17:06:59 -05:00
Vitor Pamplona
6c630c75c2 Improves design of the lists and User management screen. 2025-11-05 16:40:20 -05:00
Vitor Pamplona
8d33ea8f99 Remove description from top nav bar of the list 2025-11-05 16:39:59 -05:00
Vitor Pamplona
558d952a00 Generalize List counters 2025-11-05 16:39:37 -05:00
Vitor Pamplona
1eb2309952 sort by title instead 2025-11-05 16:39:07 -05:00
Vitor Pamplona
0ffbbee31a Switch to vector graphics 2025-11-05 16:04:03 -05:00
Vitor Pamplona
ed8b285700 Sort lists by identifier 2025-11-05 15:54:01 -05:00
Vitor Pamplona
452082c6cb Fixes multiline list name 2025-11-05 15:53:47 -05:00
Vitor Pamplona
8168d353dd Improves the look of the list of lists of people screen 2025-11-05 14:11:15 -05:00
Vitor Pamplona
59cdb222d5 Removes the Mute List from the All Follows TopNav option 2025-11-05 10:51:10 -05:00
Vitor Pamplona
712965cc8f Fixes the unfollow button on profile (removes the follow signal including all lists for now)
Also fixes spacing of the Follow button
2025-11-04 19:50:57 -05:00
Vitor Pamplona
5741914c2a Fixes User profile banner being off place in short images 2025-11-04 19:41:23 -05:00
Vitor Pamplona
b8cce80d9f Updates all libs 2025-11-04 19:41:05 -05:00
Vitor Pamplona
7dfa343ad8 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  code simplification and deduplication
  code simplification and deduplication
  Add blurhash and dim to Nip96Uploader.kt
  added blurhash to blossom upload
  New Crowdin translations by GitHub Action
  Update README to mark video capture as complete
  Parse for video segments and exclude them from image gallery
  add VideoSegment and failing tests
  Fix location being added to note even after deselecting it
  optimise imports
  New Crowdin translations by GitHub Action
  update cs, de, sv, pt translations
  Revert "update buffer to 64kb"
  update buffer to 64kb
  Change length to Long from Int: avoids potential overflow, Long seems to be used everywhere else
  Create a CountingInputStream utility to avoid duplication Prevent INT overflow in BlossomUploader.kt Connection Cleanup in ImageDownloader.kt Added try-finally in Sha256Hasher.jvmAndroid.kt
  Add streaming hash utility function to quartz multiplatform, follow the existing pool/worker design Change hashing in ImageDownloader.kt to use streaming
  stream file to calculate both hash and size without loading it all at once

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt
2025-11-04 19:28:45 -05:00
Vitor Pamplona
be804092d7 - Refactors people lists
- Adds people and follow list cache on the account class
- Updates TopNav to use the new caching
- Removes redundant viewModels for list feeds
- Adds the ability to add and remove users from the List screen
2025-11-04 19:24:24 -05:00
Vitor Pamplona
50b551bb80 Updates emoji and user autocomplete state to use Account and avoid linking AccountViewModel 2025-11-04 19:21:04 -05:00
Vitor Pamplona
7c439f2448 Merge pull request #1549 from davotoula/optimise-imports
Optimise imports
2025-11-03 14:06:59 -05:00
davotoula
5ce184d400 Merge branch 'main' into optimise-imports 2025-11-03 19:45:12 +01:00
Vitor Pamplona
bda1fcadf8 Merge pull request #1548 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-03 12:47:34 -05:00
Crowdin Bot
379e539e89 New Crowdin translations by GitHub Action 2025-11-03 17:39:48 +00:00
Vitor Pamplona
f14b545947 Merge pull request #1531 from davotoula/1530-added-streaming-hashing-utility-to-avoid-OOM-on-large-files
Fix OutOfMemoryError for Large File Uploads
2025-11-03 12:38:38 -05:00
Vitor Pamplona
b8fff0c428 Merge pull request #1546 from davotoula/1537-dont-include-video-media-in-image-gallery
dont include video media in image gallery
2025-11-03 12:37:53 -05:00
davotoula
942de50148 code simplification and deduplication 2025-11-02 20:37:20 +01:00
davotoula
76be7b0d58 code simplification and deduplication 2025-11-02 19:56:38 +01:00
davotoula
4b260cfc07 Add blurhash and dim to Nip96Uploader.kt 2025-11-02 18:25:49 +01:00
davotoula
eee1487883 added blurhash to blossom upload 2025-11-02 18:25:49 +01:00
David Kaspar
27380f276c Merge pull request #1547 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-11-01 19:26:09 +01:00
Crowdin Bot
f62b1667c3 New Crowdin translations by GitHub Action 2025-11-01 18:22:14 +00:00
David Kaspar
eb4f0b9af9 Update README to mark video capture as complete 2025-11-01 18:20:58 +00:00
davotoula
7c374fb586 Parse for video segments and exclude them from image gallery 2025-11-01 16:37:38 +01:00
davotoula
1e12decd36 add VideoSegment and failing tests 2025-11-01 16:23:35 +01:00
Vitor Pamplona
1411de32a0 Adds search of events and addresses in the Deletion Event 2025-10-31 18:43:45 -04:00
Vitor Pamplona
7ea61824c8 Moves filter interfaces to their own files 2025-10-31 18:41:41 -04:00
Vitor Pamplona
f0923fb393 Fixes crash when checking and creating new users from tags. 2025-10-31 18:38:29 -04:00
Vitor Pamplona
3eb662851b Adds a flatten to set utility 2025-10-31 18:36:05 -04:00
Vitor Pamplona
f9bd660774 Merge pull request #1545 from greenart7c3/main
Fix location being added to note even after deselecting it
2025-10-31 08:26:00 -04:00
greenart7c3
bdb2637803 Fix location being added to note even after deselecting it 2025-10-31 06:34:49 -03:00
davotoula
c3c75b46a7 optimise imports 2025-10-30 19:20:02 +01:00
David Kaspar
3c7104fb6a Merge pull request #1544 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-30 19:19:05 +01:00
Crowdin Bot
c111912d2f New Crowdin translations by GitHub Action 2025-10-30 17:56:08 +00:00
David Kaspar
0f4e514308 Merge pull request #1543 from davotoula/update-tr
update cs, de, sv, pt translations
2025-10-30 18:54:44 +01:00
davotoula
ccf9d4ce80 update cs, de, sv, pt translations 2025-10-30 18:19:38 +01:00
Vitor Pamplona
95cc0783fd Adds a req that returns the list of events in order of arrival 2025-10-29 18:30:00 -04:00
Vitor Pamplona
f9f7cdf12f Renames internal Req objects 2025-10-29 18:29:23 -04:00
Vitor Pamplona
40d417038e adds a Start request to the req listener 2025-10-29 18:28:59 -04:00
Vitor Pamplona
895562b4a9 Simplifies readme 2025-10-29 18:28:38 -04:00
Vitor Pamplona
b8511b5ac3 Adds simplified methods to create Requests from NostrClient 2025-10-29 16:51:49 -04:00
Vitor Pamplona
40ce627de0 Merge 2025-10-29 15:57:48 -04:00
Vitor Pamplona
56a856cf71 Better filter for People Lists 2025-10-29 15:55:33 -04:00
Vitor Pamplona
904b8c445f Merge pull request #1540 from davotoula/1535-dont-use-own-follow-sets-for-follows-check
only check follow sets when user1 is the logged-in user
2025-10-29 15:47:30 -04:00
davotoula
3a744b9c1d only check follow sets when user1 is the logged-in user 2025-10-29 20:33:39 +01:00
Vitor Pamplona
6d8b5fa85d Moves message button to Profile Actions 2025-10-29 15:19:11 -04:00
Vitor Pamplona
a913194c41 Updates deprecated Clipboard manager 2025-10-29 15:18:57 -04:00
Vitor Pamplona
a9ebf561e7 Merge pull request #1539 from greenart7c3/main
Fix DecryptZapRequest CommandType
2025-10-29 14:00:05 -04:00
greenart7c3
fb7610cafd Fix DecryptZapRequest CommandType 2025-10-29 14:38:54 -03:00
Vitor Pamplona
cd35c8e30e uses faster spotless check on commits 2025-10-29 12:20:37 -04:00
Vitor Pamplona
f3209d2415 Moves SettingsCategory to take integers instead of Strings 2025-10-29 12:19:38 -04:00
Vitor Pamplona
d44b24936c Activates reverse scrolling for the new post screen in order to avoid hiding the cursor behind the keyboard when typing long texts. 2025-10-29 11:23:09 -04:00
Vitor Pamplona
a9aadb60fc Merge pull request #1538 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-29 09:56:13 -04:00
Crowdin Bot
59f2cdcb2f New Crowdin translations by GitHub Action 2025-10-29 13:21:40 +00:00
Vitor Pamplona
00eb6ee3f1 Adds a missing outbox popup on posting new notes 2025-10-28 19:09:24 -04:00
Vitor Pamplona
09e4b2ea44 Fixes inconsistent relay lists 2025-10-28 18:41:43 -04:00
Vitor Pamplona
f8b920a128 Adds a popup to warn users of the lack of inbox relays 2025-10-28 13:12:36 -04:00
Vitor Pamplona
0705ee9581 Unifying the parser for the tag array 2025-10-28 13:05:57 -04:00
Vitor Pamplona
bf664ea0c5 Avoids interning of large strings and objects where the duplication is not common 2025-10-28 12:18:19 -04:00
Vitor Pamplona
39f3aef7b5 Don't intern signatures, content and tags since they are not usually duplicated 2025-10-28 12:11:34 -04:00
davotoula
7254dce3a9 Revert "update buffer to 64kb"
This reverts commit 4e31a9a5ac.
2025-10-26 10:25:40 +01:00
davotoula
4e31a9a5ac update buffer to 64kb 2025-10-26 00:09:17 +02:00
davotoula
d01f6a2178 Change length to Long from Int: avoids potential overflow, Long seems to be used everywhere else 2025-10-25 23:54:56 +02:00
davotoula
2332623cde Create a CountingInputStream utility to avoid duplication
Prevent INT overflow in BlossomUploader.kt
Connection Cleanup in ImageDownloader.kt
Added try-finally in Sha256Hasher.jvmAndroid.kt
2025-10-25 23:43:24 +02:00
davotoula
93994564f6 Add streaming hash utility function to quartz multiplatform, follow the existing pool/worker design
Change hashing in ImageDownloader.kt to use streaming
2025-10-25 23:22:31 +02:00
davotoula
e1c54f52e3 stream file to calculate both hash and size without loading it all at once 2025-10-25 21:24:12 +02:00
Vitor Pamplona
371d3bf0f7 Improves paddings, language and add stickiness backgrounds 2025-10-24 19:08:26 -04:00
Vitor Pamplona
b0e7082ec6 Solves crash when the same user is in the private and public part of the list 2025-10-24 19:07:31 -04:00
Vitor Pamplona
34971211f1 Better icon padding 2025-10-24 19:06:36 -04:00
Vitor Pamplona
b065956aec Normalize thicknesses of the divisors 2025-10-24 19:05:35 -04:00
Vitor Pamplona
36f79ca581 Correct animations for the list screens 2025-10-24 19:02:52 -04:00
Vitor Pamplona
7aeb4e7582 Enabling more threads in debug 2025-10-24 18:51:18 -04:00
Vitor Pamplona
25d6562500 Solving lack of stream name 2025-10-24 18:08:39 -04:00
Vitor Pamplona
67e3b05a42 Uses the new nostrnwc deep link for connections 2025-10-24 17:53:31 -04:00
Vitor Pamplona
a157c60697 update compose 2025-10-24 14:59:06 -04:00
Vitor Pamplona
50d63a0501 making sure the disappearing modifier is not doing anything when the top or bottom bars are not present. 2025-10-23 19:24:23 -04:00
Vitor Pamplona
e1cbfbb280 Fixes large top bars on these screens 2025-10-23 17:46:26 -04:00
Vitor Pamplona
b6aec40e9a New Disappearing Top, Bottom and Action Scaffold 2025-10-23 17:45:21 -04:00
Vitor Pamplona
e70a191191 No need for disappearing scaffolds in these screens 2025-10-23 17:42:10 -04:00
Vitor Pamplona
59a5a15866 Merge pull request #1528 from davotoula/image-gallery-improve-layout
Image gallery improve layout
2025-10-23 09:13:44 -04:00
davotoula
b82145ac10 Merge branch 'main' into image-gallery-improve-layout 2025-10-23 15:01:29 +02:00
Vitor Pamplona
ee80361478 - Removes the deprecated hacks from Contact Lists
- Removes the use of kind 3 to show hashtags which fixes issues when people unfollowing the hashtag and it would just affect the InterestListEvent
2025-10-22 18:31:00 -04:00
Vitor Pamplona
2d551d4adf Fixes the visual references to communities and hashtags in the top right of the post 2025-10-22 18:27:53 -04:00
Vitor Pamplona
35da3b4efe Makes sure the list of geohashes and hashtags use lowercase words 2025-10-22 18:26:52 -04:00
Vitor Pamplona
5306dd52d7 spotless needs to clean the compiled files to compute all unused imports 2025-10-22 16:29:47 -04:00
Vitor Pamplona
2ada3855d0 Remove imports 2025-10-22 16:28:20 -04:00
Vitor Pamplona
cabdfea8f9 No need to avoid annotations with test 2025-10-22 16:28:10 -04:00
Vitor Pamplona
d9c0a9af43 Reactivates removing imports on ktint by default 2025-10-22 16:27:52 -04:00
Vitor Pamplona
a7c846f641 Removes NIP-04 DMs with hidden words from the Notification screen 2025-10-22 16:16:15 -04:00
davotoula
f7ed4d2aaa NEEDS testing:
Swapped the local var orientation by remember { mutableStateOf(...) } for an explicit MutableState: we now create orientationState = remember(firstImage) { mutableStateOf(...) }, update it with
    orientationState.value = …, and return orientationState.value.
2025-10-22 09:32:26 +01:00
Vitor Pamplona
031e604923 Only avoids sending a message for the request state if it is a new open request 2025-10-21 20:17:04 -04:00
Vitor Pamplona
551bd344e0 Makes sure to trigger null pointer exception on message deserializer 2025-10-21 20:15:48 -04:00
Vitor Pamplona
c21b2ca60f Removes the choice of Dispatchers from Quartz 2025-10-21 20:15:05 -04:00
Vitor Pamplona
db1d5c7e82 Adds the user outbox relays when loading addressables by that user. 2025-10-21 20:09:13 -04:00
Vitor Pamplona
17618ac361 Fixes bug when loading users 2025-10-21 19:29:43 -04:00
Vitor Pamplona
bf94d0d91c For reports, only use the outbox relay if the user has it. (don't use our default bootstrapping relays) 2025-10-21 19:09:58 -04:00
Vitor Pamplona
66096e5b98 Allows an empty metadata to reload everything about the user 2025-10-21 19:09:25 -04:00
Vitor Pamplona
c8a608718c Merge 2025-10-21 18:42:11 -04:00
Vitor Pamplona
df54f7bb67 Moves all Dispatchers.Default to IO because of the amount of Synchronized code that blocks the thread underneath each coroutine and since Default only has the exact number of threads the processor offers, once they block, we lose computing power 2025-10-21 18:30:08 -04:00
David Kaspar
5b230a4c9b Merge branch 'main' into image-gallery-improve-layout 2025-10-22 00:10:34 +02:00
Vitor Pamplona
d8907e3dfb Caches the User object to avoid being deleted by the soft reference on Local Cache 2025-10-21 17:52:45 -04:00
Vitor Pamplona
889e3d63ff Merge pull request #1527 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-21 16:06:01 -04:00
Crowdin Bot
5f7b318647 New Crowdin translations by GitHub Action 2025-10-21 20:04:21 +00:00
Vitor Pamplona
e2b2b10994 - Separates a relay failure tracker to a module
- Updates finding the outbox relay set to include the set of connected relays before all of the relays.
2025-10-21 15:55:32 -04:00
davotoula
a055f473cb prevent race condition: Added a state-aware rememberFirstImageOrientation helper so each gallery watches the cache until the first image’s real aspect ratio is known 2025-10-21 15:56:18 +01:00
davotoula
c735c358bb extracted common logic 2025-10-21 15:47:20 +01:00
davotoula
50cbbb55b6 three images case: test for aspect ratio of first image and adapt gallery based on that 2025-10-21 15:41:40 +01:00
davotoula
103845225b two images case: test for aspect ratio of first image and adapt gallery based on that 2025-10-21 15:19:35 +01:00
Vitor Pamplona
a27df3a9b6 Improving click and long press interactions with the relay list item and status bar items 2025-10-21 09:19:11 -04:00
Vitor Pamplona
d51d1f4453 Updates OkHttp, Tor and Spotless back 2025-10-21 08:38:53 -04:00
Vitor Pamplona
f899d446bc Merge 2025-10-21 08:37:53 -04:00
Vitor Pamplona
c1295ed97f Reverts composable naming conventions on spotless 2025-10-21 08:34:29 -04:00
Vitor Pamplona
774c5412d3 Merge pull request #1526 from davotoula/revert-spotless-plugin
revert spotless plugin  version
2025-10-21 08:32:47 -04:00
davotoula
38d5527b91 revert spotless plugin (need to find a solution for @Composable function names)
Remove unused imports
2025-10-21 12:47:42 +01:00
Vitor Pamplona
1ce352e69f Changes the icon of account preferences to translation for now 2025-10-20 21:00:11 -04:00
Vitor Pamplona
bf3e5c4568 no need for this anymore 2025-10-20 20:57:20 -04:00
Vitor Pamplona
200db73ce4 remove fmt.wiz.biz from bootstrap relays 2025-10-20 20:22:58 -04:00
Vitor Pamplona
2d1fd4a48a Remove companions 2025-10-20 20:22:37 -04:00
Vitor Pamplona
a0cb2a7db6 Creates a default relay list for indexers in new accounts 2025-10-20 20:22:20 -04:00
Vitor Pamplona
382319addf Adds a declared follows state 2025-10-20 20:21:18 -04:00
Vitor Pamplona
37625987c1 Only listens to notification changes when the UI is visible 2025-10-20 19:41:27 -04:00
Vitor Pamplona
63bd0b6cae Watches the okHttp client for web calls (only active when screen is visible) 2025-10-20 19:40:04 -04:00
Vitor Pamplona
48a69f1d26 Watches the correct OKHttp for relays 2025-10-20 19:39:33 -04:00
Vitor Pamplona
f663bb78aa Reduce error log sizes 2025-10-20 18:41:43 -04:00
Vitor Pamplona
a163c4188f Adjustments to the lazy initialization of the app 2025-10-20 18:41:00 -04:00
Vitor Pamplona
fabec35593 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-10-20 17:13:25 -04:00
Vitor Pamplona
e4ce7415a3 Don't use "when" when just one line is needed 2025-10-20 17:13:11 -04:00
Vitor Pamplona
911fdffa95 Turns the EOSE class into a mutable map for speed 2025-10-20 17:12:36 -04:00
Vitor Pamplona
3a73de6828 Separates the new GaleryParser from Composables so that later we can cache it and parse it on the Default thread. 2025-10-20 17:12:12 -04:00
Vitor Pamplona
c4653b147c Adds new benchmark cases 2025-10-20 17:10:08 -04:00
Vitor Pamplona
cbba2fe423 Merge pull request #1525 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-18 18:14:05 -04:00
Crowdin Bot
8aa7c57a24 New Crowdin translations by GitHub Action 2025-10-18 22:13:15 +00:00
Vitor Pamplona
e45b4b11dc Restructuring Relay systems to:
- Maintain order of incoming messages for relay listeners
- Defers all processing of incoming messages to coroutines via channels, freeing OkHttp's thread as soon as possible.
- Simplifies the main relay class by using attached listener modules for each function of the relay client.
- Migrate defaultOnConnect calls to become listener based and moved to NostrClients
- Treat counts as query only, not subscriptions.
- Coordinates REQs so that if an update is required to be sent but the server has not finished processing events, waits for it to finish and sends it later as soon as EOSE or Close arrives
- Correctly sustain the local state of each Req.
- Creates an Account follow list per Relay state that only includes shared relays as a better source of functioning relays
- Changes UserLoading features in a tentative to make them faster since they are used by all functions in the app.
- Correctly marks EOSE for filters that are aligned with the Req State from NostrClient
- Avoid subsequent REQ updates before EOSE or CLOSE calls.
- Refactoring RelayClient listener to be not dependent of which module is active for a relay client.
- Refactors authenticators to do complete operation as a module
- Breaks down Relay Client modules (Auth, Reqs, Counts, Event submissions) in the Relay Pool class.
- Creates listeners just for special REQ situations
- Move statistics to outside the base relay class as a listener
- Move logs to outside the base relay class as a listener
- Better structures a Standalone Relay client
- More appropriately communicate errors to the listeners
- Remove relay state on listeners
2025-10-18 18:10:45 -04:00
Vitor Pamplona
e20afd39a3 Removing onClosing listener because it isn't being used. 2025-10-18 17:12:10 -04:00
Vitor Pamplona
4c633c7dec Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-10-18 08:44:21 -04:00
Vitor Pamplona
97fc10d6ea Increases local video disk cache to 1GB 2025-10-17 18:00:38 -04:00
Vitor Pamplona
b8d0f057c5 Adds a way to remove everyone but some keys from the list of EOSEs 2025-10-17 14:30:13 -04:00
Vitor Pamplona
990cca3d64 Moving the EmptyOTS resolver to Quartz for test cases 2025-10-17 14:29:46 -04:00
Vitor Pamplona
4a0c924d4f Merge pull request #1524 from davotoula/use-runTest-instead-of-runBlocking
change from runBlocking to runTest where appropriate
2025-10-15 18:57:42 -04:00
davotoula
5d6e7d1921 change from runBlocking to runTest where appropriate 2025-10-15 21:00:00 +02:00
Vitor Pamplona
563e985a2b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-10-14 16:13:16 -04:00
David Kaspar
50adf82d90 Merge pull request #1523 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-14 22:11:49 +02:00
Vitor Pamplona
7a05f527dd Moving Log.java to Log.kt 2025-10-14 15:18:45 -04:00
Vitor Pamplona
0668f2caa4 Updates Compose, Firebase, GMS, JNA, mock, okhttp, secp256, spotless, vico, zoomable, androidXCamera 2025-10-14 14:38:28 -04:00
Vitor Pamplona
4af576f5f4 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-10-14 14:15:38 -04:00
Crowdin Bot
fdbf261455 New Crowdin translations by GitHub Action 2025-10-14 17:43:52 +00:00
Vitor Pamplona
e422dcda50 Merge pull request #1519 from davotoula/1516-Private-Public-Bookmark-Removal-Issue
Private public bookmark removal issue
2025-10-14 13:42:42 -04:00
David Kaspar
a210bd1c8f Merge pull request #1522 from davotoula/update-translations
Updated cs, se, de, pt
2025-10-14 19:23:26 +02:00
davotoula
49f77af3e8 Updated cs, se, de, pt 2025-10-14 18:59:54 +02:00
David Kaspar
7d22ef0d12 Merge pull request #1521 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-14 17:58:02 +02:00
Crowdin Bot
deee1a1c85 New Crowdin translations by GitHub Action 2025-10-14 13:50:15 +00:00
Vitor Pamplona
7a1dcbc76e Merge pull request #1520 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-14 09:49:15 -04:00
Vitor Pamplona
68264f2c7f Merge pull request #1518 from davotoula/bugfix-dont-render-gallery-for-mixed-images-and-videos
Bugfix: dont render gallery for mixed images and videos
2025-10-14 09:49:06 -04:00
Vitor Pamplona
b1d3aa968d Merge branch 'main' into bugfix-dont-render-gallery-for-mixed-images-and-videos 2025-10-14 09:48:58 -04:00
Crowdin Bot
2ebdedafaa New Crowdin translations by GitHub Action 2025-10-14 13:47:57 +00:00
Vitor Pamplona
4ffc121003 Merge pull request #1517 from davotoula/fix-streamable-videos
Use latest LightCompressor-enhanced
2025-10-14 09:46:37 -04:00
Vitor Pamplona
a4f1e45aac Merge pull request #1507 from KotlinGeekDev/followset-improvements
More follow set improvements.
2025-10-14 09:46:07 -04:00
davotoula
3a3d1a6f25 bugfix: don't remove from public bookmarks when removing from private 2025-10-14 13:31:27 +02:00
davotoula
85bdf67ac3 added failing test: Public tags should be preserved when removing from private bookmarks 2025-10-14 13:21:46 +02:00
KotlinGeekDev
9e991f8a49 Fix user removal functions, by updating them to take into account backend modifications made earlier. 2025-10-13 21:31:22 +01:00
KotlinGeekDev
e6c841373b String resources. Remove unused string resource in other locale-specific files, to avoid much work. 2025-10-13 21:04:42 +01:00
KotlinGeekDev
ba756c9acd Take into account profile membership when removing profile. 2025-10-13 20:30:18 +01:00
davotoula
349156f380 fix: Only render as gallery if all segments are images 2025-10-13 19:32:25 +02:00
davotoula
133c897731 Added junit and mockK for androidTest UI dependencies
Added failing androidTest for mixed image/video parsing
2025-10-13 18:38:14 +02:00
davotoula
13402fa5d4 Use lightcompressor-enhanced 1.6.0 release 2025-10-13 09:25:11 +02:00
davotoula
2a3c31cc5b Use latest LightCompressor-enhanced:
Fix for Progressive download
Replaced obsolete MP4 parser libraries with native MediaMuxer/MediaCodec
2025-10-12 21:13:12 +02:00
Vitor Pamplona
5fcf0c5938 Using a week as the default cut off for notifications from random relays. 2025-10-10 12:54:58 -04:00
Vitor Pamplona
0fab3afea2 Moves static relay stats to a injectable object 2025-10-10 11:38:22 -04:00
Vitor Pamplona
0bc38c1fa4 Removes sub stats from the Subscription controller to move outside. 2025-10-10 11:34:25 -04:00
Vitor Pamplona
0c0410d1dd Adds an isValid function to all commands so we can check before sending 2025-10-10 11:33:18 -04:00
Vitor Pamplona
fa971b1822 Removes stats from Orchestrator to move out of the inner classes and be an option for app developers instead. 2025-10-10 11:31:32 -04:00
Vitor Pamplona
24f7d76dc4 Adds a Count reply message to relays 2025-10-10 11:17:43 -04:00
Vitor Pamplona
973cfd24fa Creates our own Coil Logger to minimize trash stacks in the logs 2025-10-10 10:56:25 -04:00
Vitor Pamplona
5c09cd0b55 Rejects additional urls with %20 and fixes Wss ones. 2025-10-10 10:55:46 -04:00
Vitor Pamplona
5975130824 Avoids logging at the BasicOkHttp level 2025-10-10 10:51:09 -04:00
Vitor Pamplona
0a23e23801 Moves quartz to Java 21 due to binary inconsistencies between the multiple builds of KMP (tests conflicting with main) 2025-10-10 10:49:35 -04:00
Vitor Pamplona
a0c1b01a4b Adds a build template option for NIP42 auths 2025-10-10 10:48:39 -04:00
Vitor Pamplona
0708936c23 Mocks the android log on test cases 2025-10-10 10:47:59 -04:00
KotlinGeekDev
ac60e3d221 String resources. 2025-10-09 20:13:00 +01:00
KotlinGeekDev
3c622f544c Make a method specially for cloning/copying. Refactor createListWithDescription(). 2025-10-09 19:55:40 +01:00
KotlinGeekDev
a8d01e3667 Put in the relevant code in PeopleListEvent. Use DescriptionTag instead of manual construction. 2025-10-09 17:02:42 +01:00
KotlinGeekDev
9aa5f1bd9f Add support for modifying follow set descriptions. 2025-10-09 16:56:40 +01:00
KotlinGeekDev
192a257780 Merge branch 'vitorpamplona:main' into followset-improvements 2025-10-08 01:53:14 +00:00
Vitor Pamplona
0045426840 Fixes a bug of not loading some notification message kinds 2025-10-06 19:15:48 -04:00
Vitor Pamplona
0ab7dd9797 Adds test cases for the new sub map filters. 2025-10-06 18:55:23 -04:00
Vitor Pamplona
a6e4a8d4fc Performance improvement for the address serializer. 2025-10-06 18:05:03 -04:00
Vitor Pamplona
5f59f1ab41 Adds submap queries to the addressable large cache 2025-10-06 18:04:37 -04:00
KotlinGeekDev
049430cd2e Merge branch 'vitorpamplona:main' into followset-improvements 2025-10-06 21:25:27 +00:00
Vitor Pamplona
b546294078 getOrCreateUser already checks for Hex64. No need to do it twice 2025-10-06 15:44:13 -04:00
Vitor Pamplona
456ef86c85 Speeding up OkHttp startup 2025-10-06 15:17:37 -04:00
Vitor Pamplona
b8ad2961b9 Speeds up loading of the Follow lists in start up by using the new isHex64 method 2025-10-06 15:04:12 -04:00
Vitor Pamplona
0348454e36 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-10-06 14:47:00 -04:00
Vitor Pamplona
b55cdda52a Makes the order of the Tor Relay settings match across data objects 2025-10-06 14:42:25 -04:00
Vitor Pamplona
8c93560058 Fixes trusted relay lists incorrectly only using DM relays 2025-10-06 14:41:56 -04:00
David Kaspar
72b800bfb4 Merge pull request #1514 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-06 19:19:40 +01:00
Crowdin Bot
4a6dbd4ba5 New Crowdin translations by GitHub Action 2025-10-06 18:18:10 +00:00
David Kaspar
52686c3488 Merge pull request #1513 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-06 19:16:56 +01:00
David Kaspar
d8613da3ad Merge branch 'main' into l10n_crowdin_translations 2025-10-06 19:16:34 +01:00
Crowdin Bot
bea393c4ef New Crowdin translations by GitHub Action 2025-10-06 17:34:07 +00:00
Vitor Pamplona
cf4104292c Merge pull request #1511 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-06 13:32:25 -04:00
Vitor Pamplona
deaaa30b92 Merge pull request #1512 from davotoula/fix-nwc-string-deleted-on-restart
BUGFIX: Fix nwc string deleted on restart
2025-10-06 13:32:13 -04:00
davotoula
1fdf869c05 create putOrRemove overload for Nip47URI? 2025-10-06 18:56:12 +02:00
davotoula
701acb004f remove unnecessary suspend 2025-10-06 18:52:14 +02:00
davotoula
341994e505 The putOrRemove(key: String, event: Any?) signature loses type information. When JsonMapper.toJson() tries to serialize, it sees Any? and can't find the serializer for Nip47URI, causing:
SerializationException: Serializer for class 'Any' is not found
2025-10-06 18:50:15 +02:00
KotlinGeekDev
c432a54561 Merge branch 'vitorpamplona:main' into followset-improvements 2025-10-06 10:40:57 +00:00
KotlinGeekDev
55198b1546 Move one last string to string resources. 2025-10-06 11:39:43 +01:00
Crowdin Bot
b9d369a88c New Crowdin translations by GitHub Action 2025-10-06 08:01:52 +00:00
David Kaspar
6c72c36b9a Merge pull request #1510 from davotoula/update-translations
Update local versions of CS, DE since crowdin only uses those?
2025-10-06 09:00:33 +01:00
David Kaspar
369aab2c0c Merge pull request #1509 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-06 09:00:14 +01:00
davotoula
e129796418 Update local versions of CS, DE since crowdin only uses those? 2025-10-06 09:54:40 +02:00
KotlinGeekDev
eb99febb2b Merge branch 'vitorpamplona:main' into followset-improvements 2025-10-04 06:48:59 +00:00
Crowdin Bot
da79d5b0ba New Crowdin translations by GitHub Action 2025-10-03 22:39:37 +00:00
Vitor Pamplona
0843d5db95 Separates EOSE for drafts alone. 2025-10-03 18:37:48 -04:00
Vitor Pamplona
df77bd6633 Only loads drafts if the account is writeable. 2025-10-03 18:18:16 -04:00
Vitor Pamplona
cf8c8e47a2 Creates an option to ignore reconnection relays that should be used when the network settings change. 2025-10-03 18:03:04 -04:00
Vitor Pamplona
26462a150e Makes sure the scope is cancelled when the NostrClient is done 2025-10-03 18:01:34 -04:00
Vitor Pamplona
66a5412961 Migrates fDroid flavor to the new version of the markdown 2025-10-03 17:08:16 -04:00
Vitor Pamplona
93f33cca70 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	gradle/libs.versions.toml
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt
2025-10-03 16:46:03 -04:00
Vitor Pamplona
4b959577f0 Corrects follow list logs for performance and uses the right scope 2025-10-03 16:37:20 -04:00
Vitor Pamplona
88747ad1b7 Improves log messages in the relay services 2025-10-03 16:35:10 -04:00
Vitor Pamplona
13d05dc599 Loads the main account in parallel faster before the screen needs it 2025-10-03 15:59:43 -04:00
Vitor Pamplona
b002f9f37e Uses the new isHex64 in the ContactList 2025-10-03 15:58:38 -04:00
Vitor Pamplona
7994945209 30% Faster isHex for strings with 64 bytes. 2025-10-03 15:58:01 -04:00
Vitor Pamplona
9085f4bbea Merge pull request #1508 from greenart7c3/main
Fix sendAndWaitForResponse never receiving a response
2025-10-03 13:03:10 -04:00
greenart7c3
beff7e60b7 Fix sendAndWaitForResponse never receiving a response 2025-10-03 13:30:45 -03:00
KotlinGeekDev
e418a26f41 Add support for copying/cloning follow sets in the UI. 2025-10-03 16:26:20 +01:00
KotlinGeekDev
2df575d4af Modify the models to support creating a new list with several profiles. Use IO dispatcher in FollowSetState. Introduce follow set copying/cloning, with custom names/descriptions for the clones. 2025-10-03 16:02:15 +01:00
KotlinGeekDev
88fbee84dc Merge branch 'vitorpamplona:main' into followset-improvements 2025-10-03 14:39:45 +00:00
Vitor Pamplona
21c1d705a1 Only downloads DMs if the account is writeable 2025-10-03 09:18:53 -04:00
Vitor Pamplona
97668513b5 Fixes passing onClosed messages forward from NostrClient 2025-10-03 09:11:28 -04:00
Vitor Pamplona
f6213c2d9a Slightly better user profile post filter 2025-10-03 08:25:19 -04:00
Vitor Pamplona
85b234d372 Don't show default relays in the relay screen as a user-chosen list 2025-10-03 08:10:13 -04:00
Vitor Pamplona
28df59c00d Expand the number of notes tracked by the EOSE event to 10000 2025-10-02 17:55:43 -04:00
Vitor Pamplona
bc8e18a01b Initial test cases for NostrClient and extensions 2025-10-02 17:54:56 -04:00
Vitor Pamplona
1b36d7e189 Exposes the sample rate on BaseEoseManager class 2025-10-02 17:50:33 -04:00
Vitor Pamplona
5f83379459 Offers an interface to BaseEoseManager objects 2025-10-02 17:50:10 -04:00
Vitor Pamplona
90b682a38f Avoid sending close to subs that are already closed. 2025-10-02 17:48:06 -04:00
Vitor Pamplona
c6107dd784 Starts NostrClient in active instead of waiting for a connect() call 2025-10-02 17:47:39 -04:00
Vitor Pamplona
fd6be80b7e Offers a way to destroy subscriptions 2025-10-02 17:46:54 -04:00
Vitor Pamplona
f901f8ab8f Must use unlimited channels to avoid blocking the OkHttp threads. 2025-10-02 17:46:35 -04:00
Vitor Pamplona
b83ea61522 Moves OKHttp relay implementations to use a Channel in order to guarantee incoming message order. 2025-10-02 17:45:58 -04:00
Vitor Pamplona
168caf0ec6 Fixes comment 2025-10-02 17:42:33 -04:00
Vitor Pamplona
04f5e8738a Forces an updateFilter at the start of new subs. 2025-10-02 16:54:34 -04:00
Vitor Pamplona
c8f51380aa Fixes error messages in BasicRelayClient 2025-10-02 16:41:47 -04:00
Vitor Pamplona
612dd175b3 Creates the relay on sending new requests instead of waiting for updatePool method. 2025-10-02 16:40:45 -04:00
Vitor Pamplona
a56d9631ed Solves the issue of not connecting in the first time reconnect is called 2025-10-02 16:40:16 -04:00
Vitor Pamplona
1fd5ba0451 Adds ease of use functions for the downloadFirst Event extension method 2025-10-02 16:39:47 -04:00
Vitor Pamplona
132c00d434 Fixes suspending on coroutineScope and never returning bug 2025-10-02 16:39:20 -04:00
Vitor Pamplona
47cc8fc4ae Improves log class for JVM 2025-10-02 16:38:34 -04:00
David Kaspar
e12c52ec2c Merge pull request #1506 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-02 21:16:19 +01:00
Crowdin Bot
34cd26b004 New Crowdin translations by GitHub Action 2025-10-02 19:54:33 +00:00
Vitor Pamplona
1c82417a0a Updating the documentation on the use of Quartz 2025-10-02 15:53:10 -04:00
Vitor Pamplona
c3b87e83cb Merge pull request #1505 from davotoula/update-translations
update translations
2025-10-02 13:39:02 -04:00
davotoula
e72a588dab update CS, DE, SE, PT 2025-10-02 19:11:53 +02:00
David Kaspar
3033a4cbc8 Merge pull request #1504 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-10-01 17:10:27 +01:00
Crowdin Bot
ec82f89225 New Crowdin translations by GitHub Action 2025-10-01 15:30:00 +00:00
Vitor Pamplona
d7cf8305d3 Merge pull request #1503 from davotoula/smarter-video-compression--h265
Smarter video compression: h265
2025-10-01 11:28:34 -04:00
davotoula
7c41da0c4c Update lightcompressor to release 1.5.0 2025-10-01 16:57:21 +02:00
davotoula
59be0986eb Fix bug: useH265 parameter wasn't passed to compressor 2025-10-01 15:44:52 +02:00
davotoula
4554813d72 Add codec based multiplier (h265 = 0.7x) 2025-10-01 15:44:52 +02:00
davotoula
7d69122409 remove unused imports 2025-09-30 22:10:24 +02:00
davotoula
a6e306f3de Update to use lightcompressor library version that supports h265
Add h265 toggle to NewMediaView.kt and ImageVideoDescription.kt
Touch many files to add optional h265 boolean (default is false)
2025-09-30 22:02:18 +02:00
KotlinGeekDev
f6b3fdcab4 Refactor UI to adapt to correct way of managing follow sets. 2025-09-30 15:38:16 +01:00
KotlinGeekDev
dfb683e00d Continue refactoring of models to make things correct. 2025-09-30 15:37:02 +01:00
Vitor Pamplona
bb1d805080 Removes the need to use kotlinx.datetime on quartz 2025-09-29 18:06:58 -04:00
Vitor Pamplona
c936e91ec8 Merge pull request #1499 from KotlinGeekDev/follows-and-followsets-unified
Unification of follows and follow sets.
2025-09-29 13:47:39 -04:00
Vitor Pamplona
5d2e718b93 Merge pull request #1502 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-29 13:47:31 -04:00
Crowdin Bot
4cffd6d868 New Crowdin translations by GitHub Action 2025-09-29 17:25:18 +00:00
Vitor Pamplona
26005a062d Updates Quartz instructions to Maven 2025-09-29 13:23:57 -04:00
KotlinGeekDev
80906f2c5a Refactor models to take into account mixed lists, due to misunderstanding on my part. 2025-09-29 11:17:12 +01:00
Vitor Pamplona
631f1d9cfc Makes the relayDB not private in HintIndexer 2025-09-26 16:10:30 -04:00
Vitor Pamplona
a8a6a10d6e With filter serialization, prints kinds before anything else to help debug 2025-09-26 16:10:04 -04:00
Vitor Pamplona
401a3c47d9 Removes unnecessary log of object 2025-09-26 16:09:33 -04:00
Vitor Pamplona
ff0a4eb88f Fixes userList of the FollowList state 2025-09-26 16:09:07 -04:00
Vitor Pamplona
1384a68ee3 Dumps the number of relay hints urls in the DB 2025-09-26 16:08:37 -04:00
Vitor Pamplona
86e50ac984 Logs the early start of the Application for benchmarking purposes 2025-09-26 16:08:22 -04:00
KotlinGeekDev
d2d811f670 Just use remember{} instead of adding derivedStateOf{} to avoid weird state issues. 2025-09-26 16:21:13 +01:00
KotlinGeekDev
61459187d7 Fix duplicate copyright and use derivedStateOf for followSet presence checking variable. 2025-09-26 15:46:29 +01:00
KotlinGeekDev
509d703226 Merge branch 'vitorpamplona:main' into follows-and-followsets-unified 2025-09-26 12:20:47 +00:00
Vitor Pamplona
194ba08df0 Merge pull request #1498 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-26 08:20:19 -04:00
KotlinGeekDev
bf1188addf Merge branch 'vitorpamplona:main' into follows-and-followsets-unified 2025-09-26 12:12:28 +00:00
Crowdin Bot
7ddce17bdd New Crowdin translations by GitHub Action 2025-09-26 12:07:45 +00:00
Vitor Pamplona
ee8ac08845 Merge pull request #1496 from davotoula/smarter-video-compression--sub-1mbps
Smarter video compression - switch to LightCompressor-enhanced
2025-09-26 08:06:19 -04:00
KotlinGeekDev
45192d1cf7 Remove statements to avoid polluting the logs. Modify the timing on the flow producer. 2025-09-26 12:32:44 +01:00
KotlinGeekDev
9c024e334b Replace first hack with a better solution for determining a follow.
Add FollowSetState to MergedFollowListsState and modify it to take into account users from follow sets when displaying a user's follows feed.
2025-09-26 12:24:42 +01:00
KotlinGeekDev
058278956b Merge remote-tracking branch 'origin/follows-and-followsets-unified' into follows-and-followsets-unified 2025-09-26 10:55:48 +01:00
KotlinGeekDev
564cebea9d Merge branch 'vitorpamplona:main' into follows-and-followsets-unified 2025-09-26 09:55:31 +00:00
davotoula
4fc9386393 Combine calculation of videoBitrateInBps, resizer to use compressionRules only once 2025-09-25 23:06:16 +02:00
KotlinGeekDev
6368d17d09 Refactor: Create FollowSetState and move functionality from other places to it. Modify usages accordingly. 2025-09-25 21:39:02 +01:00
KotlinGeekDev
b694e143d0 Finish renaming refactor. 2025-09-25 21:37:33 +01:00
KotlinGeekDev
4b60c3e20b Refactor: From List -> Set, and Viewmodel rename as well. Move FollowSet and related classes to model package. 2025-09-25 21:36:34 +01:00
davotoula
6e4e432adf Use new bps api of lightcompressorlibrary fork 2025-09-25 22:01:25 +02:00
davotoula
4c3eaf972c move to LightCompressor-enhanced fork
corrected image vs video compressor library names
2025-09-25 22:01:25 +02:00
KotlinGeekDev
ba0fc67a94 First hack for bolting follow sets onto follows, for determining if a user follows another. 2025-09-25 18:24:31 +01:00
Vitor Pamplona
f31292a201 Updates compose, navigation, windowCore and tests 2025-09-25 12:59:24 -04:00
Vitor Pamplona
1f6135d45d Updates markdown parser to the latest 2025-09-25 12:53:45 -04:00
Vitor Pamplona
d04cf97418 Better logs on BasicRelayClient 2025-09-24 19:24:27 -04:00
Vitor Pamplona
8466020137 Increase the delay to update filters 2025-09-24 14:34:35 -04:00
Vitor Pamplona
54afc2c4dc Remove logs 2025-09-24 14:34:24 -04:00
Vitor Pamplona
ed7fa2a80f removes log 2025-09-24 12:43:15 -04:00
Vitor Pamplona
06f94150f9 Okhttp log helper class. 2025-09-24 12:43:04 -04:00
Vitor Pamplona
361e46c99f Limits the number of posts coming from big relays with too many authors to 500 max 2025-09-24 12:38:48 -04:00
Vitor Pamplona
6ba60dce3f Throws an error if the RelayStats cannot be created. 2025-09-24 12:38:01 -04:00
Vitor Pamplona
f9e0c82fc8 No need to import 500 communities for each relay it connects to 2025-09-24 12:37:35 -04:00
Vitor Pamplona
45b7b5601b Ups the max requests thread allowed to support people with over 1000 follows 2025-09-24 12:37:04 -04:00
Vitor Pamplona
ccf64687a8 Separates index relays from the default and put it into the hint-based queries for unkown users. 2025-09-24 12:36:39 -04:00
Vitor Pamplona
0ba3385771 Merge pull request #1494 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-24 08:41:08 -04:00
Vitor Pamplona
381692a0a2 Merge pull request #1491 from KotlinGeekDev/set-management-fixes
Set management UI fixes
2025-09-24 08:40:58 -04:00
Crowdin Bot
206f8847fc New Crowdin translations by GitHub Action 2025-09-24 12:39:55 +00:00
Vitor Pamplona
5b969d92f3 Merge pull request #1492 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-24 08:38:36 -04:00
Vitor Pamplona
621290ec33 Merge pull request #1493 from greenart7c3/main
Show a dialog to select a signer when using multiple signers
2025-09-24 08:38:20 -04:00
Crowdin Bot
94a2d9e07d New Crowdin translations by GitHub Action 2025-09-24 12:19:05 +00:00
Vitor Pamplona
8b60aeaa94 Merge pull request #1489 from davotoula/smarter-video-compression
Smarter video compression
2025-09-24 08:17:33 -04:00
greenart7c3
6391ddbbaa Show a dialog to select a signer when using multiple signers 2025-09-24 09:09:51 -03:00
KotlinGeekDev
6ac2a7150f Change appearance of CustomSetItem. Use horizontal dividers instead of rounded borders. Adjust padding. 2025-09-24 11:50:15 +01:00
KotlinGeekDev
ab6d0961e8 Even more size and color adjustments. 2025-09-24 11:37:25 +01:00
KotlinGeekDev
3c87925b38 Make border and padding adjustments to some components. 2025-09-24 10:57:44 +01:00
KotlinGeekDev
54963067aa Adjust colors for some components. 2025-09-24 09:55:53 +01:00
davotoula
ec9d229392 Merge branch 'main' into smarter-video-compression 2025-09-23 23:25:23 +02:00
Vitor Pamplona
ce81157dad Adds a maven publishing to ship quartz 2025-09-23 15:16:26 -04:00
Vitor Pamplona
0499dcc647 Refining the test case to avoid json comparisons 2025-09-23 13:57:34 -04:00
Vitor Pamplona
748085078d Fixes imports for Tlv tests 2025-09-23 13:46:04 -04:00
Vitor Pamplona
fadd9c1940 Creates a shared JVM-test module and moves the jackson inlining tests there 2025-09-23 13:45:48 -04:00
Vitor Pamplona
313e2519b2 Fixes the fdroid version of the new translation component api 2025-09-23 13:33:55 -04:00
Vitor Pamplona
d8c403d3d5 Removes the list view model from the LoginPage 2025-09-23 13:26:04 -04:00
Vitor Pamplona
b30d87266d Improves borders for zap messages in notifications 2025-09-23 13:11:15 -04:00
Vitor Pamplona
f9204d37f2 Updates vico charts 2025-09-23 12:36:12 -04:00
Vitor Pamplona
d3f1db4367 Updates std lib 2025-09-23 12:34:02 -04:00
Vitor Pamplona
9851c4b6be Updates firebase and lifecycleRuntime 2025-09-23 11:32:48 -04:00
Vitor Pamplona
811ad5e00a Merge pull request #1169 from KotlinGeekDev/profiles-list-management
List/Set management: Part 1 - Profiles.
2025-09-23 11:20:21 -04:00
KotlinGeekDev
f2ca1d78da Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-23 14:00:49 +00:00
KotlinGeekDev
00d5e2fed3 Move strings to stringResources. Refactor names for consistency. Some cleanup. 2025-09-23 14:51:05 +01:00
David Kaspar
807eda8401 Merge pull request #1490 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-23 13:21:59 +01:00
Crowdin Bot
6cffda09bb New Crowdin translations by GitHub Action 2025-09-23 11:59:59 +00:00
Vitor Pamplona
1c0766cc76 Merge pull request #1480 from davotoula/add-feed-indicator-to-live-bubbles
Add feed indicator to live bubbles
2025-09-23 07:58:26 -04:00
KotlinGeekDev
5175c61171 Merge branch 'upstream-main' into profiles-list-management 2025-09-23 12:24:28 +01:00
KotlinGeekDev
3f8abd74ba Merge branch 'main' of https://github.com/vitorpamplona/amethyst into upstream-main 2025-09-23 12:22:45 +01:00
KotlinGeekDev
04b8f6cca2 Move Fab back action button to separate component. Adjust colors for the fabs in CustomListsScreen. 2025-09-23 12:19:04 +01:00
KotlinGeekDev
ec10d53195 Add menu option for adding an author to a follow set from a note. 2025-09-23 12:16:58 +01:00
davotoula
ccc5d03d84 converted VideoCompressionHelper to singleton 2025-09-23 11:28:09 +02:00
davotoula
e5df870ace move framerate adjustment into getBitrateMbpsInt to prevent ,precision errors
Clean up logging
2025-09-23 10:24:48 +02:00
davotoula
b030486e7e fixes after rebase 2025-09-23 09:55:03 +02:00
davotoula
544d6d3933 remove plan 2025-09-23 09:25:20 +02:00
davotoula
db9160a6c2 finally block ensures release() is called even if exceptions occur 2025-09-23 09:25:20 +02:00
davotoula
41a233d175 fix for library bug. Temp file name with "_temp" is returned instead of final file name 2025-09-23 09:25:19 +02:00
davotoula
b6dd6a988b change to enum for video resolutions 2025-09-23 09:25:19 +02:00
davotoula
9af1e10ec2 refactor:
Safe file size lookup using OpenableColumns.SIZE
if (continuation.isActive) before resuming.
Better logging levels (Log.e for errors, Log.w for warnings).
Configurable compression timeout
2025-09-23 09:25:19 +02:00
davotoula
ed758ef13e remove unused code 2025-09-23 09:25:18 +02:00
davotoula
947f59fa6f add bitrate multiplier for 60fps content
fix toast when new file larger than old
2025-09-23 09:25:18 +02:00
davotoula
524ead2eac move video compression into helper 2025-09-23 09:25:16 +02:00
davotoula
49dc63c876 Sanity check: if compressed file is larger than original, return original 2025-09-23 09:24:25 +02:00
davotoula
8588c9a6fc update plan 2025-09-23 09:24:25 +02:00
davotoula
f805b70ec0 added log statements and a toast 2025-09-23 09:24:22 +02:00
davotoula
f5202dd8a9 added log statements 2025-09-23 09:22:13 +02:00
davotoula
97d6c791c0 determine resizer and bitrate from input source combined with our compression rules
use 1 as lowest BitrateMbps
2025-09-23 09:22:09 +02:00
davotoula
87f224b6b8 get video resolution, framerate and rotation 2025-09-23 09:21:32 +02:00
davotoula
931c6681c6 added compression rules 2025-09-23 09:21:32 +02:00
davotoula
96b8c77bcd there is a plan 2025-09-23 09:21:32 +02:00
David Kaspar
366534d1d5 Merge pull request #1488 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-23 08:11:41 +01:00
Crowdin Bot
054cfa0de9 New Crowdin translations by GitHub Action 2025-09-23 00:07:59 +00:00
Vitor Pamplona
d93a8d94d2 Reduces number of Notifications from random relays. 2025-09-22 20:05:07 -04:00
Vitor Pamplona
0026668aca Merging with upstream 2025-09-22 19:49:26 -04:00
Vitor Pamplona
7a280a3cde - Migrates Quartz from Android to CommonMain
- Fully converts OpenTimestamp Java codebase to Kotlin, migrating the sync and async HTTP call interfaces to OkHttp and coroutines
- Redesigns parsing of relay commands, messages and filters for performance in Jackson.
- Starts the use of KotlinX Serialization when speed is not a requirement
- Migrates all Jackson field annotations to Kotlin Serialization
- Migrates Regex use in Quarts to Kotlin's Regex class
- Migrates Base64 from Android to Kotlin
- Migrates UUID from Android/Java to Kotlin
- Migrates LRUCache usage from Android/Java to Kotlin collections
- Migrates all String to bytearray conversions to Kotlin methods
- Migrates all System.arraycopy calls to kotlin native ones.
- Separates parsing code from the data classes Companion objects
- Exposes Rfc3986 normalizations to each platform.
- Exposes URI parsing classes to each platform.
- Exposes URL Encoders to each platform.
- Exposes BigDecimal to each platform.
- Exposes the Url Detector to each platform.
- Exposes MacInstances to each platform
- Exposes Diggest instances to each platform.
- Exposes a BitSet to each platform.
- Exposes GZip to each platform.
- Exposes Secp256k1 to each platform.
- Exposes SecureRandom to each platform.
- Exposes Time in seconds to each platform.
- Exposes the LargeCache to each platform.
- Exposes AES CBC and AES GCM encryption/decryption to each platform
- Migrate test assertions to Kotlin Tests
- Exposes Address class to each platform because of the Parceleable
- Creates our own ByteArrayOutputStream.
- Removes Lock features inside the Bloomfilters because we don't need data consistency/
- Migrates UserMetadata parser from Jackson to Kotlin serialization
- Removes the need for Static methods in each tag.
- Adds an event template serializer
- Adds KotlinX Datetime to migrate some of the date-based logs
- Adds support for LibSodium in the JVM platform
- Creates a shared test build for iOS targets
- Fixes several usages of Reflection when serializing classes
- Fixes a bug on loading RelayDB for the HintBloom filter test
- Increases the Bloom filter space to better use hints in the app.
- Removes support for iOS in x86
- Creates a Jackson mapper just for NIP-55, which stays in the Android build only.
- Keeps the event store in the android build as well.
- Removes @Syncronized tags in favor of Mutexes.
- Improved sendAndWaitForResponse NostrClient method to properly account for returns from each relay.
- Removes the need for GlobalScope and async calls in the downloadFirstEvent method.
- Restructures the parser and serialization of the relay messages and commands for performance with Jackson
- Removes the dependency on Jackson's error classes across the codebase.
- Moves the hint to quote tag extension methods to their own packages.
- Speeds up the generation of Bech32 addresses
- Migrates NIP-6 and Blossom uploads to use Kotlin Serialization
2025-09-22 19:44:51 -04:00
KotlinGeekDev
3eb02cbbd8 implement a fix for when a follow set becomes empty(or is empty), which is probably a Nostr limitation. 2025-09-22 15:44:11 +01:00
KotlinGeekDev
1e18edfc02 Refactor FollowSetsManagementDialog UI to correctly adapt to follow set state. Small adjustment in the NostrUserListFeedViewModel. 2025-09-22 15:41:17 +01:00
KotlinGeekDev
9f5301d30a Make a different menu style for new list creation in FollowSetsManagementDialog. Use toBestDisplayName() instead of manual method(which was the same anyways). 2025-09-22 14:24:15 +01:00
KotlinGeekDev
a07ebc6e12 Update addFollowSet() method to optionally create a list with first member. 2025-09-22 14:20:59 +01:00
KotlinGeekDev
3301371d82 Fix color of Fabs in CustomListsScreen. 2025-09-22 14:19:10 +01:00
Vitor Pamplona
7949fc66b2 Fixes test case cast 2025-09-22 09:17:54 -04:00
davotoula
1d0f584c3e use produceState instead of LaunchedEffect 2025-09-22 13:58:48 +02:00
KotlinGeekDev
6a3ebc1d95 Move FollowSetsManagementDialog to correct package. Refactor accordingly. 2025-09-19 18:22:41 +01:00
KotlinGeekDev
e10c429de0 Completely change the UI for list management when adding/removing a profile from lists. 2025-09-19 18:15:18 +01:00
KotlinGeekDev
e4a8a512d2 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-19 15:23:13 +00:00
David Kaspar
8373f6ba19 Merge branch 'vitorpamplona:main' into add-feed-indicator-to-live-bubbles 2025-09-18 21:12:14 +01:00
Vitor Pamplona
d2d5ade84e Merge pull request #1485 from davotoula/fix-for-orientation-issue-following-removing-windowSizeClass-from-global-settings
Fix for properly switch between single and two-pane layouts on rotation
2025-09-18 14:45:23 -04:00
Vitor Pamplona
3acdde7c53 Merge pull request #1483 from davotoula/reduce-sonar-code-quality-warnings
Reduce sonar code quality warnings
2025-09-18 14:45:02 -04:00
Vitor Pamplona
897396ff24 Merge pull request #1484 from davotoula/remove-void-cat-from-default-servers
remove void.cat from default servers
2025-09-18 14:44:00 -04:00
davotoula
ee39b3ae18 Fix for properly switch between single and two-pane layouts on rotation 2025-09-18 20:37:56 +02:00
David Kaspar
6c892ba733 Merge branch 'vitorpamplona:main' into add-feed-indicator-to-live-bubbles 2025-09-18 15:55:20 +01:00
davotoula
64cf81b523 remove void.cat from default servers 2025-09-18 16:33:32 +02:00
KotlinGeekDev
acade3c762 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-18 13:32:24 +00:00
davotoula
4cd86c8b1c return the boolean results from file(npub).delete() 2025-09-18 15:16:40 +02:00
davotoula
fdf372719c Replace with interface delegation using "by" in the class header
Replace abstract class with an interface
2025-09-18 15:16:40 +02:00
davotoula
50f173e406 replace repeated text string with top-level constant 2025-09-18 15:16:40 +02:00
Vitor Pamplona
edc7f49ce2 Merge pull request #1482 from davotoula/investigate-disappearing-ping-in-ms-on-relays-screen
Fix disappearing stats on relay screen
2025-09-18 08:12:06 -04:00
davotoula
b20eb860ff Increase LruCache max size from 100 to 1,000 since we have so many more relays active with outbox 2025-09-17 20:38:40 +02:00
Vitor Pamplona
aba60a87d6 Merge pull request #1481 from davotoula/remove-unused-imports
Remove unused imports and fix a test
2025-09-17 12:16:04 -04:00
davotoula
4f3b7b24f2 remove unused imports 2025-09-17 16:19:40 +02:00
davotoula
6f44d7c28a remove unused imports amethyst
Fix ThreadDualAxisChartAssemblerTest (not sure how it was not breaking the build)
2025-09-17 16:10:23 +02:00
Vitor Pamplona
a96ab764b3 Updates all dependencies 2025-09-17 09:08:49 -04:00
Vitor Pamplona
1be46b7167 Merge pull request #1478 from davotoula/reduce-max-requets-when-in-emulator
Reduce max requests when in emulator
2025-09-17 09:06:14 -04:00
Vitor Pamplona
5c601d74b9 Merge branch 'main' into reduce-max-requets-when-in-emulator 2025-09-17 09:06:00 -04:00
davotoula
2027c7d77d use default settings (64) when in emulator 2025-09-17 15:01:46 +02:00
Vitor Pamplona
faffe2a936 About 30-40% event hashing performance boost 2025-09-17 08:40:06 -04:00
Vitor Pamplona
c375fa0ab8 Adds another catch to get object errors when parsing NIP-28 metadata 2025-09-17 08:40:06 -04:00
Vitor Pamplona
123eca1f90 Fixes NPE on the invoice parser 2025-09-17 08:40:06 -04:00
Vitor Pamplona
83e87c2cf4 Adds an event id hash check that doesn't create a separate bytearray to compare between the incoming hex from the relay and the recalculated byte array from SHA256. 2025-09-17 08:40:06 -04:00
davotoula
f4501d799d Added withContext(Dispatchers.IO) for network call 2025-09-17 14:29:42 +02:00
davotoula
5796052f36 added live/offline status indicator for the DisplayLiveBubbles method
Checks streaming URLs for LiveActivitiesChannel and relay URLs for both channel types
2025-09-17 14:29:42 +02:00
davotoula
c1012bd859 let's remove the maxRequestsPerHost increase, can be done in a separate PR 2025-09-17 11:22:28 +02:00
davotoula
6440b86a5e remove commented out code 2025-09-17 11:10:40 +02:00
davotoula
dddf65e60f Added test for emulator to set maxRequests to 32 to prevent crashing emulator
Increased maxRequestsPerHost in release version to 20 from default 5 to allow for more concurrent connections to a relay (subscribing to timeline + mentions + DMs + profile updates + notifications, etc..)
2025-09-17 11:10:10 +02:00
David Kaspar
80c884a4d1 Merge pull request #1477 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-17 06:10:11 +01:00
Crowdin Bot
497b019093 New Crowdin translations by GitHub Action 2025-09-16 20:57:54 +00:00
Vitor Pamplona
03b320e5b7 Merge pull request #1476 from davotoula/fix-benchmark-apk-location
resolve intermittent CI build failures - fix benchmark apk location
2025-09-16 16:56:43 -04:00
davotoula
00c0472155 remove second identical(?) test run 2025-09-16 18:38:46 +02:00
davotoula
8a66276755 fix benchmark apk location 2025-09-16 18:06:14 +02:00
KotlinGeekDev
6eab098f2e Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-16 13:02:13 +00:00
Vitor Pamplona
eaf8cb3384 Don't check for the max size in string anymore 2025-09-15 17:25:47 -04:00
Vitor Pamplona
cd0b1b65fb Merge expanding and checking Hmac functions to avoid re-creating the Mac instance. 2025-09-15 17:25:36 -04:00
Vitor Pamplona
e329b096ba Moves the message key to the HKDF function 2025-09-15 16:21:11 -04:00
Vitor Pamplona
239cac430e Adds a test for native vs libsodium chacha20 2025-09-15 16:15:07 -04:00
Vitor Pamplona
24a5750155 Generalizes fixed key setup 2025-09-15 16:14:43 -04:00
Vitor Pamplona
cb5299e265 Simple refactoring 2025-09-15 16:14:09 -04:00
Vitor Pamplona
c3c8b87b91 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-09-15 15:15:56 -04:00
Vitor Pamplona
98aaef61c7 Faster Hkdf functions with less array copying and allocations (which can be impactful if the ciphertext is large) 2025-09-15 15:15:44 -04:00
KotlinGeekDev
1d3dadace2 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-15 18:39:36 +00:00
Vitor Pamplona
bd70058f78 Avoids creating a byte array and recreating it again inside of the Mac instance. 2025-09-15 12:30:56 -04:00
Vitor Pamplona
67f9557081 Reduces the need to keep the memory for checking the Mac and decrypting at the same time, allowing the GC to run after the check was correct. 2025-09-15 12:30:26 -04:00
Vitor Pamplona
689066fd44 Benchmark for NIP-44 operations 2025-09-15 12:29:26 -04:00
David Kaspar
cdd5995513 Merge pull request #1474 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-15 17:11:16 +01:00
Crowdin Bot
0c4cd4f940 New Crowdin translations by GitHub Action 2025-09-15 16:09:54 +00:00
Vitor Pamplona
c84554576c Fixes flow.value warnings. 2025-09-15 12:08:04 -04:00
Vitor Pamplona
7616d01166 Implements NIP-44 extension for bigger payloads https://github.com/nostr-protocol/nips/pull/1907 2025-09-15 11:58:43 -04:00
Vitor Pamplona
60b71a5561 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-09-15 08:51:41 -04:00
Vitor Pamplona
a7b89cb778 Removing unused imports 2025-09-15 08:50:16 -04:00
David Kaspar
6b18711fe6 Merge pull request #1473 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-15 13:49:59 +01:00
Crowdin Bot
f0b95a3593 New Crowdin translations by GitHub Action 2025-09-15 12:42:18 +00:00
Vitor Pamplona
52cf9e1576 Merge pull request #1468 from davotoula/gallery-view-for-multiple-media
Gallery view for multiple images
2025-09-15 08:40:48 -04:00
davotoula
43f8053b30 Extracted Parsing logic into helper class 2025-09-14 13:19:12 +02:00
davotoula
74cc9535ac Added a data structure for ParagraphImageAnalysis
Single pass analysis
All image-related decisions are made based on the single analysis result
2025-09-14 12:12:44 +02:00
davotoula
7e80ed2af9 Fix for images are mixed with text in a single paragraph
nevent1qqs04pmdf8guxpyhjuvpeg72ccwuzje7vhlm09szxl2d8x28cse5fzgpz9mhxue69uhkummnw3ezuamfdejj7q3qwl89d7yazg500lehg08p45dj2jzhhyqg2erj067458e3wd30djnsxpqqqqqqzmdjgxd
2025-09-14 11:40:20 +02:00
davotoula
1788dfc0b9 Merge branch 'refs/heads/main' into gallery-view-for-multiple-media 2025-09-13 23:32:49 +02:00
davotoula
8f0a829a4d Optimised collectConsecutiveImageParagraphs: continue statements for early loop continuation 2025-09-13 23:32:17 +02:00
davotoula
bfc920ca04 Added logging of image gallery computation time 2025-09-13 21:09:27 +02:00
davotoula
a21b6115c7 Single pass to avoid multiple intermediate lists
O(n) traversal (no drop/takeWhile/filter allocations)
Mutable buffer for collecting images
2025-09-13 20:44:00 +02:00
davotoula
8e39da347a don't use image gallery for secret message 2025-09-13 18:16:43 +02:00
Vitor Pamplona
58277cf717 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-09-13 10:51:28 -04:00
Vitor Pamplona
7ef6da1a17 Merge pull request #1470 from davotoula/fix-for-trying-to-share-when-image-not-loaded
Fix for crash when attempting to share an image that is still loading
2025-09-13 10:51:11 -04:00
Vitor Pamplona
ec48977a6f Merge pull request #1471 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-13 10:50:29 -04:00
Vitor Pamplona
c5b60c3bd8 Refactor the UISharedPreferences into two files 2025-09-13 10:50:19 -04:00
Crowdin Bot
613978d7ba New Crowdin translations by GitHub Action 2025-09-13 14:48:30 +00:00
Vitor Pamplona
afd20d3f8f Avoids using JSON parsers with DataStore to speed up loading time (loading the parser itself takes ~300ms) 2025-09-13 10:46:27 -04:00
KotlinGeekDev
ea07e52e63 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-12 22:52:03 +00:00
Vitor Pamplona
da469754c4 Adds a All User Follows to fix: https://github.com/vitorpamplona/amethyst/issues/1431 2025-09-12 16:18:12 -04:00
davotoula
6dd2f47fc9 catch exception when sharing image
Show toast
2025-09-12 21:39:57 +02:00
Vitor Pamplona
47d201f936 Fixes unnecessary typecasting from long to int in created ats 2025-09-12 15:08:10 -04:00
Vitor Pamplona
46fec1d480 Adds a payment failed enum 2025-09-12 14:24:11 -04:00
Vitor Pamplona
557ba959e2 Fixes crash on dont translate from 2025-09-12 14:17:41 -04:00
Vitor Pamplona
e121b2849c Fixes subject calculations on rendering 2025-09-12 13:58:37 -04:00
Vitor Pamplona
2e16a1dca6 Fixes a bug when comparing int and long in the comparator 2025-09-12 13:36:25 -04:00
Vitor Pamplona
33155efa99 Bugfix in loading communities from Global 2025-09-12 12:52:36 -04:00
Vitor Pamplona
bd3eed4a29 No need to duplicate these relays anymore 2025-09-12 12:52:13 -04:00
Vitor Pamplona
ac48705980 Fixes showing blog posts in the future 2025-09-12 12:39:12 -04:00
Vitor Pamplona
bc10d7184f Fixes mark as read when drafts are the latest message in the chat 2025-09-12 11:37:58 -04:00
Vitor Pamplona
0886af4386 Migrates memory counters from Long to Int 2025-09-12 11:11:16 -04:00
Vitor Pamplona
3628091511 Applying Memory trimming service to all logged in accounts. 2025-09-12 10:09:25 -04:00
KotlinGeekDev
f5061e470a Merge upstream-adjusted changes. 2025-09-12 14:20:36 +01:00
KotlinGeekDev
a0ba3802d9 Merge upstream changes and adjust. 2025-09-12 14:14:52 +01:00
KotlinGeekDev
4118e3d45d Initialize NostrUserListVM at LoggedInPage level, to reuse it at different places(in prep for other UI changes). 2025-09-12 13:48:59 +01:00
KotlinGeekDev
f850d5291e Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2025-09-12 13:37:53 +01:00
David Kaspar
cb3bc605bc Merge pull request #1469 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-12 11:49:40 +01:00
davotoula
16e3cba651 found a place where image gallery was used for single images 2025-09-12 12:14:05 +02:00
davotoula
ee9e102eb2 removed lazy component from 20+ images case to avoid nesting lazy components 2025-09-12 10:54:17 +02:00
Crowdin Bot
f01895b3e9 New Crowdin translations by GitHub Action 2025-09-11 23:05:11 +00:00
Vitor Pamplona
00a3918d20 Fixes F-droid build 2025-09-11 19:02:58 -04:00
Vitor Pamplona
35a123a185 No need for this log 2025-09-11 18:53:03 -04:00
Vitor Pamplona
c9dc30d976 - Only shows live stream bubbles that are not 24/7
- Only shows user pictures of the active users in the past 15 minutes.
2025-09-11 18:52:14 -04:00
Vitor Pamplona
c06c03928e Fixes the use of index relays and search relays to load users and events as well as become the default for global feeds. 2025-09-11 18:36:42 -04:00
Vitor Pamplona
1f8d0295d5 - Separates Application dependencies into an AppModules class to create only after the OnCreate event.
- Switches TorSettings to be per Application and not per Account anymore
- Since TorSettings is now global, moves the okHttpClient determinations out of the Account-based classes into the Application.
- Since TorSettings is now global, set's up Coil's image loader only once when creating the Application
- Moves UISettings state to App Modules instead of viewmodel
- Migrates TorSettings and UISettings to DataStore
- Accounts now have their own coroutine scopes to allow cancellation when they are unloaded/logged off
- New tor evaluator service for relay connections now uses all account's trusted relays and dm relays at the same time.
- Migrates composable-state-based UISettings to Flow-based UI settings, while observing connectivity status
- Removes the displayFeatures and windowSizeClass from the shared model
- Fixes not requesting Notification Permissions for APIs older than Tiramisu in the FDroid flavor
- Moves the NIP-11 document cache from singleton to the App Modules
- Avoids using AccountViewModel to check NIP-11 Relay documents
- Moves the UI Settings usage in composables to functions that do not observe the state since they don't need to refresh the screen when changed.
- Refactors UI Settings screen to separate components and remove the sharedViewModel
- Only starts Internal Tor if that option is selected in the TorSettings.
- Turn TorSettings into a data class to observe changes to it
- Drops the SharedPreferences ViewModel to use UISettingsFlow directly from App Modules
- Reorganizes OTS Events after simplification of the OkHttp based on TorSettings.
2025-09-11 18:04:10 -04:00
davotoula
8f1027b55d refactor
Removed duplicate GalleryImage boilerplate.
Use .chunked()
Pull out constants (ASPECT_RATIO, IMAGE_SPACING).
2025-09-11 21:52:36 +02:00
davotoula
4dace5aaf8 refactor using takeWhile
Fix edge case with white space before images
2025-09-11 21:03:06 +02:00
davotoula
8cb9d13567 Use constrained LazyVerticalGrid with calculated height for more than 20 images for performance reasons
added vertical spacing for some padding
Fixing scrollable gallery issue
2025-09-11 21:01:12 +02:00
davotoula
f092326dcd Fixing scrollable gallery issue 2025-09-11 20:58:39 +02:00
davotoula
1f6d7d3fd2 Simplify RenderRegularWithGallery
Simplify collectConsecutiveImageParagraphs: just return the end index
Extracting common signature parameters into a RenderContext data class
Extracting GalleryImage helper function
2025-09-11 20:58:15 +02:00
davotoula
062182a7ec fix for when images are separated by empty lines
refactor to reduce duplication:
Extract RenderParagraphWithFlowRow
Extract collectConsecutiveImageParagraphs
2025-09-11 20:50:21 +02:00
davotoula
9534582141 Collect batches of images
Send them to new image gallery
2025-09-11 20:25:05 +02:00
Vitor Pamplona
a078df6159 Fixes parameters on the findEarliestOtsForNote 2025-09-11 11:28:46 -04:00
Vitor Pamplona
90a5c15ee9 Moves the OTS processor from Account's decrypt process to Application since it doesn't need the account information. 2025-09-11 11:27:29 -04:00
Vitor Pamplona
bb9a03ec5b Fixes invalid hexes on preview 2025-09-11 11:23:45 -04:00
Vitor Pamplona
825d890921 Removes the otsFinding method from accountViewModel, since it is not account dependent. 2025-09-11 11:19:27 -04:00
Vitor Pamplona
05c364eff4 removes unnecessary import 2025-09-11 11:17:08 -04:00
Vitor Pamplona
404d0ab40e Removes the dependency on windowSizeClass and displayFeatures to be passed in the global settings of the app (which is activity independenty) 2025-09-11 11:16:26 -04:00
Vitor Pamplona
bf88e376de Generalizes the dependencies from Application to Context 2025-09-11 11:12:20 -04:00
Vitor Pamplona
1e74a5769d Passes appcontext down on MediaSession Pool to avoid the direct dependency on Amethyst 2025-09-11 11:09:36 -04:00
Vitor Pamplona
e1f7fae70e Removes dependency in the Amethyst class from Playback calculator 2025-09-11 11:08:04 -04:00
Vitor Pamplona
5127907acd Makes the root okhttp client for an Empty manager a lazy property to allow loading in Preview screens 2025-09-11 11:07:00 -04:00
Vitor Pamplona
2eda4d40e8 Makes max exoplayer pool size to a variable in the class 2025-09-11 11:06:09 -04:00
Vitor Pamplona
7f428a9976 Switches account cache to a flow 2025-09-11 11:05:03 -04:00
Vitor Pamplona
4347a1b104 Generalizes the type of OtsResolver needed by OtsState 2025-09-11 11:03:05 -04:00
Vitor Pamplona
abd6e6f2ac Makes the torControl option nullable to allow an Orbot-based Active flow 2025-09-11 11:02:25 -04:00
Vitor Pamplona
1af5daffcf Fixes the default Tor Settings 2025-09-11 10:58:35 -04:00
Vitor Pamplona
0e04425095 Fixes the option switching animations when loading the screen. 2025-09-11 10:57:49 -04:00
Vitor Pamplona
bbbdc05243 Fixes animations when selecting Tor options in the privacy screen 2025-09-11 10:54:22 -04:00
Vitor Pamplona
d4c39bd9cb Adds a flow to TorSettings so that any change to the inner properties can be tracked. 2025-09-11 10:46:26 -04:00
Vitor Pamplona
11597d2448 Documents OTS methods 2025-09-11 10:45:35 -04:00
Vitor Pamplona
5c3ea0a302 removes duplicated copyright notice 2025-09-11 10:45:18 -04:00
Vitor Pamplona
0c65176ecc Adds a DefaultOtsResolver builder using the internal web request classes from java. 2025-09-11 10:45:02 -04:00
KotlinGeekDev
e72bb5044b Adjust size of delete button for a Follow set item. 2025-09-11 15:02:38 +01:00
KotlinGeekDev
e8d04a2205 Modify Follow and Unfollow buttons to only change looks when in profile actions. 2025-09-11 15:01:27 +01:00
KotlinGeekDev
31328089cb FollowSetScreen: -Make a separate component for the title and description.
- Fix onBroadcastList, making sure to get the latest changes to the set before broadcast.
-Make a separate component for FollowSet list items, and make the delete button look a bit better(?).
NostrUserListVM: - Minor cleanup.
2025-09-11 14:32:58 +01:00
KotlinGeekDev
bd41354009 Make some UI adjustments for the list creation dialog. 2025-09-11 14:05:28 +01:00
KotlinGeekDev
bac3c575cb Make some UI adjustments for the list rename dialog. 2025-09-11 14:03:29 +01:00
KotlinGeekDev
3c4865c4b0 Parse follow set descriptions as-is. 2025-09-11 14:01:12 +01:00
Vitor Pamplona
ba2707e7b4 Changes the name of the nav to avoid confusing the auto import 2025-09-10 19:59:48 -04:00
Vitor Pamplona
f9590055b0 Removes dependency on the application class from DecryptAndIndexProcessor 2025-09-09 12:17:28 -04:00
Vitor Pamplona
dc97e5004e Uses local copy of cache instead of global singleton 2025-09-09 12:16:52 -04:00
Vitor Pamplona
4fb629e893 Fixes not sending the live stream events anywhere when the stream doesn't have a relay set declared. 2025-09-09 11:25:59 -04:00
Vitor Pamplona
44a34ffcfe Removes livestream chats from the home bubbles that are placed after the live stream is done 2025-09-09 11:25:30 -04:00
Vitor Pamplona
f00207d53c Fixes reply routes when clicking in the Conversations tab when the event is a PublicChat, LiveStream or Ephemeral Chat 2025-09-09 10:28:49 -04:00
Vitor Pamplona
d25e6cd07b Small speed up with contains of a char and not a string 2025-09-09 10:28:01 -04:00
Vitor Pamplona
bbec0d93d2 Avoids breaking the contract (changes during sorting) 2025-09-08 19:38:18 -04:00
Vitor Pamplona
4158d3bd45 Fixes logs for account 2025-09-08 18:47:31 -04:00
Vitor Pamplona
49e4f7bb46 Fixes feed filter update when changing the top nav filter 2025-09-08 18:03:01 -04:00
Vitor Pamplona
4249b9f872 Fixes not having author picture in the long form previews 2025-09-08 18:01:26 -04:00
Vitor Pamplona
eac2e54953 Merge pull request #1467 from davotoula/prevent-resource-leaks-with-streams
Prevent resource leaks with streams
2025-09-08 17:27:47 -04:00
Vitor Pamplona
2455a34987 Moves account saving from StateViewModel to Account 2025-09-08 17:22:54 -04:00
Vitor Pamplona
ba0ce3366c Fixes f-droid code 2025-09-08 16:58:14 -04:00
Vitor Pamplona
ccf9f47305 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-09-08 16:50:06 -04:00
Vitor Pamplona
95cf3fb6eb Changing the DVM feed to sort by follows that liked or zapped the DVM 2025-09-08 16:49:30 -04:00
Vitor Pamplona
81dbebc5a4 Fixes deletion computation in the update of feeds (it was not considering the a-tag's date and deleting newer events than the deletion request) 2025-09-08 16:49:09 -04:00
Vitor Pamplona
26599fe24f Removes dependency on the viewModel on AccountFeedStates 2025-09-08 15:58:50 -04:00
Vitor Pamplona
ed1ea4d289 Removing the dependency on an application class from AccountViewModel 2025-09-08 15:50:54 -04:00
Vitor Pamplona
e0764da095 Creates an interface for the DualHttpClientManager 2025-09-08 15:50:28 -04:00
Vitor Pamplona
4c8456c4af Fixes the crash on starting when the contentResolver is not ready yet. 2025-09-08 15:50:07 -04:00
Vitor Pamplona
ac2609b4f6 Finish migration to INostrClient 2025-09-08 15:45:50 -04:00
Vitor Pamplona
67e2189676 Moves the event processor to the Account class 2025-09-08 15:06:15 -04:00
Vitor Pamplona
222615a116 Establishing a scope for each account loaded. 2025-09-08 14:37:45 -04:00
davotoula
8f623ebb91 add TODO to investigate. Double closing of stream most often is ignored. 2025-09-08 20:32:28 +02:00
davotoula
adc4aa7906 cleaner code 2025-09-08 20:32:28 +02:00
davotoula
ec362ea0ee Use .use for streams
cleaner code
2025-09-08 20:32:28 +02:00
davotoula
e66e71a05c clener code 2025-09-08 20:32:28 +02:00
davotoula
9be55eda07 close FileInputStreams to prevent resource leak 2025-09-08 20:32:28 +02:00
David Kaspar
16b5d44c3d Merge pull request #1466 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-08 18:38:01 +01:00
Vitor Pamplona
93a639fea5 Modifies the Notification processor to use full accounts and their cached structrues again. 2025-09-08 13:29:02 -04:00
Vitor Pamplona
360f71c79a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-09-08 13:28:23 -04:00
Crowdin Bot
1f38ae5deb New Crowdin translations by GitHub Action 2025-09-08 17:26:27 +00:00
Vitor Pamplona
f633d57277 Merge pull request #1465 from greenart7c3/main
When using intents check if it's rejected
2025-09-08 13:25:19 -04:00
greenart7c3
196a7de6a3 When using intents check if it's rejected 2025-09-08 14:05:00 -03:00
David Kaspar
4c1d0ac60c Merge pull request #1464 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-08 07:58:20 +01:00
Crowdin Bot
1bdf7d9884 New Crowdin translations by GitHub Action 2025-09-08 06:57:03 +00:00
David Kaspar
9e1d2c89bb Merge pull request #1463 from davotoula/update-translations
added CZ, DE translations
2025-09-08 07:55:46 +01:00
davotoula
a58dc09b80 added CZ, DE translations 2025-09-08 08:34:16 +02:00
David Kaspar
91f85a1e1c Merge pull request #1462 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-08 07:05:42 +01:00
Crowdin Bot
cf9d630032 New Crowdin translations by GitHub Action 2025-09-08 06:03:52 +00:00
David Kaspar
f55b0f528c Merge pull request #1461 from davotoula/update-translations
updated PT, CZ, SE, GE translations
2025-09-08 07:02:34 +01:00
davotoula
463762f268 added PT, CZ, SE, GE translations 2025-09-07 19:17:53 +02:00
Vitor Pamplona
82770c204c Merge pull request #1460 from davotoula/update-secp256k1
update secp256k1KmpJniAndroid to 0.19.0
2025-09-07 12:56:13 -04:00
Vitor Pamplona
0621cfb46c Small correction in the method name 2025-09-07 12:23:34 -04:00
davotoula
1f1a7eb12c update secp256k1KmpJniAndroid to 0.19.0 2025-09-07 17:59:08 +02:00
Vitor Pamplona
93e2f66559 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-09-06 11:59:38 -04:00
Vitor Pamplona
cc2b836cd5 Migrates Account management to an Application class. 2025-09-06 11:59:01 -04:00
Vitor Pamplona
fd2a227674 Fixes the new theme parameter 2025-09-06 11:59:01 -04:00
Vitor Pamplona
67937b9fbc Nicer way to create the account view model 2025-09-06 11:59:01 -04:00
Vitor Pamplona
ea58b4fa3d Adds a dependency on Account and to the NWC state to avoid linking directly with the Application class 2025-09-06 11:59:01 -04:00
Vitor Pamplona
7087fb85f0 Changes the Theme class to only take the preferred theme directly. 2025-09-06 11:59:01 -04:00
Vitor Pamplona
caf325f240 Sets the instance of the Application class before anything else 2025-09-06 11:59:01 -04:00
Vitor Pamplona
18b2fdb7e4 Removes one of the Application dependencies in viewmodel 2025-09-06 11:59:01 -04:00
Vitor Pamplona
66eef96ccb Offers way to load the Application class from Context 2025-09-06 11:59:01 -04:00
Vitor Pamplona
757806341c Adds an isMobileOrFalse flow in connectivity 2025-09-06 11:59:01 -04:00
Vitor Pamplona
69e3d2d0d8 Creates an interface for NostrClient 2025-09-06 11:59:01 -04:00
Vitor Pamplona
a03ba7b260 Refactors zap the devs card view 2025-09-06 11:59:01 -04:00
Vitor Pamplona
51f81dce73 Merge pull request #1459 from davotoula/main
Setting JvmTarget to 21 for quartz
2025-09-06 11:57:43 -04:00
davotoula
32d2ee698a fix for missing lazysodium 2025-09-06 15:53:11 +02:00
davotoula
8aaea960f0 Set quartz JVM target to 1.8 to allow for lower requirements for building library 2025-09-06 15:34:06 +02:00
davotoula
ab9ee36263 Setting JvmTarget to 21 for quartz to fix build/push issue due to mixed Java versions when using Java 23 locally 2025-09-05 20:01:49 +02:00
KotlinGeekDev
f9cf348f8f Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-05 12:56:23 +00:00
Vitor Pamplona
254ac1a1cb Trying to remove firebase analytics 2025-09-04 21:05:06 -04:00
Vitor Pamplona
2ffa029790 Small refactoring on NotificationUtils 2025-09-04 21:04:43 -04:00
Vitor Pamplona
b0e27d0748 Forces relay status updates when connecting and disconnecting 2025-09-04 21:02:20 -04:00
Vitor Pamplona
a4569675c2 Merge pull request #1458 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-04 11:16:18 -04:00
Crowdin Bot
88457eb101 New Crowdin translations by GitHub Action 2025-09-04 15:13:49 +00:00
Vitor Pamplona
103ef01be4 Adds support for NIP-53 Meeting room to quartz 2025-09-04 11:10:54 -04:00
Vitor Pamplona
fcef6df7db Merge pull request #1457 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-09-03 18:10:34 -04:00
Crowdin Bot
19c24af592 New Crowdin translations by GitHub Action 2025-09-03 22:05:06 +00:00
Vitor Pamplona
7de119dd4f removes logs 2025-09-03 17:48:24 -04:00
Vitor Pamplona
8d73bf2cc4 Makes the new Video events non replaceable, while keeping the old ones in the replaceable 2025-09-03 17:44:40 -04:00
Vitor Pamplona
8131f3db39 Update AGP 2025-09-03 16:57:38 -04:00
Vitor Pamplona
e4e52ec4e6 Unify outbox calculation in the RelayOutbox loader. 2025-09-03 16:51:27 -04:00
Vitor Pamplona
43c9df23c4 Removes common mistakes from the outbox list from follows 2025-09-03 16:37:30 -04:00
Vitor Pamplona
85fb6dcc83 Avoids breaking line in the relay info top nav bar title 2025-09-03 16:35:10 -04:00
Vitor Pamplona
c9cdc74c91 Removes duplicated Loading of Relay Info 2025-09-03 15:45:21 -04:00
Vitor Pamplona
c64e65ddb4 Migrates to use suspending routines for OTS, statuses and edits 2025-09-02 19:20:44 -04:00
Vitor Pamplona
c46cf79501 Correctly sorts by online status when the app has already cached the status of a streaming 2025-09-02 18:34:21 -04:00
Vitor Pamplona
eef0adc6f9 Resets streaming url check if offline and entering a chat of that streaming 2025-09-02 18:30:47 -04:00
Vitor Pamplona
480ef63aed adds a border on the users of a given relay in the connected relay list 2025-09-02 18:30:01 -04:00
Vitor Pamplona
7a807e94af Exposes active filters per relay on the Nostr Client for debugging purposes 2025-09-02 18:28:41 -04:00
Vitor Pamplona
116bd1ae29 Removes any relay url that has the null byte, regardless of size 2025-09-02 18:28:14 -04:00
Vitor Pamplona
fb5c20d8da Abandoning the precision of Mills to seconds on the isOnline Check 2025-09-02 18:27:46 -04:00
Vitor Pamplona
0bc0f503be Reduces errors when trying to parse unparseable NIP-11s 2025-09-02 17:34:15 -04:00
KotlinGeekDev
70c0668962 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-02 21:23:29 +00:00
Vitor Pamplona
f6d283dafb Refining the performance of the QuickAction menu bar 2025-09-02 15:32:28 -04:00
Vitor Pamplona
7048a1da50 Fixes invalid dependency on Quartz 2025-09-02 15:30:57 -04:00
Vitor Pamplona
36b541e0c7 Fixes NPE when updating a metadata with null name 2025-09-02 14:35:43 -04:00
Vitor Pamplona
501fa25e98 v1.03.0 2025-09-02 14:01:15 -04:00
Vitor Pamplona
b2516bbae9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-09-02 13:43:56 -04:00
Vitor Pamplona
fcd56fdb3a Improves list of visible authors on live stream bubbles 2025-09-02 13:26:19 -04:00
Vitor Pamplona
8db90728aa Removes wss://%2A%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00.isekco.re/ 2025-09-02 12:37:38 -04:00
Vitor Pamplona
35146cc794 Adds the error code to avoid testing it again 2025-09-02 12:36:43 -04:00
KotlinGeekDev
bc395a09bd Merge branch 'vitorpamplona:main' into profiles-list-management 2025-09-01 17:28:50 +00:00
Vitor Pamplona
b6cd8d7b05 Merge pull request #1453 from KotlinGeekDev/quartz-kmp-build-fixes
Fix Quartz build issues.
2025-09-01 12:29:58 -04:00
KotlinGeekDev
822edf1273 Re-enable publish in androidTarget KMP section, as it works in conjunction with the Android publish block. 2025-09-01 16:47:56 +01:00
KotlinGeekDev
2f2919090e Disable publishing in androidTarget KMP configuration. Re-enable in Android block, and use updated syntax. 2025-09-01 16:40:36 +01:00
KotlinGeekDev
edf5b28e06 Verifying some changes. 2025-09-01 16:26:57 +01:00
KotlinGeekDev
c828299793 Update sourceset folder names. 2025-09-01 15:24:22 +01:00
KotlinGeekDev
cf13d24c93 Revert to using androidLibrary plugin until KMP publishing guidance is updated. Use default sourcesets for Android. 2025-09-01 15:22:53 +01:00
Vitor Pamplona
56f93170e5 Fixes product title when in a quoted post 2025-08-31 21:25:08 -04:00
Vitor Pamplona
ed4839bc4d Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-31 12:27:57 -04:00
Vitor Pamplona
b8c39ca5b2 Updating firebase 2025-08-31 12:23:32 -04:00
Vitor Pamplona
5397e4e22e Moves SoftCache logs to the app 2025-08-31 12:10:51 -04:00
Vitor Pamplona
ea7a2f3426 Fixes live flag when the URL is not available anymore. 2025-08-31 12:09:01 -04:00
Vitor Pamplona
f2ae8afe3b Merge pull request #1450 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-31 11:26:43 -04:00
Crowdin Bot
ee235ce682 New Crowdin translations by GitHub Action 2025-08-31 15:17:12 +00:00
Vitor Pamplona
408a9725e0 Fixes bug on opening the Relay Settings page with duplicated Trusted and Blocked relays. 2025-08-31 11:13:56 -04:00
Vitor Pamplona
1112fa96aa Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-30 14:22:45 -04:00
Vitor Pamplona
7da309c9cf Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-30 14:22:33 -04:00
Vitor Pamplona
caf4b5ca0b Merge pull request #1449 from newrizen/English-strings.xml
Update english strings.xml
2025-08-30 14:18:32 -04:00
Neil Rizen
c8d6a46444 Update english strings.xml
According to PR #1444, commit 64df323, which changed the hyphen that was being applied to all languages ​​to a space, this is the adjustment made in English and will be followed for other languages.

[pt_rbr already adjusted]
2025-08-30 15:08:35 -03:00
Vitor Pamplona
dd5ea51575 Updates the User Profile's Relay List to an outbox version 2025-08-30 13:49:24 -04:00
Vitor Pamplona
9d4c690760 Adds a follow list state from kind 3 to keep following users in memory. 2025-08-30 13:18:04 -04:00
Vitor Pamplona
d7a3cf94b7 Moves from RelationshipStatus to ContactCard as per NIP update 2025-08-30 13:11:29 -04:00
Vitor Pamplona
a063107a15 Adds a normalized parser to all nip65 relays 2025-08-30 12:45:32 -04:00
Vitor Pamplona
f06fc8a9cb Merge pull request #1448 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-30 11:05:21 -04:00
Crowdin Bot
0c386811e0 New Crowdin translations by GitHub Action 2025-08-30 15:02:29 +00:00
Vitor Pamplona
4c34c6b8e3 Simple fix on url normalization 2025-08-30 10:59:53 -04:00
David Kaspar
4c43399cce Merge pull request #1447 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-30 15:44:31 +01:00
Crowdin Bot
b8ffb01073 New Crowdin translations by GitHub Action 2025-08-30 13:35:16 +00:00
Vitor Pamplona
50275548b8 Merge pull request #1443 from davotoula/capture-and-post-video
Capture and post video
2025-08-30 09:34:14 -04:00
Vitor Pamplona
64df3236ce Merge pull request #1444 from newrizen/Fix-TranslatableRichTextViewer.kt
Fix hyphen applied in all languages in TranslatableRichTextViewer.kt
2025-08-30 09:33:45 -04:00
Vitor Pamplona
c5a7293120 Merge pull request #1445 from davotoula/remove-unused-imports-amethyst
remove unused imports [amethyst]
2025-08-30 09:33:24 -04:00
Vitor Pamplona
4a969cb77c Merge pull request #1446 from davotoula/remove-unused-imports-quartz
remove unused imports [quartz]
2025-08-30 09:33:17 -04:00
davotoula
8fffe3f662 remove unused imports 2025-08-30 09:10:28 +01:00
davotoula
f5722eb963 remove unused imports 2025-08-30 08:25:30 +01:00
Neil Rizen
f5289cb3d3 Update TranslatableRichTextViewer.kt
Changed the hyphen to a space to move the word "auto-translated" (and its translations) to "translations_auto" string and "from" (and its translations) to "translated_from" string, as this hyphen only exists in some languages.
2025-08-29 21:52:48 -03:00
Vitor Pamplona
0064a4c596 Trying to avoid an NPE in the maxOfOrNull iterator 2025-08-29 17:58:39 -04:00
Vitor Pamplona
e9d9ef197f Activates live streams to the top feed bubble 2025-08-29 16:55:29 -04:00
Vitor Pamplona
a5a6c875fa Adds a listener to check the use of custom TTS Engines 2025-08-29 12:27:17 -04:00
Vitor Pamplona
8e8521fd6d Assemble report properties as a table and crash as a code block 2025-08-29 10:02:39 -04:00
davotoula
e387e04b6d Added "record and post video" button to GenericCommentPostScreen.kt (GeoHashTag post and HashTag post) 2025-08-29 14:47:25 +01:00
davotoula
001167ff23 Added "record and post video" button to NewProductScreen 2025-08-29 14:38:19 +01:00
Vitor Pamplona
449a3f541e Improves the design of the Crash Report permission screen with a cancel button.
Deleted copy stack to clipboard
2025-08-29 09:36:33 -04:00
Vitor Pamplona
3b59b11e8b Ignores out of memory exceptions because the stack trace is irrelevant in those cases. 2025-08-29 09:35:05 -04:00
Vitor Pamplona
aaae806505 Makes the exception name the first line in the report 2025-08-29 09:34:41 -04:00
Vitor Pamplona
458a7c7377 Adds support for expirations in DMs 2025-08-29 09:34:26 -04:00
davotoula
c95ab2d6b7 Added "record and post video" button to NewPublicMessageScreen (I'm not sure it's used) 2025-08-29 14:27:59 +01:00
davotoula
6a3aae7dbc Added "record and post video" button to NewGroupDMScreen 2025-08-29 14:14:03 +01:00
davotoula
9179e18b06 Added "record and post video" button to media tab 2025-08-29 14:06:39 +01:00
Vitor Pamplona
ecf530d512 Merge pull request #1442 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-29 08:02:42 -04:00
Crowdin Bot
51f7081214 New Crowdin translations by GitHub Action 2025-08-29 11:41:25 +00:00
Vitor Pamplona
7f7debbb73 v1.02.1 2025-08-29 07:38:42 -04:00
Vitor Pamplona
60377c0c35 Fixes inability to delete relays from certain relay lists. 2025-08-29 07:38:18 -04:00
Vitor Pamplona
a594d741b1 Creates an additional pointer for Account lists to make sure the GC doesn't clear instances that are only stored in the LocalCache's WeakReference DB. 2025-08-29 07:37:24 -04:00
davotoula
3dd791fbbe Change LOW video compression to use VERY_LOW from lightcompressorlibrary for better streaming experience
LOW: 20% of original
VERY_LOW: 10% of original
2025-08-29 10:35:27 +01:00
davotoula
87afb7f491 refactor getVideoUri and getPhotoUri to reduce duplication 2025-08-29 10:33:39 +01:00
davotoula
5f8519d737 added translations 2025-08-29 09:19:58 +01:00
davotoula
f83166e576 Add take video functionality
Add take video button to new post screen
I18N
2025-08-29 09:13:26 +01:00
Vitor Pamplona
df85c3969c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-28 18:19:49 -04:00
Vitor Pamplona
e4311ddfa3 Creates a bootstrap relay list for inbox that doesn't have nostr.wine to avoid sending posts there and getting the notify 2025-08-28 18:12:55 -04:00
Vitor Pamplona
3a9fd925e1 Merge pull request #1440 from davotoula/upgrade-agp
Upgrade agp and add JvmTarget.JVM_21 to Quartz
2025-08-28 18:08:39 -04:00
davotoula
dbcca9cbd2 Use JvmTarget JVM_1_8 for quartz to be more forgiving about requirements. 2025-08-28 22:48:18 +01:00
davotoula
ef250eb0f1 Consistent JvmTarget JVM_21 across all modules 2025-08-28 22:17:44 +01:00
davotoula
8c26af7d40 upgrade agp to 8.12.2 2025-08-28 22:17:44 +01:00
KotlinGeekDev
3cfe195561 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-28 15:43:27 +00:00
KotlinGeekDev
e3cfa76843 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-28 15:08:48 +00:00
KotlinGeekDev
a23bc875cf Merge latest upstream changes. 2025-08-28 14:42:53 +01:00
KotlinGeekDev
92fac53705 Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2025-08-28 14:42:09 +01:00
KotlinGeekDev
1b011acbd7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst ite resolve branch. 2025-08-28 14:38:56 +01:00
KotlinGeekDev
f651dd964d Refactor FollowSetScreen parameters. Add new toast message type that includes an action to perform on dismissal. 2025-08-28 14:27:50 +01:00
KotlinGeekDev
53b74e6f82 Add Nav route for FollowSetScreen. Refactor other places accordingly. Make minor cleanup. 2025-08-28 14:25:39 +01:00
KotlinGeekDev
c1a5052cbb Fix bug in addFollowSet, and refactor to use Boolean for checking list visibility. Use runBlocking in getFollowSetNOte(for now). 2025-08-28 14:20:07 +01:00
KotlinGeekDev
3b23b9eeef Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-27 14:35:06 +00:00
KotlinGeekDev
685365eb12 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-25 20:34:00 +00:00
KotlinGeekDev
34a75dd28f Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-25 14:18:23 +00:00
KotlinGeekDev
a3be27723d Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-20 21:54:12 +00:00
KotlinGeekDev
62f2c8c01c Remove followSetsFlow from AccountViewModel in favor of using the NostrUserListFeedViewModel. Remove unused code, and refactor accordingly. 2025-08-18 21:23:22 +01:00
KotlinGeekDev
c06408537a Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-18 16:23:06 +00:00
KotlinGeekDev
850b9afc66 Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2025-08-18 17:22:26 +01:00
KotlinGeekDev
909c704bda Improved version of NostrUserListFeedViewModel. Remove unneeded LaunchedEffect in CustomListsScreen. 2025-08-18 17:21:26 +01:00
KotlinGeekDev
bcebfa58f1 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-16 01:34:30 +00:00
KotlinGeekDev
8585620045 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-14 04:22:54 +00:00
KotlinGeekDev
2238f4a807 Use runBlocking in mapEventToSet(). Rework and simplify fetching in FollowSetFeedFilter. Make FollowSetActions dropdown menu more reliable. 2025-08-10 02:22:20 +01:00
KotlinGeekDev
653494e922 Cleanup/Refactor:Remove followSetNotes property from User, and refactor accordingly. Make getFollowSetNotes() return the notes. 2025-08-10 02:18:03 +01:00
KotlinGeekDev
9e7d3146fa Merge branch 'vitorpamplona:main' into profiles-list-management 2025-08-07 19:52:15 +00:00
KotlinGeekDev
2ff56e8af8 Rename FollowList ViewModel to match it's actual use. 2025-08-06 22:17:06 +01:00
KotlinGeekDev
b1e4f65be8 Remove code that was reintroduced during merge with upstream. Fix Lists route. 2025-08-06 21:45:34 +01:00
KotlinGeekDev
1ce829ba23 Bring back some lost changes, and fix styling. 2025-08-06 20:26:03 +01:00
KotlinGeekDev
e92e36ceda Sync with upstream 2025-08-06 19:11:07 +00:00
KotlinGeekDev
b8b4377891 Upstream + adjustments. 2025-08-06 20:09:02 +01:00
KotlinGeekDev
981486f09a Add other functions and adapt them to the code already present. 2025-08-06 18:56:15 +01:00
KotlinGeekDev
7f8b2a6ccf Fix unresolved conflicts. 2025-08-06 13:34:08 +01:00
KotlinGeekDev
a38d198b70 Use background coroutines for modification operations. Use FollowSet in callbacks, rather than just the set identifier. 2025-06-07 02:42:27 +01:00
KotlinGeekDev
af2b2328cf Generalise the implementation of PeopleListEvent.removeTag() to also support public list events. Use defaults in already dependent code. 2025-06-06 18:34:45 +01:00
KotlinGeekDev
94243f4060 Make adjustments to UI code following the ViewModel method signatures. 2025-06-04 21:41:53 +01:00
KotlinGeekDev
63177b15a1 Make adjustments to UI code following the ViewModel method signatures. 2025-05-30 20:26:42 +01:00
KotlinGeekDev
69ae42d344 Implement relevant methods in NostrListFeedViewModel. Remove unnecessary subclass and bring ViewModelProvider in the actual VM itself. 2025-05-30 20:21:45 +01:00
KotlinGeekDev
0b3380ac6e Use dTag instead of the note's address. 2025-05-30 19:03:21 +01:00
KotlinGeekDev
af0061ae09 Add new methods for list management to PeopleListEvent. 2025-05-30 13:57:23 +01:00
KotlinGeekDev
44b567b01f Write down implementation for FollowSetScreen. Introduce functions(hacks, really) for making sure of the UI/UX design.
- Implement dialog for creating a new list.
2025-05-12 20:47:12 +01:00
KotlinGeekDev
5a6cfad848 Separate components out into their logical groups(by file). Add new icons for public/private list creation, and add FloatingActionButtons for each. Add test code for iterating on changes quickly(TEMPORARY). 2025-04-30 17:11:28 +01:00
KotlinGeekDev
0693e4412f Re-arrange components so as to better fit the ListOptionsButton. 2025-04-25 14:20:24 +01:00
KotlinGeekDev
b52943a826 Add stub for GeneralBookmarks tab. Use a different icon for representing private lists. Add dropdown menu for follow set component(for actions such as delete, rename, etc). 2025-04-25 13:42:39 +01:00
KotlinGeekDev
3a5e6dbe90 Use a tabbed layout for the different types of lists/sets, and migrate follow sets. Use empty info for next tab(labeled bookmarks). 2025-04-21 18:27:25 +01:00
KotlinGeekDev
2aa2605b48 Migrate Lists route to new navigation setup. 2025-04-18 14:24:51 +01:00
KotlinGeekDev
f97c97e00e Merge modified upstream changes. 2025-04-15 11:43:10 +01:00
KotlinGeekDev
dc22c7ea76 Keep local changes for later refactoring. Apply styling fixes. 2025-04-15 11:42:41 +01:00
KotlinGeekDev
acc59d2b40 Manual merge, due to other unfinished work. 2025-04-15 11:39:25 +01:00
KotlinGeekDev
4d45f86e06 Add CuratedBookmarkList type. Introduce FollowSetScreen. 2025-04-15 11:17:31 +01:00
KotlinGeekDev
0dae538c31 Use better naming for the view models, reflecting their true nature, and preparing for future work here. 2025-04-14 17:45:44 +01:00
KotlinGeekDev
7d90b9f87b Remove map in FollowSetFeedFilter, in favor of retrieving note from cache if modification is needed. Amend FollowSet to carry the address of the note. Add Refreshable for the Lists screen. 2025-03-31 21:31:50 +01:00
KotlinGeekDev
28093256e7 Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2025-03-31 21:19:28 +01:00
KotlinGeekDev
d7d8554626 Make naming clearer. Some refactoring. Add a function to retrieve the not corresponding to a follow set. 2025-03-31 21:19:05 +01:00
KotlinGeekDev
bfa673ca2c Make naming clearer. Some refactoring. Add a function to retrieve the not corresponding to a follow set. 2025-03-31 21:15:06 +01:00
KotlinGeekDev
82f8fe96a1 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-03-31 17:50:33 +00:00
KotlinGeekDev
c57153221c Merge branch 'vitorpamplona:main' into profiles-list-management 2025-03-30 23:05:31 +00:00
KotlinGeekDev
ab32518b8e Merge branch 'vitorpamplona:main' into profiles-list-management 2025-03-28 23:02:13 +00:00
KotlinGeekDev
24a9d7659a Merge branch 'upstream-main' into profiles-list-management
Merge upstream changes.
2025-03-28 12:12:25 +01:00
KotlinGeekDev
85fb56bb22 Updates to conflict-causing files. 2025-03-28 12:10:58 +01:00
KotlinGeekDev
c1f14cea2e Use a general type to represent lists, NostrList. Make FollowSet a subclass. Rename ListType to ListVisibility. Refactor accordingly. 2025-03-27 18:47:57 +01:00
KotlinGeekDev
f2a06184b4 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-03-25 10:53:49 +00:00
KotlinGeekDev
2ae9d272f4 Use Set rather than List, to avoid duplication. Introduce follow sets flow, and use it in FollowSetsActionMenu. 2025-03-22 21:02:10 +01:00
KotlinGeekDev
1d90e37f71 Fix mixup in follow set categorization logic. 2025-03-22 19:55:53 +01:00
KotlinGeekDev
c802c395a6 Unify the underlying implementation for follow sets and use it in both profile actions and CustomListsScreen. 2025-03-21 13:04:16 +01:00
KotlinGeekDev
fa7425e114 Remove unnecessary delay call. 2025-03-20 17:30:01 +01:00
KotlinGeekDev
5af094ab9b Re-introduce the follow sets action menu(with test data) in ProfileActions. 2025-03-20 15:56:11 +01:00
KotlinGeekDev
74ccbb3d07 Re-introduce the follow sets action menu(in a separate file, this time). 2025-03-20 15:47:23 +01:00
KotlinGeekDev
d9c3badf8a Re-introduce button styling changes(border shape). 2025-03-20 15:11:33 +01:00
KotlinGeekDev
dde59d4741 Switch to nameOrTitle() for follow set titles. 2025-03-18 13:49:35 +01:00
KotlinGeekDev
3e0f8a81b4 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-03-18 12:05:14 +00:00
KotlinGeekDev
c9dbe7d203 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-03-14 23:18:09 +00:00
KotlinGeekDev
aae0e1bb4b Merge branch 'vitorpamplona:main' into profiles-list-management 2025-03-11 23:59:51 +00:00
KotlinGeekDev
4855126d6d Fix some imports and remove unneeded functions(as per the upstream code). 2025-03-11 13:56:16 +01:00
KotlinGeekDev
3b140b16eb Merge pull request #4 from vitorpamplona/main
Merge upstream changes.
2025-03-11 11:47:57 +00:00
KotlinGeekDev
9f23bd034c Merge changes from upstream. 2025-03-08 00:52:38 +01:00
KotlinGeekDev
33d690c403 Updates to conflict-causing files. 2025-03-08 00:52:10 +01:00
KotlinGeekDev
1dcbcc04d2 Fix merge-related conflicts. 2025-03-08 00:49:18 +01:00
KotlinGeekDev
0fe6fd40be Add component for displaying the number of people in the follow list. Make some minor refactoring. 2025-02-06 14:12:09 +01:00
KotlinGeekDev
e25dce32e4 Refactor to adjust to previous change. Make other little changes. 2025-02-05 15:42:46 +01:00
KotlinGeekDev
606f630cb6 Use data classes/objects for state. Only update follow sets through exposed functions. 2025-02-05 15:31:48 +01:00
KotlinGeekDev
c2857664d3 Just use simple List. Remove explicit dispatcher calls in unneeded places. 2025-01-30 14:50:05 +01:00
KotlinGeekDev
a300a4b25c Remove the tick as list indicator in favour of textual indication. Make add/remove button look slightly better. 2025-01-30 13:52:07 +01:00
KotlinGeekDev
fad545a094 Introduce user removal functionality. 2025-01-30 10:00:25 +01:00
KotlinGeekDev
a066285667 Make some visual changes to the add/remove user button, and move the add callback to the right place. 2025-01-29 23:45:52 +01:00
KotlinGeekDev
d13bf43de0 Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2025-01-28 11:42:40 +01:00
KotlinGeekDev
7325644689 Fix some code after upstream changes. 2025-01-28 11:42:05 +01:00
KotlinGeekDev
4b7b5f1276 Merge branch 'upstream' into profiles-list-management
Sync with upstream
2025-01-28 11:20:12 +01:00
KotlinGeekDev
467b8d4184 Merge upstream, cont'd. 2025-01-28 10:18:03 +01:00
KotlinGeekDev
410888b271 Merge branch 'vitorpamplona:main' into profiles-list-management 2025-01-04 21:34:56 +00:00
KotlinGeekDev
920241abab Merge branch 'vitorpamplona:main' into profiles-list-management 2025-01-03 14:38:02 +00:00
KotlinGeekDev
8ca827a816 Merge branch 'vitorpamplona-main' into profiles-list-management
Merge upstream changes.
2025-01-01 13:39:19 +01:00
KotlinGeekDev
b2dd2a2adb Merge upstream with local changes. 2025-01-01 13:29:00 +01:00
KotlinGeekDev
7166439b97 Arrange the placement of list status indicators. 2025-01-01 13:06:56 +01:00
KotlinGeekDev
966936bba3 Use FilterChip instead of Custom Text component for presence indication. Add some test code(to be removed later). 2024-11-18 19:57:23 +01:00
KotlinGeekDev
732ce790c2 Introduce FollowSetItem, with funny defaults for now. 2024-11-07 20:20:48 +01:00
KotlinGeekDev
276b6cbf46 Use a different icon for public lists. 2024-11-07 19:21:02 +01:00
KotlinGeekDev
c485f75206 Make custom button border naming clearer, and fix list dropdown button border. 2024-11-07 12:27:00 +01:00
KotlinGeekDev
883f7a975f Experiment with Icon as a button. Refactor FollowButton and UnfollowButton to take into account the modified design, without affecting other components. 2024-11-06 10:56:33 +01:00
KotlinGeekDev
aa64e42bfe Introduce FollowSetsActionMenu. 2024-11-05 21:17:08 +01:00
KotlinGeekDev
aed914dfcf Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-11-01 22:28:20 +01:00
KotlinGeekDev
d763f43b40 Restore function for getting follow sets(due to merge of upstream). 2024-11-01 22:27:55 +01:00
KotlinGeekDev
2fcd6af24b Merge branch 'vitorpamplona:main' into profiles-list-management 2024-11-01 15:42:11 +00:00
KotlinGeekDev
4b76f56293 Merge branch 'vitorpamplona:main' into profiles-list-management 2024-10-31 22:15:32 +00:00
KotlinGeekDev
3ad26f0039 Merge branch 'vitorpamplona-main' into profiles-list-management
Merging upstream changes/resolving conflicts.
2024-10-31 19:42:05 +01:00
KotlinGeekDev
2366f26ac7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst into vitorpamplona-main
merging upstream changes.
2024-10-31 19:36:20 +01:00
KotlinGeekDev
f2640deeaf Minor refactor. 2024-10-31 19:27:15 +01:00
KotlinGeekDev
8f798d1003 Add some error handling in the feed filter. 2024-10-31 19:02:38 +01:00
KotlinGeekDev
a39834dc87 Use FollowSetState in in CustomListsScreen. Make other fixes. 2024-10-31 18:43:07 +01:00
KotlinGeekDev
00b7ace00b Change followSets signature again. Refactor followSetNotesFLow(). 2024-10-31 18:39:05 +01:00
KotlinGeekDev
ee8148ae50 Just use NavigationRow instead of IconRow. 2024-10-31 16:58:36 +01:00
KotlinGeekDev
63654b742a Refactor NostrUserFollowSetFeedViewModel to use the FollowSetViewModel. Change follow sets signature. Might be changed later. 2024-10-31 16:55:13 +01:00
KotlinGeekDev
f3edc2d43b Introduce FollowSetFeedViewModel and FollowSetState. 2024-10-31 16:48:45 +01:00
KotlinGeekDev
0e9cc54245 Refactor FollowSetFeedFilter and Account. 2024-10-31 16:45:12 +01:00
KotlinGeekDev
3d4483ed48 Move FollowSet to it's own class. 2024-10-31 16:37:05 +01:00
KotlinGeekDev
ddbaca383f Navigation: Add custom lists to main app navigation control helper. 2024-10-30 15:01:27 +01:00
KotlinGeekDev
5b51e97594 Replace TODOs with provided feed state implementations. 2024-10-30 14:57:27 +01:00
KotlinGeekDev
d348e7a4ad Introduce ListsScreen. Current implementation is focused on follows. 2024-10-30 14:51:46 +01:00
KotlinGeekDev
0b014c5c7c Move mapping function from FollowSetFeedFilter to FollowSet. 2024-10-30 14:48:58 +01:00
KotlinGeekDev
bc7d9eef9b Make FollowSet stable. The class may be moved to another location. 2024-10-30 13:31:37 +01:00
KotlinGeekDev
7dec46f3fd Create Viewmodel for Follow sets. Refactor User.followSets to be a collection of Notes. Adjust FollowSetFeedFilter. 2024-10-27 20:19:23 +01:00
KotlinGeekDev
caad5066ff Refactor followSetNotesFlow() and introduce FollowSetFeedFilter. 2024-10-27 17:48:52 +01:00
KotlinGeekDev
c86ce1b1ce Load and populate follow sets in Account(since Account is initialized in AccountViewModel anyway). 2024-10-27 00:20:35 +01:00
KotlinGeekDev
aa1175ce1d Implement method for retrieving follow sets. 2024-10-26 18:00:09 +01:00
KotlinGeekDev
afca104b87 Add FollowSet class, and property in User. 2024-10-26 13:15:43 +01:00
KotlinGeekDev
c79157f415 Add description() for list events. 2024-10-24 18:12:50 +01:00
KotlinGeekDev
5a38267fc1 Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-23 15:29:35 +01:00
KotlinGeekDev
0c9bea3c19 Add function to get all follow sets from cache. 2024-10-23 15:28:50 +01:00
KotlinGeekDev
27d9b6384e Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-20 10:42:58 +01:00
KotlinGeekDev
bcd220191e Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-18 09:10:46 +01:00
KotlinGeekDev
1076e00aa0 Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-17 19:39:23 +01:00
KotlinGeekDev
9bca9187a6 Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-17 09:39:33 +01:00
KotlinGeekDev
bbeec18e0e Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-16 16:28:01 +01:00
KotlinGeekDev
3d11845396 Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-16 15:50:14 +01:00
KotlinGeekDev
34a6be67cf Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-09 13:54:29 +01:00
KotlinGeekDev
3c22129c4f Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management 2024-10-05 13:50:00 +01:00
KotlinGeekDev
c5be20b4d2 Add icon for Lists and create Drawer/Route entry. 2024-10-01 20:18:11 +01:00
1737 changed files with 48991 additions and 14283 deletions

View File

@@ -8,7 +8,8 @@ trim_trailing_whitespace = true
max_line_length = 5000
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test
ktlint_function_naming_ignore_when_annotated_with=Composable
ktlint_standard_no-unused-imports=enabled
[*.gradle]
indent_size = 4

View File

@@ -61,13 +61,13 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: Play Benchmark APK
path: amethyst/build/outputs/apk/play/debug/amethyst-play-universal-benchmark.apk
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
- name: Upload FDroid APK Benchmark
uses: actions/upload-artifact@v4
with:
name: FDroid Benchmark APK
path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-benchmark.apk
path: amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk
- name: Upload Compose Reports
uses: actions/upload-artifact@v4
@@ -75,9 +75,6 @@ jobs:
name: Compose Reports
path: amethyst/build/compose_compiler
- name: Tests
run: ./gradlew test --no-daemon
- name: Upload Test Results
uses: actions/upload-artifact@v4
with:

View File

@@ -228,3 +228,11 @@ jobs:
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
asset_content_type: application/zip
- name: Publish Quartz Lib
run: ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}

2
.gitignore vendored
View File

@@ -17,6 +17,8 @@
/.idea/ChatHistory_schema_v2.xml
/.idea/artifacts/*
/.idea/kotlinNotebook.xml
/.idea/ChatHistory_schema_v3.xml
/.idea/markdown.xml
.DS_Store
/build
/captures

6655
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -136,7 +136,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
- [ ] Key Migration (NIP-xx/Draft)
- [ ] Time-based Sync (NIP-xx/Draft)
- [x] Image Capture in the app
- [ ] Video Capture in the app
- [x] Video Capture in the app
- [ ] Local Database
- [ ] Workspaces
- [ ] Infinity Scroll
@@ -237,47 +237,46 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
## Using the Quartz library
Setup [JitPack.io](https://jitpack.io/#vitorpamplona/amethyst/v0.84.3) to your build file
### Installing
Add `maven { url 'https://jitpack.io' }` to settings.gradle at the end of repositories:
Add the following line to your `commonMain` dependencies:
```gradle
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
implementation('com.vitorpamplona.quartz:quartz:<Amethyst Version>')
```
Add the dependency by using one of the versions [here](https://jitpack.io/#vitorpamplona/amethyst/)
Variations to each platform are also available:
```gradle
implementation('com.github.vitorpamplona.amethyst:quartz:<tag, commit, -SNAPSHOT>')
implementation('com.vitorpamplona.quartz:quartz-android:<Amethyst Version>')
implementation('com.vitorpamplona.quartz:quartz-jvm:<Amethyst Version>')
implementation('com.vitorpamplona.quartz:quartz-iosarm64:<Amethyst Version>')
implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:<Amethyst Version>')
```
### How to use
Manage logged in users with the `KeyPair` class
```kt
val keys = KeyPair() // creates a random key
val keys = KeyPair("hex...".hexToByteArray())
val keys = KeyPair("nsec1...".bechToBytes())
val keys = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
val keyPair = KeyPair() // creates a random key
val keyPair = KeyPair("hex...".hexToByteArray())
val keyPair = KeyPair("nsec1...".bechToBytes())
val keyPair = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
val readOnly = KeyPair(pubKey = "hex...".hexToByteArray())
val readOnly = KeyPair(pubKey = "npub1...".bechToBytes())
```
Create signers that can be internal, when you have the private key or when it is a read-only user
or external, when it is controlled by Amber in NIP-55
Create signers that can be Internal, when you have the private key or a read-only public key,
or External, when it is controlled by Amber in NIP-55.
the `NostrSignerInternal` and `NostrSignerExternal` classes.
Use either the `NostrSignerInternal` or `NostrSignerExternal` class:
```kt
val signer = NostrSignerInternal(keyPair)
val amberSigner = NostrSignerExternal(
pubKey = keyPair.pubKey.toHexKey(),
packageName = signerPackageName,
packageName = signerPackageName, // Amber package name
contentResolver = appContext.contentResolver,
)
```
@@ -300,37 +299,63 @@ val client = NostrClient(socketBuilder, appScope)
If you want to auth, given a logged-in `signer`:
```kt
val authCoordinator = RelayAuthenticator(client, applicationIOScope) { challenge, relay ->
val authedEvent = RelayAuthEvent.create(relayUrl, challenge, signer)
val authCoordinator = RelayAuthenticator(client, appScope) { challenge, relay ->
val authedEvent = RelayAuthEvent.create(relay.url, challenge, signer)
client.sendIfExists(authedEvent, relay.url)
}
```
To manage subscriptions, the suggested approach is to use subscriptions in the Application class.
To make a request subscription simply do:
```kt
val metadataSub = RelayClientSubscription(
client = client,
filter = {
val metadataSub = client.req(
relays = listOf("wss://nos.lol", "wss://nostr.mom"),
filters = Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(signer.pubkey)
)
) { event ->
/* consume event */
}
```
The client will add the relay to the pool, connect to it and start receiving events. The
`metadataSub` will be active until you call `metadataSub.close()`. If the client disconnects
and reconnects, the sub will be active again.
To manage subscriptions that change over time, the simplest approach is to build mutable
subscriptions with a filters lambda that you can change at will.
```kt
val metadataSub = client.req(
filters = {
// Let's say you have a list of users that need to be rendered
val users = pubkeysSeeingInTheScreen()
// And a cache repository with their outbox relays
val outboxRelays = outboxRelays(users)
val filters = listOf(
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(signer.pubkey)
authors = users
)
)
val signerOutboxRelays = listOfNotNull(
RelayUrlNormalizer.normalizeOrNull("wss://relay1.com"),
RelayUrlNormalizer.normalizeOrNull("wss://relay2.com")
)
signerOutboxRelays.associateWith { filters }
outboxRelays.associateWith { filters }
}
) { event ->
/* consume event */
}
```
In that way, you can simply call `metadataSub.updateFilter()` when you need to update
subscriptions to all relays. Or call `metadataSub.close()` to stop the sub
without deleting it.
When your app goes to the background, you can use NostrClient's `connect` and `disconnect`
methods to stop all communication to relays. Add the `connect` to your `onResume` and `disconnect`
to `onPause` methods.
## Contributing
Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst)

View File

@@ -44,9 +44,9 @@ android {
applicationId = "com.vitorpamplona.amethyst"
minSdk = libs.versions.android.minSdk.get().toInteger()
targetSdk = libs.versions.android.targetSdk.get().toInteger()
versionCode = 426
versionName = generateVersionName("1.02.0")
buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\""
versionCode = 429
versionName = generateVersionName("1.04.0")
buildConfigField "String", "RELEASE_NOTES_ID", "\"3a03c75d85aaf6b181d3b232d064c4d4feea5c73f0bea2bd91ed61b8da7cd6a6\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -152,6 +152,15 @@ android {
}
}
// TODO: remove this when lightcompressor uses one MP4 parser only
packaging {
resources {
resources.pickFirsts.add('builddef.lst')
resources.pickFirsts.add('META-INF/LICENSE.md')
resources.pickFirsts.add('META-INF/LICENSE-notice.md')
}
}
flavorDimensions = ["channel"]
productFlavors {
@@ -304,6 +313,8 @@ dependencies {
// Language picker and Theme chooser
implementation libs.androidx.appcompat
// Dynamically adjust between phone and tablet UI
implementation libs.androidx.window.core.android
// Local model for language identification
@@ -332,13 +343,16 @@ dependencies {
implementation libs.audiowaveform
// Video compression lib
implementation libs.abedElazizShe.image.compressor
implementation libs.abedElazizShe.video.compressor.fork
// Image compression lib
implementation libs.zelory.video.compressor
implementation libs.zelory.image.compressor
// Cbor for cashuB format
implementation libs.kotlinx.serialization.cbor
// Kotlin serialization for the times where we need the Json tree and performance is not that important.
implementation(libs.kotlinx.serialization.json)
implementation libs.tor.android
implementation libs.jtorctl
@@ -350,6 +364,8 @@ dependencies {
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.junit.ktx
androidTestImplementation libs.androidx.espresso.core
androidTestImplementation libs.androidx.ui.test.junit4
androidTestImplementation libs.mockk.android
debugImplementation platform(libs.androidx.compose.bom)
debugImplementation libs.androidx.ui.tooling

View File

@@ -24,8 +24,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.cashu.CashuParser
import com.vitorpamplona.amethyst.service.cashu.CashuToken
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import junit.framework.TestCase.assertEquals
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -38,7 +39,7 @@ class CashuBTest {
@Test()
fun parseCashuA() {
runBlocking {
val parsed = (CashuParser().parse(cashuTokenA) as GenericLoadable.Loaded<List<CashuToken>>).loaded[0]
val parsed = (CashuParser().parse(cashuTokenA) as GenericLoadable.Loaded<ImmutableList<CashuToken>>).loaded[0]
assertEquals(cashuTokenA, parsed.token)
assertEquals("https://8333.space:3338", parsed.mint)
@@ -59,7 +60,7 @@ class CashuBTest {
@Test()
fun parseCashuB() =
runBlocking {
val parsed = (CashuParser().parse(cashuTokenB1) as GenericLoadable.Loaded<List<CashuToken>>).loaded
val parsed = (CashuParser().parse(cashuTokenB1) as GenericLoadable.Loaded<ImmutableList<CashuToken>>).loaded
assertEquals(cashuTokenB1, parsed[0].token)
assertEquals("http://localhost:3338", parsed[0].mint)
@@ -84,7 +85,7 @@ class CashuBTest {
@Test()
fun parseCashuB2() =
runBlocking {
val parsed = (CashuParser().parse(cashuTokenB2) as GenericLoadable.Loaded<List<CashuToken>>).loaded
val parsed = (CashuParser().parse(cashuTokenB2) as GenericLoadable.Loaded<ImmutableList<CashuToken>>).loaded
assertEquals(cashuTokenB2, parsed[0].token)
assertEquals("http://lbutlh5lfggq5r7xpiwhrajdl7sxpupgagazxl65w4c5cg72wtofasad.onion:3338", parsed[0].mint)

View File

@@ -24,10 +24,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.okhttp.EncryptedBlobInterceptor
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
import junit.framework.TestCase.assertEquals
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
import okhttp3.OkHttpClient
import okhttp3.Request
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

View File

@@ -41,7 +41,6 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
import com.vitorpamplona.quartz.utils.sha256.sha256
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.fail
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -49,9 +48,9 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.Request.method
import org.junit.runner.RunWith
import java.io.ByteArrayOutputStream
import kotlin.random.Random
@@ -248,13 +247,6 @@ class ImageUploadTesting {
testBase(ServerName("sovbit", "https://files.sovbit.host", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testVoidCat() =
runBlocking {
testBase(ServerName("void.cat", "https://void.cat", ServerType.NIP96))
}
@Test()
@Ignore("Returns invalid image size")
fun testNostrPic() =

View File

@@ -21,17 +21,15 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpBitcoinExplorer
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpCalendarBuilder
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache
import junit.framework.TestCase.assertEquals
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpOtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -45,51 +43,50 @@ class OkHttpOtsTest {
val otsCache = OtsBlockHeightCache()
val resolver =
OtsResolver(
OkHttpBitcoinExplorer(
baseAPI = OkHttpBitcoinExplorer.MEMPOOL_API_URL,
client = OkHttpClient.Builder().build(),
cache = otsCache,
),
OkHttpCalendarBuilder { OkHttpClient.Builder().build() },
)
OkHttpOtsResolverBuilder(
otsCache,
) { OkHttpClient.Builder().build() }.build()
@Test
fun verifyNostrEvent() {
val ots = JsonMapper.fromJson(otsEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1707688818L, ots.verify(resolver))
}
fun verifyNostrEvent() =
runBlocking {
val ots = OptimizedJsonMapper.fromJson(otsEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1707688818L, ots.verify(resolver))
}
@Test
fun verifyNostrEvent2() {
val ots = JsonMapper.fromJson(otsEvent2) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1706322179L, ots.verify(resolver))
}
fun verifyNostrEvent2() =
runBlocking {
val ots = OptimizedJsonMapper.fromJson(otsEvent2) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1706322179L, ots.verify(resolver))
}
@Test
fun verifyNostrPendingEvent() {
val ots = JsonMapper.fromJson(otsPendingEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(null, ots.verify(resolver))
}
fun verifyNostrPendingEvent() =
runBlocking {
val ots = OptimizedJsonMapper.fromJson(otsPendingEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(null, ots.verify(resolver))
}
@Test
fun createOTSEventAndVerify() {
val signer = NostrSignerInternal(KeyPair())
fun createOTSEventAndVerify() =
runBlocking {
val signer = NostrSignerInternal(KeyPair())
val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver)
val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver)
val ots =
runBlocking {
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile))
}
val ots =
runBlocking {
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile))
}
println(ots.toJson())
println(resolver.info(ots.otsByteArray()))
println(ots.toJson())
println(resolver.info(ots.otsByteArray()))
// Should not be valid because we need to wait for confirmations
assertEquals(null, ots.verify(resolver))
}
// Should not be valid because we need to wait for confirmations
assertEquals(null, ots.verify(resolver))
}
}

View File

@@ -27,22 +27,24 @@ import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip01Core.crypto.verify
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.verify
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder
import junit.framework.TestCase
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -50,24 +52,30 @@ import org.junit.runner.RunWith
class ThreadDualAxisChartAssemblerTest {
companion object {
val keyPair = KeyPair()
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val client =
NostrClient(
OkHttpWebSocket.Builder {
OkHttpClient
.Builder()
.followRedirects(true)
.followSslRedirects(true)
.build()
},
scope,
)
val account =
Account(
settings = AccountSettings(keyPair = keyPair),
signer = NostrSignerInternal(keyPair),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
nwcFilterAssembler = NWCPaymentFilterAssembler(client),
otsResolverBuilder = EmptyOtsResolverBuilder,
cache = LocalCache,
client =
NostrClient(
OkHttpWebSocket.Builder {
OkHttpClient
.Builder()
.followRedirects(true)
.followSslRedirects(true)
.build()
},
CoroutineScope(Dispatchers.IO + SupervisorJob()),
),
client = client,
scope = scope,
)
val db =
@@ -149,7 +157,7 @@ class ThreadDualAxisChartAssemblerTest {
fun threadOrderTest() =
runBlocking {
val eventArray =
JsonMapper.mapper.readValue<ArrayList<Event>>(db) as List<Event> + Event.fromJson(header)
JacksonMapper.mapper.readValue<List<Event>>(db) + Event.fromJson(header)
var counter = 0
eventArray.forEach {
@@ -169,7 +177,7 @@ class ThreadDualAxisChartAssemblerTest {
val filter = ThreadFeedFilter(account, naddr.toTag())
val calculatedFeed = filter.feed()
val expecteedOrder =
val expectedOrder =
listOf(
"30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599",
"e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e",
@@ -211,8 +219,8 @@ class ThreadDualAxisChartAssemblerTest {
"7a18dda355525d468b31bba4fa947cba98cc19048d4a3099d5e9ba045d878c26",
)
for (i in expecteedOrder.indices) {
assertEquals(expecteedOrder[i], calculatedFeed[i].idHex)
for (i in expectedOrder.indices) {
assertEquals(expectedOrder[i], calculatedFeed[i].idHex)
}
}
}

View File

@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst
import com.vitorpamplona.amethyst.ui.navigation.findParameterValue
import junit.framework.TestCase.assertEquals
import org.junit.Assert.assertEquals
import org.junit.Test
import java.net.URI

View File

@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.notifications
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.quartz.utils.Log
import org.unifiedpush.android.connector.UnifiedPush
interface PushDistributorActions {
@@ -53,7 +53,7 @@ object PushDistributorHandler : PushDistributorActions {
endpointInternal = ""
}
fun appContext(): Context = Amethyst.instance.applicationContext
fun appContext(): Context = Amethyst.instance.appContext
override fun getSavedDistributor(): String = unifiedPush.getSavedDistributor(appContext()) ?: ""

View File

@@ -22,15 +22,13 @@ package com.vitorpamplona.amethyst.service.notifications
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import android.util.LruCache
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -44,7 +42,7 @@ class PushMessageReceiver : MessagingReceiver() {
private val TAG = "Amethyst-OSSPushReceiver"
}
private val appContext = Amethyst.instance.applicationContext
private val appContext = Amethyst.instance.appContext
private val scope = Amethyst.instance.applicationIOScope
private val eventCache = LruCache<String, String>(100)
private val pushHandler = PushDistributorHandler
@@ -95,8 +93,8 @@ class PushMessageReceiver : MessagingReceiver() {
PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) {
Amethyst.instance.okHttpClients.getHttpClient(Amethyst.instance.torManager.isSocksReady())
}
notificationManager().getOrCreateZapChannel(appContext)
notificationManager().getOrCreateDMChannel(appContext)
NotificationUtils.getOrCreateZapChannel(appContext)
NotificationUtils.getOrCreateDMChannel(appContext)
}
} else {
Log.d(TAG, "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint")

View File

@@ -20,7 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.components
import android.util.Log
import android.Manifest
import android.os.Build
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -36,6 +37,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -44,33 +46,52 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.MarkdownParseOptions
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.UiSettingsFlow
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.checkifItNeedsToRequestNotificationPermission
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) {
val notificationPermissionState =
checkifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel)
fun SelectNotificationProvider(sharedPrefs: UiSettingsFlow) {
val notificationPermissionGranted =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
if (notificationPermissionState.status.isGranted) {
if (!sharedPreferencesViewModel.sharedPrefs.dontShowPushNotificationSelector) {
if (!notificationPermissionState.status.isGranted) {
val dontAskForNotificationPermissions by sharedPrefs.dontAskForNotificationPermissions.collectAsStateWithLifecycle()
if (!dontAskForNotificationPermissions) {
sharedPrefs.dontAskForNotificationPermissions()
// This will pause the APP, including the connection with relays.
LaunchedEffect(notificationPermissionState) {
notificationPermissionState.launchPermissionRequest()
}
}
}
notificationPermissionState.status.isGranted
} else {
true
}
if (notificationPermissionGranted) {
val pushNotificationSelector by sharedPrefs.dontShowPushNotificationSelector.collectAsStateWithLifecycle()
if (!pushNotificationSelector) {
val context = LocalContext.current
var distributorPresent by remember {
mutableStateOf(PushDistributorHandler.savedDistributorExists())
@@ -84,8 +105,8 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
onSelect = { index ->
if (list[index] == "None") {
PushDistributorHandler.forceRemoveDistributor(context)
sharedPreferencesViewModel.dontAskForNotificationPermissions()
sharedPreferencesViewModel.dontShowPushNotificationSelector()
sharedPrefs.dontAskForNotificationPermissions()
sharedPrefs.dontShowPushNotificationSelector()
} else {
val fullDistributorName = list[index]
PushDistributorHandler.saveDistributor(fullDistributorName)
@@ -107,7 +128,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
val astNode =
remember {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content)
}
RichText(
@@ -125,7 +146,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
TextButton(
onClick = {
distributorPresent = true
sharedPreferencesViewModel.dontShowPushNotificationSelector()
sharedPrefs.dontShowPushNotificationSelector()
},
) {
Text(stringRes(R.string.quick_action_dont_show_again_button))
@@ -187,19 +208,19 @@ fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, Immuta
}
@Composable
fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) {
fun PushNotificationSettingsRow(sharedPrefs: UiSettingsFlow) {
val context = LocalContext.current
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
SettingsRow(
R.string.push_server_title,
R.string.push_server_explainer,
selectedItens = readableListWithExplainer,
selectedItems = readableListWithExplainer,
selectedIndex = list.indexOf(currentDistributor),
) { index ->
if (list[index] == "None") {
sharedPreferencesViewModel.dontAskForNotificationPermissions()
sharedPreferencesViewModel.dontShowPushNotificationSelector()
sharedPrefs.dontAskForNotificationPermissions()
sharedPrefs.dontShowPushNotificationSelector()
PushDistributorHandler.forceRemoveDistributor(context)
} else {
PushDistributorHandler.saveDistributor(list[index])

View File

@@ -28,7 +28,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
@Composable
fun TranslatableRichTextViewer(
@@ -59,6 +60,7 @@ fun TranslatableRichTextViewer(
fun TranslatableRichTextViewer(
content: String,
id: String,
translationMessageModifier: Modifier = MaxWidthPaddingTop5dp,
accountViewModel: AccountViewModel,
displayText: @Composable (String) -> Unit,
) = displayText(content)

View File

@@ -8,8 +8,12 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrsigner" />
</intent>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
<!-- Doesn't require a camera -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
@@ -85,6 +89,7 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostr" />
<data android:scheme="amethyst" />
</intent-filter>
<intent-filter android:label="Amethyst">
@@ -99,6 +104,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrwalletconnect" />
<data android:scheme="nostr+walletconnect" />
<data android:scheme="amethyst+walletconnect" />
</intent-filter>
<intent-filter android:label="New Post">

View File

@@ -21,198 +21,37 @@
package com.vitorpamplona.amethyst
import android.app.Application
import android.content.ContentResolver
import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache
import com.vitorpamplona.amethyst.service.crashreports.UnexpectedCrashSaver
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
import com.vitorpamplona.amethyst.service.images.ImageCacheFactory
import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.logging.Logging
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.File
import com.vitorpamplona.quartz.utils.Log
class Amethyst : Application() {
val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}"
init {
Log.d("AmethystApp", "Creating App $this")
}
// Exists to avoid exceptions stopping the coroutine
val exceptionHandler =
CoroutineExceptionHandler { _, throwable ->
Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable)
}
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
// Key cache service to download and decrypt encrypted files before caching them.
val keyCache = EncryptionKeyCache()
// App services that should be run as soon as there are subscribers to their flows
val locationManager = LocationState(this, applicationIOScope)
val connManager = ConnectivityManager(this, applicationIOScope)
val torManager = TorManager(this, applicationIOScope)
// Service that will run at all times to receive events from Pokey
val pokeyReceiver = PokeyReceiver()
// manages all the other connections separately from relays.
val okHttpClients =
DualHttpClientManager(
userAgent = appAgent,
proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull,
keyCache = keyCache,
scope = applicationIOScope,
)
// manages all relay connections
val okHttpClientForRelays =
DualHttpClientManager(
userAgent = appAgent,
proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull,
keyCache = keyCache,
scope = applicationIOScope,
)
// manages all relay connections
val okHttpClientForRelaysForDms =
DualHttpClientManager(
userAgent = appAgent,
proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull,
keyCache = keyCache,
scope = applicationIOScope,
)
val torProxySettingsAnchor = ProxySettingsAnchor()
// Connects the NostrClient class with okHttp
val websocketBuilder =
OkHttpWebSocket.Builder { url ->
val useTor = torProxySettingsAnchor.useProxy(url)
if (torProxySettingsAnchor.isDM(url)) {
okHttpClientForRelaysForDms.getHttpClient(useTor)
} else {
okHttpClientForRelays.getHttpClient(useTor)
}
}
// Caches all events in Memory
val cache: LocalCache = LocalCache
// Organizes cache clearing
val trimmingService = MemoryTrimmingService(cache)
// Provides a relay pool
val client: NostrClient = NostrClient(websocketBuilder, applicationIOScope)
// Watches for changes on Tor and Relay List Settings
val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, torManager, applicationIOScope)
// Verifies and inserts in the cache from all relays, all subscriptions
val cacheClientConnector = CacheClientConnector(client, cache)
// Show messages from the Relay and controls their dismissal
val notifyCoordinator = NotifyCoordinator(client)
// Authenticates with relays.
val authCoordinator = AuthCoordinator(client, applicationIOScope)
val logger = if (isDebug) RelaySpeedLogger(client) else null
// Coordinates all subscriptions for the Nostr Client
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
// saves the .content of NIP-95 blobs in disk to save memory
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
// local video cache with disk + memory
val videoCache: VideoCache by lazy { VideoCacheFactory.new(this) }
// image cache in disk for coil
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(this) }
// image cache in memory for coil
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(this) }
val crashReportCache: CrashReportCache by lazy { CrashReportCache(this) }
// Application-wide ots verification cache
val otsVerifCache by lazy { VerificationStateCache() }
// Application-wide block height request cache
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
companion object {
lateinit var instance: AppModules
private set
}
override fun onCreate() {
super.onCreate()
Log.d("AmethystApp", "onCreate $this")
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
instance = this
instance = AppModules(this)
if (isDebug) {
Logging.setup()
}
// initializes diskcache on an IO thread.
applicationIOScope.launch {
diskCache
videoCache
}
// registers to receive events
pokeyReceiver.register(this)
instance.initiate(this)
}
override fun onTerminate() {
super.onTerminate()
Log.d("AmethystApp", "onTerminate $this")
pokeyReceiver.unregister(this)
applicationIOScope.cancel("Application onTerminate $this")
instance.terminate(this)
}
fun contentResolverFn(): ContentResolver = contentResolver
fun setImageLoader(shouldUseTor: (String) -> Boolean?) {
ImageLoaderSetup.setup(this, diskCache, memoryCache) { url ->
shouldUseTor(url)?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false)
}
}
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
/**
* Release memory when the UI becomes hidden or when system resources become low.
*
@@ -221,13 +60,6 @@ class Amethyst : Application() {
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Log.d("AmethystApp", "onTrimMemory $level")
applicationIOScope.launch(Dispatchers.Default) {
trimmingService.run(null, LocalPreferences.allSavedAccounts())
}
}
companion object {
lateinit var instance: Amethyst
private set
instance.trim()
}
}

View File

@@ -0,0 +1,305 @@
/**
* Copyright (c) 2025 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 android.content.ContentResolver
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier
import com.vitorpamplona.amethyst.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
import com.vitorpamplona.amethyst.model.preferences.TorSharedPreferences
import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
import com.vitorpamplona.amethyst.model.torState.AccountsTorStateConnector
import com.vitorpamplona.amethyst.model.torState.TorRelayState
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache
import com.vitorpamplona.amethyst.service.crashreports.UnexpectedCrashSaver
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
import com.vitorpamplona.amethyst.service.images.ImageCacheFactory
import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.stats.RelayReqStats
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.File
class AppModules(
val appContext: Context,
) {
val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}"
// Exists to avoid exceptions stopping the coroutine
val exceptionHandler =
CoroutineExceptionHandler { _, throwable ->
Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable)
}
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
val applicationDefaultScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
// Blocking load of UI Preferences to avoid theme/language blinking
val uiPrefs by lazy {
UiSharedPreferences(appContext, applicationIOScope)
}
// Blocking load of Tor Settings to avoid connection leaks
val torPrefs by lazy {
TorSharedPreferences(appContext, applicationIOScope)
}
// App services that should be run as soon as there are subscribers to their flows
val locationManager = LocationState(appContext, applicationIOScope)
val connManager = ConnectivityManager(appContext, applicationIOScope)
val uiState by lazy {
UiSettingsState(uiPrefs.value, connManager.isMobileOrFalse, applicationIOScope)
}
val torManager = TorManager(torPrefs, appContext, applicationIOScope)
// Service that will run at all times to receive events from Pokey
val pokeyReceiver = PokeyReceiver()
// Key cache service to download and decrypt encrypted files before caching them.
val keyCache = EncryptionKeyCache()
// manages all the other connections separately from relays.
val okHttpClients =
DualHttpClientManager(
userAgent = appAgent,
proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull,
keyCache = keyCache,
scope = applicationIOScope,
)
// manages all relay connections
val okHttpClientForRelays =
DualHttpClientManager(
userAgent = appAgent,
proxyPortProvider = torManager.activePortOrNull,
isMobileDataProvider = connManager.isMobileOrNull,
keyCache = keyCache,
scope = applicationIOScope,
)
// Offers easy methods to know when connections are happening through Tor or not
val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value)
// Application-wide block height request cache
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
val otsResolverBuilder: TorAwareOkHttpOtsResolverBuilder =
TorAwareOkHttpOtsResolverBuilder(
roleBasedHttpClientBuilder::okHttpClientForMoney,
roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
otsBlockHeightCache,
)
// Application-wide ots verification cache
val otsVerifCache by lazy { VerificationStateCache(otsResolverBuilder) }
val torEvaluatorFlow =
TorRelayState(
okHttpClients,
torPrefs.value,
applicationDefaultScope,
)
// Connects the NostrClient class with okHttp
val websocketBuilder =
OkHttpWebSocket.Builder { url ->
val useTor = torEvaluatorFlow.flow.value.useTor(url)
okHttpClientForRelays.getHttpClient(useTor)
}
// Caches all events in Memory
val cache: LocalCache = LocalCache
// Provides a relay pool
val client: INostrClient = NostrClient(websocketBuilder, applicationDefaultScope)
// Watches for changes on Tor and Relay List Settings
val relayProxyClientConnector =
RelayProxyClientConnector(
torEvaluatorFlow.flow,
okHttpClientForRelays,
connManager,
torManager,
client,
applicationDefaultScope,
)
// Verifies and inserts in the cache from all relays, all subscriptions
val cacheClientConnector = CacheClientConnector(client, cache)
// Show messages from the Relay and controls their dismissal
val notifyCoordinator = NotifyCoordinator(client)
// Authenticates with relays.
val authCoordinator = AuthCoordinator(client, applicationDefaultScope)
// Tries to verify new OTS events when they arrive.
val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationDefaultScope)
// Tracks if it is possible to connect to relays.
val failureTracker = RelayOfflineTracker(client)
// Captures statistics about relays
val relayStats = RelayStats(client)
// Logs debug messages when needed
val detailedLogger = if (isDebug) RelayLogger(client, true, false) else null
val relayReqStats = if (isDebug) RelayReqStats(client) else null
val logger = if (isDebug) RelaySpeedLogger(client) else null
// Coordinates all subscriptions for the Nostr Client
val sources: RelaySubscriptionsCoordinator =
RelaySubscriptionsCoordinator(
LocalCache,
client,
authCoordinator.receiver,
failureTracker,
applicationDefaultScope,
)
// keeps all accounts live
val accountsCache =
AccountCacheState(
geolocationFlow = locationManager.geohashStateFlow,
nwcFilterAssembler = sources.nwc,
contentResolverFn = ::contentResolverFn,
otsResolverBuilder = otsResolverBuilder,
cache = cache,
client = client,
)
// Organizes cache clearing
val trimmingService = MemoryTrimmingService(cache)
// as new accounts are loaded, updates the state of the TorRelaySettings, which produces new TorRelayEvaluator
// and reconnects relays if the configuration has been changed.
val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationDefaultScope)
// saves the .content of NIP-95 blobs in disk to save memory
val nip95cache: File by lazy { Nip95CacheFactory.new(appContext) }
// local video cache with disk + memory
val videoCache: VideoCache by lazy { VideoCacheFactory.new(appContext) }
// image cache in disk for coil
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(appContext) }
// image cache in memory for coil
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(appContext) }
// crash report storage
val crashReportCache: CrashReportCache by lazy { CrashReportCache(appContext) }
// cache for NIP-11 documents
val nip11Cache: Nip11CachedRetriever by lazy {
Nip11CachedRetriever(torEvaluatorFlow::okHttpClientForRelay)
}
fun contentResolverFn(): ContentResolver = appContext.contentResolver
fun setImageLoader() {
ImageLoaderSetup.setup(appContext, { diskCache }, { memoryCache }) { url ->
okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(url))
}
}
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(appContext, npub)
fun initiate(appContext: Context) {
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
applicationIOScope.launch {
// loads main account quickly.
LocalPreferences.loadAccountConfigFromEncryptedStorage()
}
// forces initialization of uiPrefs in the main thread to avoid blinking themes
uiPrefs
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// preloads tor preferences
torPrefs
}
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// Sets Coil - Tor - OkHttp link
setImageLoader()
}
// registers to receive events
pokeyReceiver.register(appContext)
}
fun terminate(appContext: Context) {
pokeyReceiver.unregister(appContext)
applicationIOScope.cancel("Application onTerminate $appContext")
applicationDefaultScope.cancel("Application onTerminate $appContext")
accountsCache.clear()
}
fun trim() {
applicationDefaultScope.launch {
val loggedIn = accountsCache.accounts.value.values
trimmingService.run(loggedIn, LocalPreferences.allSavedAccounts())
}
}
}

View File

@@ -24,43 +24,42 @@ import android.app.ActivityManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Debug
import android.util.Log
import androidx.core.content.getSystemService
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls
import com.vitorpamplona.quartz.utils.Log
import kotlin.time.DurationUnit
import kotlin.time.measureTimedValue
@Suppress("SENSELESS_COMPARISON")
val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
fun debugState(context: Context) {
// Amethyst.instance.client
// .allSubscriptions()
// .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.filters.joinToString { it.filter.toJson() }}") }
private const val STATE_DUMP_TAG = "STATE DUMP"
fun debugState(context: Context) {
val totalMemoryMb = Runtime.getRuntime().totalMemory() / (1024 * 1024)
val freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024)
val maxMemoryMb = Runtime.getRuntime().maxMemory() / (1024 * 1024)
val jvmHeapAllocatedMb = totalMemoryMb - freeMemoryMb
Log.d("STATE DUMP", "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB")
Log.d(STATE_DUMP_TAG, "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB")
val nativeHeap = Debug.getNativeHeapAllocatedSize() / (1024 * 1024)
val maxNative = Debug.getNativeHeapSize() / (1024 * 1024)
Log.d("STATE DUMP", "Total Native Heap Allocated: $nativeHeap/$maxNative MB")
Log.d(STATE_DUMP_TAG, "Total Native Heap Allocated: $nativeHeap/$maxNative MB")
val activityManager: ActivityManager? = context.getSystemService()
if (activityManager != null) {
val isLargeHeap = (context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP) != 0
val memClass = if (isLargeHeap) activityManager.largeMemoryClass else activityManager.memoryClass
Log.d("STATE DUMP", "Memory Class $memClass MB (largeHeap $isLargeHeap)")
Log.d(STATE_DUMP_TAG, "Memory Class $memClass MB (largeHeap $isLargeHeap)")
}
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Connected Relays: " +
Amethyst.instance.client
.relayStatusFlow()
@@ -71,16 +70,23 @@ fun debugState(context: Context) {
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Indexed Relays: " +
Amethyst.instance.cache.relayHints.relayDB
.size() + "/" + normalizedUrls.size(),
)
Log.d(
STATE_DUMP_TAG,
"Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB",
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB",
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Notes: " +
LocalCache.notes.filter { _, it -> it.flowSet != null }.size +
" / " +
@@ -89,7 +95,7 @@ fun debugState(context: Context) {
LocalCache.notes.size(),
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Addressables: " +
LocalCache.addressables.filter { _, it -> it.flowSet != null }.size +
" / " +
@@ -98,7 +104,7 @@ fun debugState(context: Context) {
LocalCache.addressables.size(),
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Users: " +
LocalCache.users.filter { _, it -> it.flowSet != null }.size +
" / " +
@@ -107,12 +113,48 @@ fun debugState(context: Context) {
LocalCache.users.size(),
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Public Chat Channels: " +
LocalCache.publicChatChannels.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.publicChatChannels.size() +
" / " +
LocalCache.publicChatChannels.values().sumOf { it.notes.size() },
)
Log.d(
STATE_DUMP_TAG,
"Live Chat Channels: " +
LocalCache.liveChatChannels.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.liveChatChannels.size() +
" / " +
LocalCache.liveChatChannels.values().sumOf { it.notes.size() },
)
Log.d(
STATE_DUMP_TAG,
"Ephemeral Chat Channels: " +
LocalCache.ephemeralChannels.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.ephemeralChannels.size() +
" / " +
LocalCache.ephemeralChannels.values().sumOf { it.notes.size() },
)
LocalCache.chatroomList.forEach { key, room ->
Log.d(
STATE_DUMP_TAG,
"Private Chats $key: " +
room.rooms.size() +
" / " +
room.rooms.sumOf { key, value -> value.messages.size },
)
}
Log.d(
STATE_DUMP_TAG,
"Deletion Events: " +
LocalCache.deletionIndex.size(),
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Observable Events: " +
LocalCache.observablesByKindAndETag.size +
" / " +
@@ -120,15 +162,15 @@ fun debugState(context: Context) {
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Spam: " +
LocalCache.antiSpam.spamMessages.size() + " / " + LocalCache.antiSpam.recentEventIds.size() + " / " + LocalCache.antiSpam.recentAddressables.size(),
)
Log.d(
"STATE DUMP",
STATE_DUMP_TAG,
"Memory used by Events: " +
LocalCache.notes.sumOfLong { _, note -> note.event?.countMemory() ?: 0L } / (1024 * 1024) +
LocalCache.notes.sumOf { _, note -> note.event?.countMemory() ?: 0 } / (1024 * 1024) +
" MB",
)
@@ -137,16 +179,16 @@ fun debugState(context: Context) {
val bytesNotes =
LocalCache.notes
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
val bytesAddressables =
LocalCache.addressables
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ")
Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ")
}
qttAddressables.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB ")
Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB ")
}
}

View File

@@ -23,26 +23,23 @@ package com.vitorpamplona.amethyst
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.core.content.edit
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.model.UiSettings
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
@@ -55,16 +52,19 @@ import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import java.io.File
// Release mode (!BuildConfig.DEBUG) always uses encrypted preferences
@@ -74,6 +74,7 @@ private const val DEBUG_PLAINTEXT_PREFERENCES = false
private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
@Immutable
@Serializable
data class AccountInfo(
val npub: String,
val hasPrivKey: Boolean,
@@ -98,6 +99,7 @@ private object PrefKeys {
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
const val LATEST_INDEX_RELAY_LIST = "latestIndexRelayList"
const val LATEST_BLOCKED_RELAY_LIST = "latestBlockedRelayList"
const val LATEST_TRUSTED_RELAY_LIST = "latestTrustedRelayList"
const val LATEST_MUTE_LIST = "latestMuteList"
@@ -163,7 +165,7 @@ object LocalPreferences {
with(encryptedPreferences()) {
val newSystemOfAccounts =
getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let {
JsonMapper.mapper.readValue<List<AccountInfo>>(it)
JsonMapper.fromJson<List<AccountInfo>>(it)
}
if (!newSystemOfAccounts.isNullOrEmpty()) {
@@ -183,7 +185,9 @@ object LocalPreferences {
savedAccounts.emit(migrated)
edit { putString(PrefKeys.ALL_ACCOUNT_INFO, JsonMapper.mapper.writeValueAsString(savedAccounts.value)) }
edit {
putString(PrefKeys.ALL_ACCOUNT_INFO, JsonMapper.toJson(migrated))
}
}
}
}
@@ -203,14 +207,14 @@ object LocalPreferences {
.edit {
putString(
PrefKeys.ALL_ACCOUNT_INFO,
JsonMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }),
JsonMapper.toJson(accounts.filter { !it.isTransient }),
)
}
}
}
private val prefsDirPath: String
get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/"
get() = "${Amethyst.instance.appContext.filesDir.parent}/shared_prefs/"
private suspend fun addAccount(accInfo: AccountInfo) {
val accounts = savedAccounts().filter { it.npub != accInfo.npub }.plus(accInfo)
@@ -255,7 +259,7 @@ object LocalPreferences {
return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
val preferenceFile =
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
Amethyst.instance.appContext.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
} else {
Amethyst.instance.encryptedStorage(npub)
}
@@ -310,7 +314,7 @@ object LocalPreferences {
putString(
PrefKeys.DEFAULT_FILE_SERVER,
JsonMapper.mapper.writeValueAsString(settings.defaultFileServer),
JsonMapper.toJson(settings.defaultFileServer),
)
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value)
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value)
@@ -322,72 +326,18 @@ object LocalPreferences {
PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST,
settings.defaultDiscoveryFollowList.value,
)
putString(
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
JsonMapper.mapper.writeValueAsString(settings.zapPaymentRequest.value?.denormalize()),
)
if (settings.backupContactList != null) {
putString(
PrefKeys.LATEST_CONTACT_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupContactList),
)
} else {
remove(PrefKeys.LATEST_CONTACT_LIST)
}
if (settings.backupUserMetadata != null) {
putString(
PrefKeys.LATEST_USER_METADATA,
JsonMapper.mapper.writeValueAsString(settings.backupUserMetadata),
)
} else {
remove(PrefKeys.LATEST_USER_METADATA)
}
putOrRemove(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, settings.zapPaymentRequest.value?.denormalize())
if (settings.backupDMRelayList != null) {
putString(
PrefKeys.LATEST_DM_RELAY_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupDMRelayList),
)
} else {
remove(PrefKeys.LATEST_DM_RELAY_LIST)
}
putOrRemove(PrefKeys.LATEST_CONTACT_LIST, settings.backupContactList)
if (settings.backupNIP65RelayList != null) {
putString(
PrefKeys.LATEST_NIP65_RELAY_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupNIP65RelayList),
)
} else {
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
}
if (settings.backupSearchRelayList != null) {
putString(
PrefKeys.LATEST_SEARCH_RELAY_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupSearchRelayList),
)
} else {
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
}
if (settings.backupBlockedRelayList != null) {
putString(
PrefKeys.LATEST_BLOCKED_RELAY_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupBlockedRelayList),
)
} else {
remove(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
}
if (settings.backupTrustedRelayList != null) {
putString(
PrefKeys.LATEST_TRUSTED_RELAY_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupTrustedRelayList),
)
} else {
remove(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
}
putOrRemove(PrefKeys.LATEST_USER_METADATA, settings.backupUserMetadata)
putOrRemove(PrefKeys.LATEST_DM_RELAY_LIST, settings.backupDMRelayList)
putOrRemove(PrefKeys.LATEST_NIP65_RELAY_LIST, settings.backupNIP65RelayList)
putOrRemove(PrefKeys.LATEST_SEARCH_RELAY_LIST, settings.backupSearchRelayList)
putOrRemove(PrefKeys.LATEST_INDEX_RELAY_LIST, settings.backupIndexRelayList)
putOrRemove(PrefKeys.LATEST_BLOCKED_RELAY_LIST, settings.backupBlockedRelayList)
putOrRemove(PrefKeys.LATEST_TRUSTED_RELAY_LIST, settings.backupTrustedRelayList)
if (settings.localRelayServers.value.isNotEmpty()) {
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value)
@@ -395,77 +345,15 @@ object LocalPreferences {
remove(PrefKeys.LOCAL_RELAY_SERVERS)
}
if (settings.backupMuteList != null) {
putString(
PrefKeys.LATEST_MUTE_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupMuteList),
)
} else {
remove(PrefKeys.LATEST_MUTE_LIST)
}
putOrRemove(PrefKeys.LATEST_MUTE_LIST, settings.backupMuteList)
putOrRemove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, settings.backupPrivateHomeRelayList)
putOrRemove(PrefKeys.LATEST_APP_SPECIFIC_DATA, settings.backupAppSpecificData)
if (settings.backupPrivateHomeRelayList != null) {
putString(
PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupPrivateHomeRelayList),
)
} else {
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
}
if (settings.backupAppSpecificData != null) {
putString(
PrefKeys.LATEST_APP_SPECIFIC_DATA,
JsonMapper.mapper.writeValueAsString(settings.backupAppSpecificData),
)
} else {
remove(PrefKeys.LATEST_APP_SPECIFIC_DATA)
}
if (settings.backupChannelList != null) {
putString(
PrefKeys.LATEST_CHANNEL_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupChannelList),
)
} else {
remove(PrefKeys.LATEST_CHANNEL_LIST)
}
if (settings.backupCommunityList != null) {
putString(
PrefKeys.LATEST_COMMUNITY_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupCommunityList),
)
} else {
remove(PrefKeys.LATEST_COMMUNITY_LIST)
}
if (settings.backupHashtagList != null) {
putString(
PrefKeys.LATEST_HASHTAG_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupHashtagList),
)
} else {
remove(PrefKeys.LATEST_HASHTAG_LIST)
}
if (settings.backupGeohashList != null) {
putString(
PrefKeys.LATEST_GEOHASH_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupGeohashList),
)
} else {
remove(PrefKeys.LATEST_GEOHASH_LIST)
}
if (settings.backupEphemeralChatList != null) {
putString(
PrefKeys.LATEST_EPHEMERAL_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupEphemeralChatList),
)
} else {
remove(PrefKeys.LATEST_EPHEMERAL_LIST)
}
putOrRemove(PrefKeys.LATEST_CHANNEL_LIST, settings.backupChannelList)
putOrRemove(PrefKeys.LATEST_COMMUNITY_LIST, settings.backupCommunityList)
putOrRemove(PrefKeys.LATEST_HASHTAG_LIST, settings.backupHashtagList)
putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList)
putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList)
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
@@ -475,8 +363,6 @@ object LocalPreferences {
remove(PrefKeys.USE_PROXY)
remove(PrefKeys.PROXY_PORT)
putString(PrefKeys.TOR_SETTINGS, JsonMapper.mapper.writeValueAsString(settings.torSettings.toSettings()))
val regularMap =
settings.lastReadPerRoute.value.mapValues {
it.value.value
@@ -484,13 +370,13 @@ object LocalPreferences {
putString(
PrefKeys.LAST_READ_PER_ROUTE,
JsonMapper.mapper.writeValueAsString(regularMap),
JsonMapper.toJson(regularMap),
)
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
putString(
PrefKeys.PENDING_ATTESTATIONS,
JsonMapper.mapper.writeValueAsString(settings.pendingAttestations.value),
JsonMapper.toJson(settings.pendingAttestations.value),
)
}
}
@@ -498,23 +384,25 @@ object LocalPreferences {
Log.d("LocalPreferences", "Saved to encrypted storage")
}
suspend fun loadCurrentAccountFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
suspend fun loadAccountConfigFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadAccountConfigFromEncryptedStorage(it) }
suspend fun saveSharedSettings(
sharedSettings: Settings,
fun saveSharedSettings(
sharedSettings: UiSettings,
prefs: SharedPreferences = encryptedPreferences(),
) {
Log.d("LocalPreferences", "Saving to shared settings")
prefs.edit {
putString(PrefKeys.SHARED_SETTINGS, JsonMapper.mapper.writeValueAsString(sharedSettings))
putString(PrefKeys.SHARED_SETTINGS, JsonMapper.toJson(sharedSettings))
}
}
suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? {
fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): UiSettings? {
Log.d("LocalPreferences", "Load shared settings")
with(prefs) {
return try {
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { JsonMapper.mapper.readValue<Settings>(it) }
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let {
JsonMapper.fromJson<UiSettings>(it)
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
@@ -529,7 +417,7 @@ object LocalPreferences {
val mutex = Mutex()
suspend fun loadCurrentAccountFromEncryptedStorage(npub: String): AccountSettings? {
suspend fun loadAccountConfigFromEncryptedStorage(npub: String): AccountSettings? {
// if already loaded, return right away
if (cachedAccounts.containsKey(npub)) {
return cachedAccounts[npub]
@@ -583,6 +471,7 @@ object LocalPreferences {
val latestDmRelayList = parseEventOrNull<ChatMessageRelayListEvent>(PrefKeys.LATEST_DM_RELAY_LIST)
val latestNip65RelayList = parseEventOrNull<AdvertisedRelayListEvent>(PrefKeys.LATEST_NIP65_RELAY_LIST)
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
val latestIndexRelayList = parseEventOrNull<IndexerRelayListEvent>(PrefKeys.LATEST_INDEX_RELAY_LIST)
val latestBlockedRelayList = parseEventOrNull<BlockedRelayListEvent>(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
val latestTrustedRelayList = parseEventOrNull<TrustedRelayListEvent>(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
@@ -598,8 +487,6 @@ object LocalPreferences {
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
val torSettings = parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings()
val lastReadPerRoute =
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
MutableStateFlow(it.value)
@@ -627,6 +514,7 @@ object LocalPreferences {
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
backupSearchRelayList = latestSearchRelayList,
backupIndexRelayList = latestIndexRelayList,
backupBlockedRelayList = latestBlockedRelayList,
backupTrustedRelayList = latestTrustedRelayList,
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
@@ -637,7 +525,6 @@ object LocalPreferences {
backupHashtagList = latestHashtagList,
backupGeohashList = latestGeohashList,
backupEphemeralChatList = latestEphemeralList,
torSettings = TorSettingsFlow.build(torSettings),
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
pendingAttestations = MutableStateFlow(pendingAttestations),
@@ -648,7 +535,7 @@ object LocalPreferences {
return result
}
private inline fun <reified T> SharedPreferences.parseOrNull(key: String): T? {
private inline fun <reified T : Any> SharedPreferences.parseOrNull(key: String): T? {
val value = getString(key, null)
if (value.isNullOrEmpty() || value == "null") {
return null
@@ -657,7 +544,7 @@ object LocalPreferences {
if (T::class.java.isInstance(Event::class.java)) {
Event.fromJson(value) as T?
} else {
JsonMapper.mapper.readValue<T?>(value)
JsonMapper.fromJson<T>(value)
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
@@ -679,4 +566,37 @@ object LocalPreferences {
null
}
}
fun SharedPreferences.Editor.putOrRemove(
key: String,
event: Any?,
) {
if (event != null) {
putString(key, JsonMapper.toJson(event))
} else {
remove(key)
}
}
fun SharedPreferences.Editor.putOrRemove(
key: String,
event: Event?,
) {
if (event != null) {
putString(key, OptimizedJsonMapper.toJson(event))
} else {
remove(key)
}
}
fun SharedPreferences.Editor.putOrRemove(
key: String,
nwc: Nip47WalletConnect.Nip47URI?,
) {
if (nwc != null) {
putString(key, JsonMapper.toJson(nwc))
} else {
remove(key)
}
}
}

View File

@@ -20,11 +20,11 @@
*/
package com.vitorpamplona.amethyst.model
import android.util.Log
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.logTime
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
@@ -35,9 +35,11 @@ import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState
import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState
import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState
import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState
import com.vitorpamplona.amethyst.model.nip02FollowLists.DeclaredFollowsPerOutboxRelay
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListReusedOutboxOrProxyRelays
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay
import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState
import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState
import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
@@ -52,7 +54,6 @@ import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
@@ -65,6 +66,9 @@ import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayLis
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache
@@ -78,18 +82,20 @@ import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
import com.vitorpamplona.amethyst.model.privacyOptions.PrivacyState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineWithIndexRelayListsState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineWithSearchRelayListsState
import com.vitorpamplona.amethyst.model.serverList.MergedServerListState
import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
import com.vitorpamplona.amethyst.model.torState.TorRelayState
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
@@ -121,18 +127,16 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
@@ -161,8 +165,6 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
@@ -173,7 +175,6 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
@@ -200,12 +201,15 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.containsAny
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -218,9 +222,11 @@ import kotlin.coroutines.cancellation.CancellationException
class Account(
val settings: AccountSettings = AccountSettings(KeyPair()),
val signer: NostrSigner,
geolocationFlow: StateFlow<LocationState.LocationResult>,
val geolocationFlow: StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val otsResolverBuilder: OtsResolverBuilder,
val cache: LocalCache,
val client: NostrClient,
val client: INostrClient,
val scope: CoroutineScope,
) {
private var userProfileCache: User? = null
@@ -229,7 +235,7 @@ class Account(
val userMetadata = UserMetadataState(signer, cache, scope, settings)
val nip47SignerState = NwcSignerState(signer, cache, scope, settings)
val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings)
val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings)
val localRelayList = LocalRelayListState(signer, cache, scope, settings)
@@ -257,7 +263,7 @@ class Account(
val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer)
val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings)
val kind3FollowList = FollowListState(signer, cache, scope, settings)
val kind3FollowList = Kind3FollowListState(signer, cache, scope, settings)
val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer)
val ephemeralChatList = EphemeralChatListState(signer, cache, ephemeralChatListDecryptionCache, scope, settings)
@@ -279,6 +285,8 @@ class Account(
val peopleListDecryptionCache = PeopleListDecryptionCache(signer)
val blockPeopleList = BlockPeopleListState(signer, cache, peopleListDecryptionCache, scope)
val peopleLists = PeopleListsState(signer, cache, peopleListDecryptionCache, scope)
val followLists = FollowListsState(signer, cache, scope)
val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings)
@@ -301,13 +309,22 @@ class Account(
// Follows Relays
val followOutboxesOrProxy = FollowListOutboxOrProxyRelays(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope)
val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, indexerRelayList, scope)
// only follow relays that are declared in more than one user.
val followSharedOutboxesOrProxy = FollowListReusedOutboxOrProxyRelays(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope)
val followPlusAllMineWithIndex = MergedFollowPlusMineWithIndexRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, indexerRelayList, scope)
val followPlusAllMineWithSearch = MergedFollowPlusMineWithSearchRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, searchRelayList, scope)
val defaultGlobalRelays = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, scope)
// keeps a cache of the declared outbox relays for each author
val declaredFollowsPerRelay = DeclaredFollowsPerOutboxRelay(kind3FollowList, cache, scope).flow
// keeps a cache of the outbox relays for each author
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow
// Merges all follow lists to create a single All Follows feed.
val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope)
val allFollows = MergedFollowListsState(kind3FollowList, peopleLists, followLists, hashtagList, geohashList, communityList, scope)
val privateDMDecryptionCache = PrivateDMCache(signer)
val privateZapsDecryptionCache = PrivateZapCache(signer)
@@ -315,17 +332,7 @@ class Account(
val chatroomList = cache.getOrCreateChatroomList(signer.pubKey)
val privacyState = PrivacyState(settings)
val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope)
val otsResolverBuilder: OkHttpOtsResolverBuilder =
OkHttpOtsResolverBuilder(
{
Amethyst.instance.okHttpClients.getHttpClient(privacyState.shouldUseTorForMoneyOperations(it))
},
privacyState::shouldUseTorForMoneyOperations,
Amethyst.instance.otsBlockHeightCache,
)
val newNotesPreProcessor = EventProcessor(this, cache)
val otsState = OtsState(signer, cache, otsResolverBuilder, scope, settings)
@@ -342,9 +349,10 @@ class Account(
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> =
FeedTopNavFilterState(
feedFilterListName = settings.defaultHomeFollowList,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = followPlusAllMine.flow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
caches = feedDecryptionCaches,
@@ -357,9 +365,10 @@ class Account(
val liveStoriesFollowLists: StateFlow<IFeedTopNavFilter> =
FeedTopNavFilterState(
feedFilterListName = settings.defaultStoriesFollowList,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = followPlusAllMine.flow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
caches = feedDecryptionCaches,
@@ -372,9 +381,10 @@ class Account(
val liveDiscoveryFollowLists: StateFlow<IFeedTopNavFilter> =
FeedTopNavFilterState(
feedFilterListName = settings.defaultDiscoveryFollowList,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = followPlusAllMine.flow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
caches = feedDecryptionCaches,
@@ -387,9 +397,10 @@ class Account(
val liveNotificationFollowLists: StateFlow<IFeedTopNavFilter> =
FeedTopNavFilterState(
feedFilterListName = settings.defaultNotificationFollowList,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = followPlusAllMine.flow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
caches = feedDecryptionCaches,
@@ -399,18 +410,6 @@ class Account(
val liveNotificationFollowListsPerRelay = OutboxLoaderState(liveNotificationFollowLists, cache, scope).flow
/*
val mergedTopFeedAuthorLists =
MergedTopFeedAuthorListsState(
liveHomeFollowListsPerRelay,
liveStoriesFollowListsPerRelay,
liveDiscoveryFollowListsPerRelay,
liveNotificationFollowListsPerRelay,
scope,
).flow
*/
fun isWriteable(): Boolean = settings.isWriteable()
suspend fun updateWarnReports(warnReports: Boolean): Boolean {
@@ -704,7 +703,8 @@ class Account(
fun computeRelayListToBroadcast(event: Event): Set<NormalizedRelayUrl> {
if (event is MetadataEvent || event is AdvertisedRelayListEvent) {
return followPlusAllMine.flow.value + client.relayStatusFlow().value.available
// everywhere
return followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available
}
if (event is GiftWrapEvent) {
val receiver = event.recipientPubKey()
@@ -813,32 +813,32 @@ class Account(
}
}
fun broadcast(note: Note) {
note.event?.let {
if (it is WrappedEvent && it.host != null) {
suspend fun broadcast(note: Note) {
note.event?.let { noteEvent ->
if (noteEvent is WrappedEvent && noteEvent.host != null) {
// download the event and send it.
it.host?.let { host ->
client.downloadFirstEvent(
filters =
note.relays.associateWith { relay ->
listOf(
Filter(
ids = listOf(host.id),
),
)
},
onResponse = {
client.send(it, computeRelayListToBroadcast(it))
},
)
noteEvent.host?.let { host ->
client
.downloadFirstEvent(
filters =
note.relays.associateWith { relay ->
listOf(
Filter(
ids = listOf(host.id),
),
)
},
)?.let { downloadedEvent ->
client.send(downloadedEvent, computeRelayListToBroadcast(downloadedEvent))
}
}
} else {
client.send(it, computeRelayListToBroadcast(note))
client.send(noteEvent, computeRelayListToBroadcast(note))
}
}
}
suspend fun updateAttestations() = sendAutomatic(otsState.updateAttestations())
fun upgradeAttestations() = otsState.upgradeAttestationsIfNeeded(::sendAutomatic)
suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user))
@@ -904,7 +904,7 @@ class Account(
}
fun sendLiterallyEverywhere(event: Event) {
client.send(event, outboxRelays.flow.value + indexerRelayList.flow.value + client.relayStatusFlow().value.available)
client.send(event, followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available)
cache.justConsumeMyOwnEvent(event)
}
@@ -1469,18 +1469,6 @@ class Account(
val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) }
mine.forEach { giftWrap ->
val gift = giftWrap.unwrapOrNull(signer)
if (gift is SealedRumorEvent) {
val rumor = gift.unsealOrNull(signer)
if (rumor != null) {
cache.justConsumeMyOwnEvent(rumor)
}
}
if (gift != null) {
cache.justConsumeMyOwnEvent(gift)
}
cache.justConsumeMyOwnEvent(giftWrap)
}
@@ -1558,13 +1546,10 @@ class Account(
}
}
suspend fun sendAuthEvent(
relay: IRelayClient,
suspend fun createAuthEvent(
relay: NormalizedRelayUrl,
challenge: String,
) {
val auth = RelayAuthEvent.create(relay.url, challenge, signer)
client.sendIfExists(auth, relay.url)
}
): RelayAuthEvent = RelayAuthEvent.create(relay, challenge, signer)
suspend fun hideWord(word: String) {
sendMyPublicAndPrivateOutbox(muteList.hideWord(word))
@@ -1683,10 +1668,24 @@ class Account(
note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users
}
fun isDecryptedContentHidden(noteEvent: PrivateDmEvent): Boolean =
if (hiddenUsers.flow.value.hiddenWordsCase
.isNotEmpty()
) {
val decrypted = privateDMDecryptionCache.cachedDM(noteEvent)
decrypted?.containsAny(hiddenUsers.flow.value.hiddenWordsCase) == true
} else {
false
}
fun isFollowing(user: User): Boolean = user.pubkeyHex in followingKeySet()
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
fun isKnown(user: User): Boolean = user.pubkeyHex in allFollows.flow.value.authors
fun isKnown(user: HexKey): Boolean = user in allFollows.flow.value.authors
fun isAcceptable(note: Note): Boolean {
return note.author?.let { isAcceptable(it) } ?: true &&
// if user hasn't hided this author
@@ -1737,22 +1736,6 @@ class Account(
suspend fun sendBlossomServersList(servers: List<String>) = sendMyPublicAndPrivateOutbox(blossomServers.saveBlossomServersList(servers))
fun getAllPeopleLists(): List<AddressableNote> = getAllPeopleLists(signer.pubKey)
fun getAllPeopleLists(pubkey: HexKey): List<AddressableNote> =
cache.addressables
.filter { _, addressableNote ->
val noteEvent = addressableNote.event
if (noteEvent is PeopleListEvent) {
noteEvent.pubKey == pubkey && (noteEvent.hasAnyTaggedUser() || peopleListDecryptionCache.cachedUserIdSet(noteEvent).isNotEmpty())
} else if (noteEvent is FollowListEvent) {
noteEvent.pubKey == pubkey && noteEvent.hasAnyTaggedUser()
} else {
false
}
}
fun markAsRead(
route: String,
timestampInSecs: Long,
@@ -1767,7 +1750,7 @@ class Account(
fun observeDonatedInThisVersion() =
settings
.observeDonatedInVersion(BuildConfig.VERSION_NAME)
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, hasDonatedInThisVersion())
fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME)
@@ -1775,7 +1758,7 @@ class Account(
init {
Log.d("AccountRegisterObservers", "Init")
scope.launch(Dispatchers.Default) {
scope.launch {
cache.antiSpam.flowSpam.collect {
it.cache.spamMessages.snapshot().values.forEach { spammer ->
if (!hiddenUsers.isHidden(spammer.pubkeyHex) && spammer.shouldHide()) {
@@ -1786,5 +1769,34 @@ class Account(
}
}
}
scope.launch {
cache.live.newEventBundles.collect { newNotes ->
logTime("Account ${userProfile().toBestDisplayName()} newEventBundle Update with ${newNotes.size} new notes") {
upgradeAttestations()
newNotesPreProcessor.runNew(newNotes)
peopleLists.newNotes(newNotes)
followLists.newNotes(newNotes)
}
}
}
scope.launch {
cache.live.deletedEventBundles.collect { deletedNotes ->
logTime("Account ${userProfile().toBestDisplayName()} deletedEventBundle Update with ${deletedNotes.size} new notes") {
newNotesPreProcessor.runDeleted(deletedNotes)
peopleLists.deletedNotes(deletedNotes)
followLists.deletedNotes(deletedNotes)
}
}
}
scope.launch(Dispatchers.IO) {
settings.saveable.debounce(1000).collect {
if (it.accountSettings != null) {
LocalPreferences.saveToEncryptedStorage(it.accountSettings)
}
}
}
}
}

View File

@@ -20,19 +20,14 @@
*/
package com.vitorpamplona.amethyst.model
import android.content.ContentResolver
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
@@ -46,10 +41,10 @@ import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
@@ -103,6 +98,12 @@ val GLOBAL_FOLLOWS = " Global "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val ALL_FOLLOWS = " All Follows "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val ALL_USER_FOLLOWS = " All User Follows "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val KIND3_FOLLOWS = " Main User Follows "
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val AROUND_ME = " Around Me "
@@ -126,6 +127,7 @@ class AccountSettings(
var backupDMRelayList: ChatMessageRelayListEvent? = null,
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
var backupSearchRelayList: SearchRelayListEvent? = null,
var backupIndexRelayList: IndexerRelayListEvent? = null,
var backupBlockedRelayList: BlockedRelayListEvent? = null,
var backupTrustedRelayList: TrustedRelayListEvent? = null,
var backupMuteList: MuteListEvent? = null,
@@ -136,7 +138,6 @@ class AccountSettings(
var backupHashtagList: HashtagListEvent? = null,
var backupGeohashList: GeohashListEvent? = null,
var backupEphemeralChatList: EphemeralChatListEvent? = null,
val torSettings: TorSettingsFlow = TorSettingsFlow(),
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
@@ -154,21 +155,6 @@ class AccountSettings(
fun isWriteable(): Boolean = keyPair.privKey != null || externalSignerPackageName != null
fun createSigner(contentResolver: ContentResolver) =
if (keyPair.privKey != null) {
NostrSignerInternal(keyPair)
} else {
when (val packageName = externalSignerPackageName) {
null -> NostrSignerInternal(keyPair)
else ->
NostrSignerExternal(
pubKey = keyPair.pubKey.toHexKey(),
packageName = packageName,
contentResolver = contentResolver,
)
}
}
// ---
// Zaps and Reactions
// ---
@@ -268,17 +254,6 @@ class AccountSettings(
}
}
// ---
// proxy settings
// ---
fun setTorSettings(newTorSettings: TorSettings): Boolean =
if (torSettings.update(newTorSettings)) {
saveAccountSettings()
true
} else {
false
}
// ---
// language services
// ---
@@ -372,6 +347,16 @@ class AccountSettings(
}
}
fun updateIndexRelayList(newIndexRelayList: IndexerRelayListEvent?) {
if (newIndexRelayList == null || newIndexRelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupIndexRelayList?.id != newIndexRelayList.id) {
backupIndexRelayList = newIndexRelayList
saveAccountSettings()
}
}
fun updateBlockedRelayList(newBlockedRelayList: BlockedRelayListEvent?) {
if (newBlockedRelayList == null || newBlockedRelayList.tags.isEmpty()) return

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.serialization.Serializable
import java.util.Locale
val DefaultReactions =
@@ -47,6 +48,7 @@ fun getLanguagesSpokenByUser(): Set<String> {
return codedList
}
@Serializable
class AccountSyncedSettingsInternal(
val reactions: AccountReactionPreferencesInternal = AccountReactionPreferencesInternal(),
val zaps: AccountZapPreferencesInternal = AccountZapPreferencesInternal(),
@@ -54,21 +56,25 @@ class AccountSyncedSettingsInternal(
val security: AccountSecurityPreferencesInternal = AccountSecurityPreferencesInternal(),
)
@Serializable
class AccountReactionPreferencesInternal(
var reactionChoices: List<String> = DefaultReactions,
)
@Serializable
class AccountZapPreferencesInternal(
var zapAmountChoices: List<Long> = DefaultZapAmounts,
val defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
)
@Serializable
class AccountLanguagePreferencesInternal(
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
var languagePreferences: Map<String, String> = mapOf(),
var translateTo: String = Locale.getDefault().language,
)
@Serializable
class AccountSecurityPreferencesInternal(
val showSensitiveContent: Boolean? = null,
var warnAboutPostsWithReports: Boolean = true,

View File

@@ -20,18 +20,18 @@
*/
package com.vitorpamplona.amethyst.model
import android.util.Log
import android.util.LruCache
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.ui.note.njumpLink
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.flow.MutableStateFlow
data class Spammer(
@@ -65,7 +65,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 && event.content.startsWith("nostr:") && Nip19Parser.nip19regex.matcher(event.content).find()) return false
if (event.content.length < 180 && event.content.startsWith("nostr:") && Nip19Parser.hasAny(event.content)) return false
// double list strategy:
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
@@ -93,7 +93,9 @@ class AntiSpamFilter {
val spammer = logOffender(hash, event)
if (spammer.shouldHide() && relay != null) {
RelayStats.newSpam(relay, "$link1 $link2")
Amethyst.instance.relayStats
.get(relay)
.newSpam("$link1 $link2")
}
flowSpam.tryEmit(AntiSpamState(this))
@@ -119,7 +121,9 @@ class AntiSpamFilter {
val spammer = logOffender(hash, event)
if (spammer.shouldHide() && relay != null) {
RelayStats.newSpam(relay, "$link1 $link2")
Amethyst.instance.relayStats
.get(relay)
.newSpam("$link1 $link2")
}
flowSpam.tryEmit(AntiSpamState(this))

View File

@@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.ListChange
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.LargeCache
import com.vitorpamplona.quartz.utils.cache.LargeCache
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -43,11 +43,21 @@ abstract class Channel : NotesGatherer {
fun changesFlow(): MutableSharedFlow<ListChange<Note>> {
val current = changesFlow.get()
if (current != null) return current
val new = MutableSharedFlow<ListChange<Note>>(0, 100, BufferOverflow.DROP_OLDEST)
val new = MutableSharedFlow<ListChange<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
changesFlow = WeakReference(new)
return new
}
open fun participatingAuthors(maxTimeLimit: Long) =
notes.mapNotNull { key, value ->
val createdAt = value.createdAt()
if (createdAt != null && createdAt > maxTimeLimit) {
value.author
} else {
null
}
}
abstract fun toBestDisplayName(): String
open fun relays(): Set<NormalizedRelayUrl> =
@@ -86,7 +96,7 @@ abstract class Channel : NotesGatherer {
notes.put(note.idHex, note)
note.addGatherer(this)
if ((note.createdAt() ?: 0) > (lastNote?.createdAt() ?: 0)) {
if ((note.createdAt() ?: 0L) > (lastNote?.createdAt() ?: 0L)) {
lastNote = note
}

View File

@@ -35,7 +35,6 @@ object Constants {
val bitcoiner = RelayUrlNormalizer.normalize("wss://nostr.bitcoiner.social")
val oxtr = RelayUrlNormalizer.normalize("wss://nostr.oxtr.dev")
val fmtwiz = RelayUrlNormalizer.normalize("wss://nostr.fmt.wiz.biz")
val nostoday = RelayUrlNormalizer.normalize("wss://search.nos.today")
@@ -46,7 +45,8 @@ object Constants {
val coracle = RelayUrlNormalizer.normalize("wss://indexer.coracle.social")
val userkinds = RelayUrlNormalizer.normalize("wss://user.kindpag.es")
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr, fmtwiz)
val bootstrapInbox = setOf(band, damus, primal, mom, nos, bitcoiner, oxtr)
val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr)
val defaultSearchRelaySet = setOf(band, wine, where)
}

View File

@@ -49,23 +49,24 @@ import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
import com.vitorpamplona.amethyst.ui.components.HashTag
import com.vitorpamplona.amethyst.ui.components.RenderRegular
import com.vitorpamplona.amethyst.ui.components.RenderTextParagraph
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
@Preview
@Composable
fun RenderHashTagIconsPreview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
RenderRegular(
"Testing rendering of hashtags: #flowerstr #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain",
EmptyTagList,
) { word, state ->
when (word) {
is HashTagSegment -> HashTag(word, accountViewModel, EmptyNav)
is RegularTextSegment -> Text(word.segmentText)
) { paragraph, state, spaceWidth, modifier ->
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
when (word) {
is HashTagSegment -> HashTag(word, EmptyNav())
is RegularTextSegment -> Text(word.segmentText)
}
}
}
}

View File

@@ -18,14 +18,15 @@
* 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.quartz.utils
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.quartz.utils.cache.CacheOperations
import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentSkipListMap
import java.util.function.BiConsumer
class LargeSoftCache<K, V> : CacheOperations<K, V> {
private val cache = ConcurrentSkipListMap<K, WeakReference<V>>()
protected val cache = ConcurrentSkipListMap<K, WeakReference<V>>()
fun keys() = cache.keys
@@ -117,15 +118,23 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
}
keysToRemove.forEach { key, value ->
cache.remove(key, value)
println("Cleaned up entry for key: $key (object was garbage collected)")
}
println("Cache cleanup completed. Remaining size: ${cache.size}")
}
override fun forEach(consumer: BiConsumer<K, V>) {
cache.forEach(BiConsumerWrapper(this, consumer))
}
override fun forEach(
from: K,
to: K,
consumer: BiConsumer<K, V>,
) {
cache
.subMap(from, true, to, true)
.forEach(BiConsumerWrapper(this, consumer))
}
class BiConsumerWrapper<K, V>(
val cache: LargeSoftCache<K, V>,
val inner: BiConsumer<K, V>,

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2025 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 com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.cache.CacheCollectors
const val START_KEY = "0000000000000000000000000000000000000000000000000000000000000000"
const val END_KEY = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
const val START_D_TAG = ""
const val END_D_TAG = "\uFFFF\uFFFF\uFFFF\uFFFF"
fun kindStart(
kind: Int,
pubKey: HexKey,
) = Address(kind, pubKey, "")
fun kindEnd(
kind: Int,
pubKey: HexKey,
) = Address(kind, pubKey, END_D_TAG)
fun kindStart(kind: Int) = kindStart(kind, START_KEY)
fun kindEnd(kind: Int) = kindEnd(kind, END_KEY)
val ACCEPT_ALL_FILTER = CacheCollectors.BiFilter<Address, AddressableNote> { key, note -> true }
fun LargeSoftCache<Address, AddressableNote>.filter(
kind: Int,
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
): List<AddressableNote> = filter(kindStart(kind), kindEnd(kind), consumer)
fun LargeSoftCache<Address, AddressableNote>.filter(
kinds: List<Int>,
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
): List<AddressableNote> {
val set = mutableSetOf<AddressableNote>()
kinds.forEach {
set.addAll(filter(kindStart(it), kindEnd(it), consumer))
}
return set.toList()
}
fun LargeSoftCache<Address, AddressableNote>.filter(
kind: Int,
pubKey: HexKey,
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
): List<AddressableNote> = filter(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
kind: Int,
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
): Set<AddressableNote> = filterIntoSet(kindStart(kind), kindEnd(kind), consumer)
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
kinds: List<Int>,
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
): Set<AddressableNote> {
val set = mutableSetOf<AddressableNote>()
kinds.forEach {
set.addAll(filterIntoSet(kindStart(it), kindEnd(it), consumer))
}
return set
}
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
kind: Int,
pubKey: HexKey,
consumer: CacheCollectors.BiFilter<Address, AddressableNote> = ACCEPT_ALL_FILTER,
): Set<AddressableNote> = filterIntoSet(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)
fun <R> LargeSoftCache<Address, AddressableNote>.mapNotNullIntoSet(
kind: Int,
consumer: CacheCollectors.BiMapper<Address, AddressableNote, R?>,
): Set<R> = mapNotNullIntoSet(kindStart(kind), kindEnd(kind), consumer)
fun <R> LargeSoftCache<Address, AddressableNote>.mapNotNullIntoSet(
kind: Int,
pubKey: HexKey,
consumer: CacheCollectors.BiMapper<Address, AddressableNote, R?>,
): Set<R> = mapNotNullIntoSet(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)

View File

@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.model
import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst
@@ -50,36 +49,37 @@ import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.checkSignature
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.tagValueContains
import com.vitorpamplona.quartz.nip01Core.crypto.checkSignature
import com.vitorpamplona.quartz.nip01Core.crypto.verify
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
import com.vitorpamplona.quartz.nip01Core.tags.events.forEachTaggedEventId
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers
import com.vitorpamplona.quartz.nip01Core.verify
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.VerificationState
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex
@@ -184,9 +184,9 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.LargeCache
import com.vitorpamplona.quartz.utils.LargeSoftCache
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.cache.LargeCache
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException
@@ -298,18 +298,19 @@ object LocalCache : ILocalCache {
observablesByKindAndAuthor[event.kind]?.get(event.pubKey)?.updateIfMatches(event)
}
fun checkGetOrCreateUser(key: String): User? {
if (isValidHex(key)) {
return getOrCreateUser(key)
}
return null
}
fun checkGetOrCreateUser(key: String): User? = runCatching { getOrCreateUser(key) }.getOrNull()
fun load(keys: List<String>): List<User> = keys.mapNotNull(::checkGetOrCreateUser)
fun load(keys: Set<String>): Set<User> = keys.mapNotNullTo(mutableSetOf(), ::checkGetOrCreateUser)
fun getOrCreateUser(key: HexKey): User {
require(isValidHex(key = key)) { "$key is not a valid hex" }
return users.getOrCreate(key) {
User(it)
val nip65RelayListNote = getOrCreateAddressableNoteInternal(AdvertisedRelayListEvent.createAddress(key))
val dmRelayListNote = getOrCreateAddressableNoteInternal(ChatMessageRelayListEvent.createAddress(key))
User(it, nip65RelayListNote, dmRelayListNote)
}
}
@@ -413,11 +414,8 @@ object LocalCache : ILocalCache {
}
private fun isValidHex(key: String): Boolean {
if (key.isBlank()) return false
if (key.length != 64) return false
if (key.contains(":")) return false
return Hex.isHex(key)
return Hex.isHex64(key)
}
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? =
@@ -698,7 +696,7 @@ object LocalCache : ILocalCache {
if (isVerified || justVerify(event)) {
val replyTo = computeReplyTo(event)
if (event.createdAt > (note.createdAt() ?: 0)) {
if (event.createdAt > (note.createdAt() ?: 0L)) {
note.loadEvent(event, author, replyTo)
refreshNewNoteObservers(note)
@@ -741,7 +739,7 @@ object LocalCache : ILocalCache {
}
if (isVerified || justVerify(event)) {
if (event.createdAt > (note.createdAt() ?: 0)) {
if (event.createdAt > (note.createdAt() ?: 0L)) {
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
@@ -843,7 +841,7 @@ object LocalCache : ILocalCache {
if (note.event?.id == event.id) return false
if (event.createdAt > (note.createdAt() ?: 0) && (isVerified || justVerify(event))) {
if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
note.loadEvent(event, author, emptyList())
val channel = getOrCreateLiveChannel(note.address)
@@ -1042,13 +1040,13 @@ object LocalCache : ILocalCache {
event: VideoNormalEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
) = consumeRegularEvent(event, relay, wasVerified)
private fun consume(
event: VideoShortEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
) = consumeRegularEvent(event, relay, wasVerified)
fun consume(
event: StatusEvent,
@@ -1071,7 +1069,7 @@ object LocalCache : ILocalCache {
// Already processed this event.
if (note.event?.id == event.id) return false
if (event.createdAt > (note.createdAt() ?: 0) && (isVerified || justVerify(event))) {
if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
note.loadEvent(event, author, emptyList())
author.flowSet?.statuses?.invalidateData()
@@ -1085,7 +1083,7 @@ object LocalCache : ILocalCache {
}
fun consume(
event: RelationshipStatusEvent,
event: ContactCardEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
@@ -1194,7 +1192,7 @@ object LocalCache : ILocalCache {
// Already processed this event.
if (replaceableNote.event?.id == event.id) return isVerified
if (event.createdAt > (replaceableNote.createdAt() ?: 0) && (isVerified || justVerify(event))) {
if (event.createdAt > (replaceableNote.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
// clear index from previous tags
replaceableNote.replyTo?.forEach {
it.removeNote(replaceableNote)
@@ -1255,7 +1253,7 @@ object LocalCache : ILocalCache {
val deleteNoteEvent = deleteNote.event
if (deleteNoteEvent is AddressableEvent) {
val addressableNote = getAddressableNoteIfExists(deleteNoteEvent.addressTag())
if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0) <= event.createdAt) {
if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0L) <= event.createdAt) {
// Counts the replies
deleteNote(addressableNote)
@@ -1277,7 +1275,7 @@ object LocalCache : ILocalCache {
.mapNotNull { getAddressableNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) <= event.createdAt) {
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0L) <= event.createdAt) {
// Counts the replies
deleteNote(deleteNote)
@@ -1588,7 +1586,7 @@ object LocalCache : ILocalCache {
val isVerified =
if (event.createdAt > oldChannel.updatedMetadataAt) {
if (wasVerified || justVerify(event)) {
oldChannel.updateChannelInfo(author, event)
oldChannel.updateChannelInfo(author, event, note)
true
} else {
false
@@ -1840,15 +1838,15 @@ object LocalCache : ILocalCache {
note.addRelay(relay)
}
var isVerified =
val isVerified =
try {
val cachePath = Amethyst.instance.nip95cache
cachePath.mkdirs()
val file = File(cachePath, event.id)
if (!file.exists() && (wasVerified || justVerify(event))) {
val stream = FileOutputStream(file)
stream.write(event.decode())
stream.close()
FileOutputStream(file).use { stream ->
stream.write(event.decode())
}
Log.i(
"FileStorageEvent",
"NIP95 File received from $relay and saved to disk as $file",
@@ -1968,7 +1966,7 @@ object LocalCache : ILocalCache {
requestNote?.let { request -> zappedNote?.addZapPayment(request, note) }
GlobalScope.launch(Dispatchers.Default) {
GlobalScope.launch(Dispatchers.IO) {
responseCallback(event)
}
@@ -2132,7 +2130,9 @@ object LocalCache : ILocalCache {
}
}
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
fun getPeopleListNotesFor(user: User): List<AddressableNote> = addressables.filter(PeopleListEvent.KIND, user.pubkeyHex)
fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
checkNotInMainThread()
return addressables
@@ -2151,20 +2151,36 @@ object LocalCache : ILocalCache {
suspend fun findEarliestOtsForNote(
note: Note,
resolverBuilder: OtsResolverBuilder,
otsVerifCache: VerificationStateCache,
): Long? {
checkNotInMainThread()
var minTime: Long? = null
val time = TimeUtils.now()
notes.forEach { _, item ->
val noteEvent = item.event
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
(Amethyst.instance.otsVerifCache.cacheVerify(noteEvent, resolverBuilder) as? VerificationState.Verified)?.verifiedTime?.let { stampedTime ->
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
minTime = stampedTime
val candidates =
notes.mapNotNull { _, item ->
val noteEvent = item.event
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
val cachedTime = (otsVerifCache.justCache(noteEvent) as? VerificationState.Verified)?.verifiedTime
if (cachedTime != null) {
if (minTime == null || cachedTime < (minTime ?: Long.MAX_VALUE)) {
minTime = cachedTime
}
null
} else {
// tries to verify again
noteEvent
}
} else {
null
}
}
candidates.forEach { noteEvent ->
(otsVerifCache.cacheVerify(noteEvent) as? VerificationState.Verified)?.verifiedTime?.let { stampedTime ->
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
minTime = stampedTime
}
}
}
@@ -2176,7 +2192,7 @@ object LocalCache : ILocalCache {
fun cachedModificationEventsForNote(note: Note): List<Note>? = modificationCache[note.idHex]
suspend fun findLatestModificationForNote(note: Note): List<Note> {
fun findLatestModificationForNote(note: Note): List<Note> {
checkNotInMainThread()
val noteAuthor = note.author ?: return emptyList()
@@ -2201,9 +2217,17 @@ object LocalCache : ILocalCache {
}
fun cleanMemory() {
Log.d("LargeCache", "Notes cleanup started. Current size: ${notes.size()}")
notes.cleanUp()
Log.d("LargeCache", "Notes cleanup completed. Remaining size: ${notes.size()}")
Log.d("LargeCache", "Addressables cleanup started. Current size: ${addressables.size()}")
addressables.cleanUp()
Log.d("LargeCache", "Addressables cleanup completed. Remaining size: ${addressables.size()}")
Log.d("LargeCache", "Users cleanup started. Current size: ${users.size()}")
users.cleanUp()
Log.d("LargeCache", "Users cleanup completed. Remaining size: ${users.size()}")
}
fun cleanObservers() {
@@ -2330,9 +2354,9 @@ object LocalCache : ILocalCache {
val children =
if (noteEvent is WrappedEvent) {
noteEvent.host?.id?.let {
getNoteIfExists(it)?.let {
removeFromCache(it)
it.removeAllChildNotes()
getNoteIfExists(it)?.let { it2 ->
removeFromCache(it2)
it2.removeAllChildNotes()
}
}
} else {
@@ -2608,7 +2632,7 @@ object LocalCache : ILocalCache {
if (isDebug) {
Log.d("LocalCache", "Updating ${relay.url.url} with a Deletion Event ${event.id} ${deletionEvent.id} because of ${event.toJson()} with ${deletionEvent.toJson()}")
}
relay.send(deletionEvent)
relay.sendIfConnected(EventCmd(deletionEvent))
note.addRelay(relay.url)
}
}
@@ -2626,7 +2650,7 @@ object LocalCache : ILocalCache {
Log.d("LocalCache", "Updating ${relay.url.url} with a new version of ${event.kind} ${event.id} to ${existingEvent.id}")
}
relay.send(existingEvent)
relay.sendIfConnected(EventCmd(existingEvent))
// only send once.
note.addRelay(relay.url)
}
@@ -2812,7 +2836,7 @@ object LocalCache : ILocalCache {
is PeopleListEvent -> consume(event, relay, wasVerified)
is PollNoteEvent -> consume(event, relay, wasVerified)
is ReactionEvent -> consume(event, relay, wasVerified)
is RelationshipStatusEvent -> consume(event, relay, wasVerified)
is ContactCardEvent -> consume(event, relay, wasVerified)
is RelaySetEvent -> consume(event, relay, wasVerified)
is ReportEvent -> consume(event, relay, wasVerified)
is RepostEvent -> consume(event, relay, wasVerified)
@@ -2882,10 +2906,10 @@ class LocalCacheFlow {
val deletedEventBundles = _deletedEventBundles.asSharedFlow() // read-only public view
// Refreshes observers in batches.
private val bundler = BundledInsert<Note>(1000, Dispatchers.Default)
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
// Refreshes observers in batches.
private val bundler2 = BundledInsert<Note>(1000, Dispatchers.Default)
private val bundler2 = BundledInsert<Note>(1000, Dispatchers.IO)
fun newNote(newNote: Note) {
bundler.invalidateList(newNote) { bundledNewNotes ->

View File

@@ -20,12 +20,13 @@
*/
package com.vitorpamplona.amethyst.model
import android.util.Log
import androidx.compose.runtime.Stable
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 com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
@@ -160,7 +161,7 @@ fun parseResourceBundleOrNull(json: String): FhirElementDatabase? {
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return try {
val resource = mapper.readValue(json, Resource::class.java)
val resource = mapper.readValue<Resource>(json)
val db =
when (resource) {

View File

@@ -31,18 +31,17 @@ import com.vitorpamplona.amethyst.ui.note.toShortDisplay
import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue
import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.anyHashTag
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
@@ -72,7 +71,9 @@ import com.vitorpamplona.quartz.utils.containsAny
import com.vitorpamplona.quartz.utils.launchAndWaitAll
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import java.math.BigDecimal
@@ -101,14 +102,6 @@ class AddressableNote(
fun dTag(): String = address.dTag
override fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>,
deletionAddressables: Set<Address>,
): Boolean {
val thisEvent = event
return deletionAddressables.contains(address) || (thisEvent != null && deletionEvents.contains(thisEvent.id))
}
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl())
fun toATag() = ATag(address, relayHintUrl())
@@ -755,7 +748,7 @@ open class Note(
val dayAgo = TimeUtils.oneDayAgo()
return reports.isNotEmpty() ||
(
author?.reports?.any { it.value.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null }
author?.reports?.any { it.value.firstOrNull { (it.createdAt() ?: 0L) > dayAgo } != null }
?: false
)
}
@@ -787,14 +780,14 @@ open class Note(
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
return boosts.any {
it.author == loggedIn && (it.createdAt() ?: 0) > fiveMinsAgo
it.author == loggedIn && (it.createdAt() ?: 0L) > fiveMinsAgo
}
}
fun hasBoostedInTheLast5Minutes(loggedIn: HexKey): Boolean {
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
return boosts.any {
(it.createdAt() ?: 0) > fiveMinsAgo && it.author?.pubkeyHex == loggedIn
(it.createdAt() ?: 0L) > fiveMinsAgo && it.author?.pubkeyHex == loggedIn
}
}
@@ -911,14 +904,6 @@ open class Note(
}
}
open fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>,
deletionAddressables: Set<Address>,
): Boolean {
val thisEvent = event
return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.address()))
}
fun toETag(): ETag {
val noteEvent = event
return if (noteEvent != null) {
@@ -1011,3 +996,38 @@ class NoteBundledRefresherFlow(
class NoteState(
val note: Note,
)
fun List<AddressableNote>.eventIdSet() = mapNotNullTo(mutableSetOf<HexKey>()) { it.event?.id }
fun <T : Event> Array<NoteState>.events() = mapNotNull { it.note.event as? T }
fun <T : Event> List<AddressableNote>.events() = mapNotNull { it.event as? T }
fun <T : Event> List<AddressableNote>.updateFlow(): Flow<List<T>> =
if (this.isEmpty()) {
MutableStateFlow(emptyList())
} else {
combine(
flows = this.map { it.flow().metadata.stateFlow },
) {
it.events()
}
}
public inline fun <T> Iterable<Note>.anyEvent(predicate: (T) -> Boolean): Boolean {
if (this is Collection && isEmpty()) return false
for (note in this) {
val noteEvent = note.event as? T
if (noteEvent != null && predicate(noteEvent)) return true
}
return false
}
public inline fun Iterable<Note>.anyNotNullEvent(predicate: (Event) -> Boolean): Boolean {
if (this is Collection && isEmpty()) return false
for (note in this) {
val noteEvent = note.event
if (noteEvent != null && predicate(noteEvent)) return true
}
return false
}

View File

@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import kotlinx.collections.immutable.ImmutableSet

View File

@@ -22,9 +22,11 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.R
import kotlinx.serialization.Serializable
@Stable
data class Settings(
@Serializable
data class UiSettings(
val theme: ThemeType = ThemeType.SYSTEM,
val preferredLanguage: String? = null,
val automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2025 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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
class UiSettingsFlow(
val theme: MutableStateFlow<ThemeType> = MutableStateFlow(ThemeType.SYSTEM),
val preferredLanguage: MutableStateFlow<String?> = MutableStateFlow(null),
val automaticallyShowImages: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
val automaticallyStartPlayback: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
val automaticallyShowUrlPreview: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
val automaticallyHideNavigationBars: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
val automaticallyShowProfilePictures: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
val dontShowPushNotificationSelector: MutableStateFlow<Boolean> = MutableStateFlow(false),
val dontAskForNotificationPermissions: MutableStateFlow<Boolean> = MutableStateFlow(false),
val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED),
val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC),
) {
// emits at every change in any of the propertyes.
val propertyWatchFlow =
combine(
listOf(
theme,
preferredLanguage,
automaticallyShowImages,
automaticallyStartPlayback,
automaticallyShowUrlPreview,
automaticallyHideNavigationBars,
automaticallyShowProfilePictures,
dontShowPushNotificationSelector,
dontAskForNotificationPermissions,
featureSet,
gallerySet,
),
) { flows ->
UiSettings(
flows[0] as ThemeType,
flows[1] as String?,
flows[2] as ConnectivityType,
flows[3] as ConnectivityType,
flows[4] as ConnectivityType,
flows[5] as BooleanType,
flows[6] as ConnectivityType,
flows[7] as Boolean,
flows[8] as Boolean,
flows[9] as FeatureSetType,
flows[10] as ProfileGalleryType,
)
}
fun toSettings(): UiSettings =
UiSettings(
theme.value,
preferredLanguage.value,
automaticallyShowImages.value,
automaticallyStartPlayback.value,
automaticallyShowUrlPreview.value,
automaticallyHideNavigationBars.value,
automaticallyShowProfilePictures.value,
dontShowPushNotificationSelector.value,
dontAskForNotificationPermissions.value,
featureSet.value,
gallerySet.value,
)
fun update(torSettings: UiSettings): Boolean {
var any = false
if (theme.value != torSettings.theme) {
theme.tryEmit(torSettings.theme)
any = true
}
if (preferredLanguage.value != torSettings.preferredLanguage) {
preferredLanguage.tryEmit(torSettings.preferredLanguage)
any = true
}
if (automaticallyShowImages.value != torSettings.automaticallyShowImages) {
automaticallyShowImages.tryEmit(torSettings.automaticallyShowImages)
any = true
}
if (automaticallyStartPlayback.value != torSettings.automaticallyStartPlayback) {
automaticallyStartPlayback.tryEmit(torSettings.automaticallyStartPlayback)
any = true
}
if (automaticallyShowUrlPreview.value != torSettings.automaticallyShowUrlPreview) {
automaticallyShowUrlPreview.tryEmit(torSettings.automaticallyShowUrlPreview)
any = true
}
if (automaticallyHideNavigationBars.value != torSettings.automaticallyHideNavigationBars) {
automaticallyHideNavigationBars.tryEmit(torSettings.automaticallyHideNavigationBars)
any = true
}
if (automaticallyShowProfilePictures.value != torSettings.automaticallyShowProfilePictures) {
automaticallyShowProfilePictures.tryEmit(torSettings.automaticallyShowProfilePictures)
any = true
}
if (dontShowPushNotificationSelector.value != torSettings.dontShowPushNotificationSelector) {
dontShowPushNotificationSelector.tryEmit(torSettings.dontShowPushNotificationSelector)
any = true
}
if (dontAskForNotificationPermissions.value != torSettings.dontAskForNotificationPermissions) {
dontAskForNotificationPermissions.tryEmit(torSettings.dontAskForNotificationPermissions)
any = true
}
if (featureSet.value != torSettings.featureSet) {
featureSet.tryEmit(torSettings.featureSet)
any = true
}
if (gallerySet.value != torSettings.gallerySet) {
gallerySet.tryEmit(torSettings.gallerySet)
any = true
}
return any
}
fun dontShowPushNotificationSelector() {
if (!dontShowPushNotificationSelector.value) {
dontShowPushNotificationSelector.tryEmit(true)
}
}
fun dontAskForNotificationPermissions() {
if (!dontAskForNotificationPermissions.value) {
dontAskForNotificationPermissions.tryEmit(true)
}
}
companion object {
fun build(torSettings: UiSettings): UiSettingsFlow =
UiSettingsFlow(
MutableStateFlow(torSettings.theme),
MutableStateFlow(torSettings.preferredLanguage),
MutableStateFlow(torSettings.automaticallyShowImages),
MutableStateFlow(torSettings.automaticallyStartPlayback),
MutableStateFlow(torSettings.automaticallyShowUrlPreview),
MutableStateFlow(torSettings.automaticallyHideNavigationBars),
MutableStateFlow(torSettings.automaticallyShowProfilePictures),
MutableStateFlow(torSettings.dontShowPushNotificationSelector),
MutableStateFlow(torSettings.dontAskForNotificationPermissions),
MutableStateFlow(torSettings.featureSet),
MutableStateFlow(torSettings.gallerySet),
)
}
}

View File

@@ -25,6 +25,7 @@ import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
import com.vitorpamplona.quartz.lightning.Lud06
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -33,7 +34,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.toNpub
@@ -50,6 +50,8 @@ import java.math.BigDecimal
@Stable
class User(
val pubkeyHex: String,
val nip65RelayListNote: Note,
val dmRelayListNote: Note,
) {
var info: UserMetadata? = null
@@ -72,9 +74,9 @@ class User(
fun pubkeyDisplayHex() = pubkeyNpub().toShortDisplay()
fun dmInboxRelayList() = (LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(pubkeyHex))?.event as? ChatMessageRelayListEvent)
fun dmInboxRelayList() = dmRelayListNote.event as? ChatMessageRelayListEvent
fun authorRelayList() = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)
fun authorRelayList() = nip65RelayListNote.event as? AdvertisedRelayListEvent
fun toNProfile() = NProfile.create(pubkeyHex, relayHints())
@@ -139,8 +141,6 @@ class User(
?.followers
?.invalidateData()
}
flowSet?.relays?.invalidateData()
}
fun addReport(note: Note) {
@@ -227,7 +227,7 @@ class User(
here.counter++
}
flowSet?.relayInfo?.invalidateData()
flowSet?.usedRelays?.invalidateData()
}
fun updateUserInfo(
@@ -339,20 +339,18 @@ class UserFlowSet(
// Observers line up here.
val metadata = UserBundledRefresherFlow(u)
val follows = UserBundledRefresherFlow(u)
val relays = UserBundledRefresherFlow(u)
val followers = UserBundledRefresherFlow(u)
val reports = UserBundledRefresherFlow(u)
val relayInfo = UserBundledRefresherFlow(u)
val usedRelays = UserBundledRefresherFlow(u)
val zaps = UserBundledRefresherFlow(u)
val statuses = UserBundledRefresherFlow(u)
fun isInUse(): Boolean =
metadata.hasObservers() ||
relays.hasObservers() ||
follows.hasObservers() ||
followers.hasObservers() ||
reports.hasObservers() ||
relayInfo.hasObservers() ||
usedRelays.hasObservers() ||
zaps.hasObservers() ||
statuses.hasObservers()
}

View File

@@ -0,0 +1,121 @@
/**
* Copyright (c) 2025 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.accountsCache
import android.content.ContentResolver
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
class AccountCacheState(
val geolocationFlow: StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val contentResolverFn: () -> ContentResolver,
val otsResolverBuilder: OtsResolverBuilder,
val cache: LocalCache,
val client: INostrClient,
) {
val accounts = MutableStateFlow<Map<HexKey, Account>>(emptyMap())
fun removeAccount(pubkey: HexKey) {
accounts.update { existingAccounts ->
val oldValue = existingAccounts[pubkey]
oldValue?.scope?.cancel()
existingAccounts.minus(pubkey)
}
}
fun loadAccount(accountSettings: AccountSettings): Account =
loadAccount(
signer =
if (accountSettings.keyPair.privKey != null) {
NostrSignerInternal(accountSettings.keyPair)
} else {
when (val packageName = accountSettings.externalSignerPackageName) {
null -> NostrSignerInternal(accountSettings.keyPair)
else ->
NostrSignerExternal(
pubKey = accountSettings.keyPair.pubKey.toHexKey(),
packageName = packageName,
contentResolver = contentResolverFn(),
)
}
},
accountSettings = accountSettings,
)
fun loadAccount(
signer: NostrSigner,
accountSettings: AccountSettings,
): Account {
val cached = accounts.value[signer.pubKey]
if (cached != null) return cached
return Account(
settings = accountSettings,
signer = signer,
geolocationFlow = geolocationFlow,
nwcFilterAssembler = nwcFilterAssembler,
otsResolverBuilder = otsResolverBuilder,
cache = cache,
client = client,
scope =
CoroutineScope(
Dispatchers.IO +
SupervisorJob() +
CoroutineExceptionHandler { _, throwable ->
Log.e("AccountCacheState", "Account ${signer.pubKey} caught exception: ${throwable.message}", throwable)
},
),
).also { newAccount ->
accounts.update { existingAccounts ->
existingAccounts.plus(Pair(signer.pubKey, newAccount))
}
}
}
fun clear() {
accounts.update { existingAccounts ->
existingAccounts.forEach {
it.value.scope.cancel()
}
emptyMap()
}
}
}

View File

@@ -20,15 +20,14 @@
*/
package com.vitorpamplona.amethyst.model.edits
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -48,13 +47,14 @@ class PrivateStorageRelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val privateOutboxListNote = cache.getOrCreateAddressableNote(getPrivateOutboxRelayListAddress())
fun getPrivateOutboxRelayListAddress() = PrivateOutboxRelayListEvent.createAddress(signer.pubKey)
fun getPrivateOutboxRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getPrivateOutboxRelayListAddress())
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = privateOutboxListNote.flow().metadata.stateFlow
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = getPrivateOutboxRelayListNote().flow().metadata.stateFlow
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = privateOutboxListNote.event as? PrivateOutboxRelayListEvent
suspend fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList
@@ -65,8 +65,8 @@ class PrivateStorageRelayListState(
getPrivateOutboxRelayListFlow()
.map { normalizePrivateOutboxRelayListWithBackup(it.note) }
.onStart {
emit(normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()))
}.flowOn(Dispatchers.Default)
emit(normalizePrivateOutboxRelayListWithBackup(privateOutboxListNote))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -99,7 +99,7 @@ class PrivateStorageRelayListState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start")
getPrivateOutboxRelayListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}")

View File

@@ -20,15 +20,14 @@
*/
package com.vitorpamplona.amethyst.model.emphChat
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -49,13 +48,14 @@ class EphemeralChatListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val ephemeralChatListNote = cache.getOrCreateAddressableNote(getEphemeralChatListAddress())
fun getEphemeralChatListAddress() = EphemeralChatListEvent.createAddress(signer.pubKey)
fun getEphemeralChatListNote(): AddressableNote = cache.getOrCreateAddressableNote(getEphemeralChatListAddress())
fun getEphemeralChatListFlow(): StateFlow<NoteState> = ephemeralChatListNote.flow().metadata.stateFlow
fun getEphemeralChatListFlow(): StateFlow<NoteState> = getEphemeralChatListNote().flow().metadata.stateFlow
fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent
fun getEphemeralChatList(): EphemeralChatListEvent? = ephemeralChatListNote.event as? EphemeralChatListEvent
suspend fun ephemeralChatListWithBackup(note: Note): Set<RoomId> {
val event = note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList
@@ -68,8 +68,8 @@ class EphemeralChatListState(
.transformLatest { noteState ->
emit(ephemeralChatListWithBackup(noteState.note))
}.onStart {
emit(ephemeralChatListWithBackup(getEphemeralChatListNote()))
}.flowOn(Dispatchers.Default)
emit(ephemeralChatListWithBackup(ephemeralChatListNote))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -117,7 +117,7 @@ class EphemeralChatListState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start")
getEphemeralChatListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "EphemeralChatList List for ${signer.pubKey}")

View File

@@ -43,7 +43,7 @@ class LocalRelayListState(
val flow =
settings.localRelayServers
.map { normalizeLocalRelayListWithBackup(it) }
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -43,7 +43,7 @@ class AccountHomeRelayState(
local.flow,
) { nip65Outbox, privateOutBox, localRelays ->
nip65Outbox + privateOutBox + localRelays
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -46,7 +46,7 @@ class AccountOutboxRelayState(
broadcast.flow,
) { nip65Outbox, privateOutBox, localRelays, broadcastRelays ->
nip65Outbox + privateOutBox + localRelays + broadcastRelays
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -40,7 +40,7 @@ class NotificationInboxRelayState(
localRelayList.flow,
) { nip65Inbox, localRelays ->
nip65Inbox + localRelays
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -20,13 +20,12 @@
*/
package com.vitorpamplona.amethyst.model.nip01UserMetadata
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -40,13 +39,14 @@ class UserMetadataState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val user = cache.getOrCreateUser(signer.pubKey)
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
fun getUserMetadataUser(): User = cache.getOrCreateUser(signer.pubKey)
fun getUserMetadataFlow(): StateFlow<UserState> = user.flow().metadata.stateFlow
fun getUserMetadataFlow(): StateFlow<UserState> = getUserMetadataUser().flow().metadata.stateFlow
fun getUserMetadataEvent(): MetadataEvent? = getUserMetadataUser().latestMetadata
fun getUserMetadataEvent(): MetadataEvent? = user.latestMetadata
suspend fun sendNewUserMetadata(
name: String? = null,
@@ -112,7 +112,7 @@ class UserMetadataState(
}
// saves contact list for the next time.
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
getUserMetadataFlow().collect {
Log.d("AccountRegisterObservers", "Updating Kind 0 ${it.user.toBestDisplayName()}")

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2025 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.nip02FollowLists
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
class DeclaredFollowsPerOutboxRelay(
kind3Follows: Kind3FollowListState,
val cache: LocalCache,
scope: CoroutineScope,
) {
val calculator = OutboxRelayLoader(true)
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
kind3Follows.flow
.transformLatest {
emitAll(
calculator.toAuthorsPerRelayFlow(it.authors, cache) { it },
)
}.onStart {
emit(
calculator.authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it },
)
}.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyMap(),
)
}

View File

@@ -20,18 +20,14 @@
*/
package com.vitorpamplona.amethyst.model.nip02FollowLists
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -44,49 +40,27 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlin.collections.flatten
class FollowListOutboxOrProxyRelays(
kind3Follows: FollowListState,
kind3Follows: Kind3FollowListState,
blockedRelayList: BlockedRelayListState,
proxyRelayList: ProxyRelayListState,
val cache: LocalCache,
scope: CoroutineScope,
) {
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey)
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
fun combineAllFlows(flows: List<StateFlow<NoteState>>): Flow<Set<NormalizedRelayUrl>> =
combine(flows) { relayListNotes: Array<NoteState> ->
relayListNotes
.mapNotNull {
(it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()
}.flatten()
.toSet()
}
@OptIn(ExperimentalCoroutinesApi::class)
val outboxRelayFlow: StateFlow<Set<NormalizedRelayUrl>> =
kind3Follows.flow
.transformLatest {
emitAll(combineAllFlows(allRelayListFlows(it.authors)))
.transformLatest { follows ->
emitAll(
OutboxRelayLoader(true).toAuthorsPerRelayFlow(follows.authors, cache) { it.keys },
)
}.onStart {
emit(
kind3Follows.flow.value.authors
.mapNotNull {
getNIP65RelayList(it)?.writeRelaysNorm()
}.flatten()
.toSet(),
OutboxRelayLoader(true).authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it.keys },
)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -98,7 +72,7 @@ class FollowListOutboxOrProxyRelays(
followList.minus(blockedRelays)
}.onStart {
emit(outboxRelayFlow.value.minus(blockedRelayList.flow.value.toSet()))
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -120,7 +94,7 @@ class FollowListOutboxOrProxyRelays(
outboxRelayMinusBlockedFlow.value
},
)
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -134,7 +108,7 @@ class FollowListOutboxOrProxyRelays(
relayList.mapTo(mutableSetOf()) { it.url }
}.onStart {
emit(flow.value.mapTo(mutableSetOf()) { it.url })
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -0,0 +1,135 @@
/**
* Copyright (c) 2025 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.nip02FollowLists
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
class FollowListReusedOutboxOrProxyRelays(
kind3Follows: Kind3FollowListState,
blockedRelayList: BlockedRelayListState,
proxyRelayList: ProxyRelayListState,
val cache: LocalCache,
scope: CoroutineScope,
) {
@OptIn(ExperimentalCoroutinesApi::class)
val outboxRelayFlow: StateFlow<Set<NormalizedRelayUrl>> =
kind3Follows.flow
.transformLatest { follows ->
emitAll(
OutboxRelayLoader(true).toAuthorsPerRelayFlow(follows.authors, cache) { authorsPerRelay ->
authorsPerRelay
.mapNotNull {
if (it.value.size > 1) {
it.key
} else {
null
}
}.toSet()
},
)
}.onStart {
emit(
OutboxRelayLoader(true).authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { authorsPerRelay ->
authorsPerRelay
.mapNotNull {
if (it.value.size > 1) {
it.key
} else {
null
}
}.toSet()
},
)
}.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
val outboxRelayMinusBlockedFlow: StateFlow<Set<NormalizedRelayUrl>> =
combine(outboxRelayFlow, blockedRelayList.flow) { followList, blockedRelays ->
followList.minus(blockedRelays)
}.onStart {
emit(outboxRelayFlow.value.minus(blockedRelayList.flow.value.toSet()))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<Set<NormalizedRelayUrl>> =
proxyRelayList.flow
.flatMapLatest { proxyRelays ->
if (proxyRelays.isEmpty()) {
outboxRelayMinusBlockedFlow
} else {
MutableStateFlow(proxyRelays)
}
}.onStart {
emit(
proxyRelayList.flow.value.ifEmpty {
outboxRelayMinusBlockedFlow.value
},
)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val flowSet: StateFlow<Set<String>> =
flow
.map { relayList ->
relayList.mapTo(mutableSetOf()) { it.url }
}.onStart {
emit(flow.value.mapTo(mutableSetOf()) { it.url })
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
}

View File

@@ -20,20 +20,15 @@
*/
package com.vitorpamplona.amethyst.model.nip02FollowLists
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@@ -45,63 +40,27 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlin.collections.ifEmpty
class FollowsPerOutboxRelay(
kind3Follows: FollowListState,
kind3Follows: Kind3FollowListState,
blockedRelayList: BlockedRelayListState,
proxyRelayList: ProxyRelayListState,
val cache: LocalCache,
scope: CoroutineScope,
) {
fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey)
fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey))
fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow<NoteState> = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow
fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent
fun allRelayListFlows(followList: Set<HexKey>): List<StateFlow<NoteState>> = followList.map { getNIP65RelayListFlow(it) }
fun combineAllRelayListFlows(flows: List<StateFlow<NoteState>>): Flow<Map<NormalizedRelayUrl, Set<HexKey>>> =
combine(flows) { relayListNotes: Array<NoteState> ->
mapOfSet {
relayListNotes.forEach { noteState ->
noteState.note.author?.pubkeyHex?.let { authorHex ->
val outboxRelayList =
getNIP65RelayList(authorHex)?.writeRelaysNorm()
?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null }
?: Constants.eventFinderRelays
outboxRelayList.forEach { relay ->
add(relay, authorHex)
}
}
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
val outboxPerRelayFlow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
kind3Follows.flow
.transformLatest {
emitAll(combineAllRelayListFlows(allRelayListFlows(it.authors)))
emitAll(
OutboxRelayLoader().toAuthorsPerRelayFlow(it.authors, cache) { it },
)
}.onStart {
emit(
mapOfSet {
kind3Follows.flow.value.authors.map { authorHex ->
val outboxRelayList =
getNIP65RelayList(authorHex)?.writeRelaysNorm()
?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null }
?: Constants.eventFinderRelays
outboxRelayList.forEach { relay ->
add(relay, authorHex)
}
}
},
OutboxRelayLoader().authorsPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it },
)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -113,7 +72,7 @@ class FollowsPerOutboxRelay(
followList.minus(blockedRelays)
}.onStart {
emit(outboxPerRelayFlow.value.minus(blockedRelayList.flow.value.toSet()))
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -137,7 +96,7 @@ class FollowsPerOutboxRelay(
} else {
emit(proxyRelayList.flow.value.associateWith { kind3Follows.flow.value.authors })
}
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -20,44 +20,41 @@
*/
package com.vitorpamplona.amethyst.model.nip02FollowLists
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
class FollowListState(
class Kind3FollowListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val user = cache.getOrCreateUser(signer.pubKey)
fun getFollowListUser(): User = cache.getOrCreateUser(signer.pubKey)
fun getFollowListFlow(): StateFlow<UserState> = user.flow().follows.stateFlow
fun getFollowListFlow(): StateFlow<UserState> = getFollowListUser().flow().follows.stateFlow
fun getFollowListEvent(): ContactListEvent? = getFollowListUser().latestContactList
fun getFollowListEvent(): ContactListEvent? = user.latestContactList
@OptIn(ExperimentalCoroutinesApi::class)
private val innerFlow: Flow<Kind3Follows> =
@@ -67,7 +64,7 @@ class FollowListState(
val flow =
innerFlow
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -75,20 +72,31 @@ class FollowListState(
buildKind3Follows(getFollowListEvent() ?: settings.backupContactList),
)
// Creates a long-term reference for all follows of a user
val userList =
flow
.map { kind3Follows ->
kind3Follows.authors.mapNotNull {
cache.checkGetOrCreateUser(it)
}
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
// this has priority.
flow.value.authors.mapNotNull {
cache.checkGetOrCreateUser(it)
},
)
/**
This contains a big OR of everything the user wants to see in the a single feed.
*/
@Immutable
class Kind3Follows(
val authors: Set<String> = emptySet(),
val authorsPlusMe: Set<String>,
val hashtags: Set<String> = emptySet(),
val geotags: Set<String> = emptySet(),
val communities: Set<String> = emptySet(),
) {
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
}
val authors: Set<HexKey> = emptySet(),
val authorsPlusMe: Set<HexKey>,
)
fun buildKind3Follows(latestContactList: ContactListEvent?): Kind3Follows {
// makes sure the output include only valid p tags
@@ -97,19 +105,6 @@ class FollowListState(
return Kind3Follows(
authors = verifiedFollowingUsers,
authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
hashtags =
latestContactList
?.unverifiedFollowTagSet()
?.map { it.lowercase() }
?.toSet() ?: emptySet(),
geotags =
latestContactList
?.geohashes()
?.toSet() ?: emptySet(),
communities =
latestContactList
?.verifiedFollowAddressSet()
?.toSet() ?: emptySet(),
)
}
@@ -143,14 +138,14 @@ class FollowListState(
init {
settings.backupContactList?.let {
Log.d("AccountRegisterObservers", "Loading saved contacts ${it.toJson()}")
Log.d("AccountRegisterObservers", "Loading saved ${it.tags.size} contacts")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
// saves contact list for the next time.
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
getFollowListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}")

View File

@@ -18,40 +18,42 @@
* 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.quartz.nip01Core.relay.client.accessories
package com.vitorpamplona.amethyst.model.nip03Timestamp
import android.util.Log
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
class RelayAuthenticator(
val client: NostrClient,
val scope: CoroutineScope,
val authenticate: suspend (challenge: String, relay: IRelayClient) -> Unit,
class IncomingOtsEventVerifier(
private val otsVerifCache: VerificationStateCache,
private val cache: LocalCache,
private val scope: CoroutineScope,
) {
private val clientListener =
object : IRelayClientListener {
override fun onAuth(
relay: IRelayClient,
challenge: String,
) {
scope.launch {
authenticate(challenge, relay)
val verifying =
cache.live.newEventBundles
.onEach { newNotes ->
newNotes.forEach {
consume(it)
}
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
null,
)
suspend fun consume(note: Note) {
note.event?.let { event ->
if (event is OtsEvent) {
otsVerifCache.cacheVerify(event)
}
}
init {
Log.d("${this.javaClass.simpleName}", "Init, Subscribe")
client.subscribe(clientListener)
}
fun destroy() {
// makes sure to run
Log.d("${this.javaClass.simpleName}", "Destroy, Unsubscribe")
client.unsubscribe(clientListener)
}
}

View File

@@ -20,25 +20,40 @@
*/
package com.vitorpamplona.amethyst.model.nip03Timestamp
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.Base64
class OtsState(
val signer: NostrSigner,
val cache: LocalCache,
val otsResolver: OkHttpOtsResolverBuilder,
val otsResolver: OtsResolverBuilder,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
var lastTimeItTriedToUpdateAttestations: Long = 0
fun upgradeAttestationsIfNeeded(onReady: (List<OtsEvent>) -> Unit) {
// only tries to upgrade every hour
val now = TimeUtils.now()
if (now - lastTimeItTriedToUpdateAttestations > TimeUtils.ONE_HOUR) {
lastTimeItTriedToUpdateAttestations = now
scope.launch {
onReady(updateAttestations())
}
}
}
suspend fun updateAttestations(): List<OtsEvent> {
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
@@ -70,7 +85,7 @@ class OtsState(
return settings.pendingAttestations.value[id] != null
}
fun timestamp(note: Note) {
suspend fun timestamp(note: Note) {
if (note.isDraft()) return
val id = note.event?.id ?: note.idHex

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2025 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.nip03Timestamp
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpCalendar
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
import okhttp3.OkHttpClient
class TorAwareOkHttpOtsResolverBuilder(
val okHttpClient: (url: String) -> OkHttpClient,
val isTorActive: (url: String) -> Boolean,
val cache: OtsBlockHeightCache,
) : OtsResolverBuilder {
fun getAPI(usingTor: Boolean) =
if (usingTor) {
OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL
} else {
OkHttpBitcoinExplorer.Companion.BLOCKSTREAM_API_URL
}
override fun build(): OtsResolver =
OtsResolver(
explorer =
OkHttpBitcoinExplorer(
baseUrl = {
getAPI(usingTor = isTorActive(OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL))
},
client = okHttpClient(OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL),
cache = cache,
),
calendar = OkHttpCalendar(okHttpClient),
)
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2025 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.nip11RelayInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import com.vitorpamplona.quartz.utils.Log
@Composable
fun loadRelayInfo(relay: NormalizedRelayUrl): State<Nip11RelayInformation> = loadRelayInfo(relay, Amethyst.instance.nip11Cache)
@Composable
fun loadRelayInfo(
relay: NormalizedRelayUrl,
nip11Cache: Nip11CachedRetriever,
): State<Nip11RelayInformation> =
produceState(
nip11Cache.getFromCache(relay),
relay,
) {
nip11Cache.loadRelayInfo(
relay = relay,
onInfo = {
value = it
},
onError = { url, errorCode, exceptionMessage ->
Log.e("RelayInfo", "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage")
},
)
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2025 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.nip11RelayInfo
import android.util.LruCache
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import okhttp3.OkHttpClient
class Nip11CachedRetriever(
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
) {
private val relayInformationEmptyCache = LruCache<NormalizedRelayUrl, Nip11RelayInformation>(1000)
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(1000)
private val retriever = Nip11Retriever(okHttpClient)
fun getEmpty(relay: NormalizedRelayUrl): Nip11RelayInformation {
relayInformationEmptyCache.get(relay)?.let { return it }
val info =
Nip11RelayInformation(
name = relay.displayUrl(),
icon = relay.toHttp() + "favicon.ico",
)
relayInformationEmptyCache.put(relay, info)
return info
}
fun getFromCache(relay: NormalizedRelayUrl): Nip11RelayInformation {
val result = relayInformationDocumentCache.get(relay)
if (result == null) {
// resets the clock
val empty = getEmpty(relay)
relayInformationDocumentCache.put(relay, RetrieveResult.Empty(empty))
return empty
}
return when (result) {
is RetrieveResult.Success -> result.data
is RetrieveResult.Error -> result.data
is RetrieveResult.Empty -> result.data
is RetrieveResult.Loading -> result.data
}
}
suspend fun loadRelayInfo(
relay: NormalizedRelayUrl,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
) {
val doc = relayInformationDocumentCache.get(relay)
if (doc != null) {
when (doc) {
is RetrieveResult.Success -> onInfo(doc.data)
is RetrieveResult.Loading -> {
if (doc.isValid()) {
// just wait.
} else {
retrieve(relay, onInfo, onError)
}
}
is RetrieveResult.Error -> {
if (doc.isValid()) {
onError(relay, doc.error, null)
} else {
retrieve(relay, onInfo, onError)
}
}
is RetrieveResult.Empty -> retrieve(relay, onInfo, onError)
}
} else {
retrieve(relay, onInfo, onError)
}
}
private suspend fun retrieve(
relay: NormalizedRelayUrl,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
) {
relayInformationDocumentCache.put(relay, RetrieveResult.Loading(getEmpty(relay)))
retriever.loadRelayInfo(
relay = relay,
onInfo = {
relayInformationDocumentCache.put(relay, RetrieveResult.Success(it))
relayInformationEmptyCache.remove(relay)
onInfo(it)
},
onError = { relay, code, errorMsg ->
relayInformationDocumentCache.put(relay, RetrieveResult.Error(getEmpty(relay), code, errorMsg))
relayInformationEmptyCache.remove(relay)
onError(relay, code, errorMsg)
},
)
}
}

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 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.nip11RelayInfo
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.coroutines.executeAsync
class Nip11Retriever(
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
) {
enum class ErrorCode {
FAIL_TO_ASSEMBLE_URL,
FAIL_TO_REACH_SERVER,
FAIL_TO_PARSE_RESULT,
FAIL_WITH_HTTP_STATUS,
}
suspend fun loadRelayInfo(
relay: NormalizedRelayUrl,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (NormalizedRelayUrl, ErrorCode, String?) -> Unit,
) {
val url = relay.toHttp()
try {
val request: Request =
Request
.Builder()
.header("Accept", "application/nostr+json")
.url(url)
.build()
val client = okHttpClient(relay)
client.newCall(request).executeAsync().use { response ->
withContext(Dispatchers.IO) {
val body = response.body.string()
try {
if (response.isSuccessful) {
if (body.startsWith("{")) {
onInfo(Nip11RelayInformation.fromJson(body))
} else {
onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, body)
}
} else {
onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, response.code.toString())
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e(
"RelayInfoFail",
"Resulting Message from Relay ${relay.url} in not parseable: $body",
e,
)
onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, e.message)
}
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("RelayInfoFail", "Invalid URL ${relay.url}", e)
onError(relay, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
}
}
}

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2025 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.nip11RelayInfo
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import com.vitorpamplona.quartz.utils.TimeUtils
sealed class RetrieveResult(
val data: Nip11RelayInformation,
val time: Long,
) {
class Error(
data: Nip11RelayInformation,
val error: Nip11Retriever.ErrorCode,
val msg: String? = null,
) : RetrieveResult(data, TimeUtils.now())
class Success(
data: Nip11RelayInformation,
) : RetrieveResult(data, TimeUtils.now())
class Loading(
data: Nip11RelayInformation,
) : RetrieveResult(data, TimeUtils.now())
class Empty(
data: Nip11RelayInformation,
) : RetrieveResult(data, TimeUtils.now())
fun isValid() = time > TimeUtils.oneHourAgo()
}

View File

@@ -47,7 +47,7 @@ class DmInboxRelayState(
localRelayList.flow,
) { nip65Inbox, dmRelayList, privateOutBox, localRelays ->
nip65Inbox + dmRelayList + privateOutBox + localRelays
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -20,15 +20,14 @@
*/
package com.vitorpamplona.amethyst.model.nip17Dms
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -47,13 +46,14 @@ class DmRelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val dmListNote = cache.getOrCreateAddressableNote(getDMRelayListAddress())
fun getDMRelayListAddress() = ChatMessageRelayListEvent.createAddress(signer.pubKey)
fun getDMRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getDMRelayListAddress())
fun getDMRelayListFlow(): StateFlow<NoteState> = dmListNote.flow().metadata.stateFlow
fun getDMRelayListFlow(): StateFlow<NoteState> = getDMRelayListNote().flow().metadata.stateFlow
fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent
fun getDMRelayList(): ChatMessageRelayListEvent? = dmListNote.event as? ChatMessageRelayListEvent
fun normalizeDMRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList
@@ -63,8 +63,8 @@ class DmRelayListState(
val flow =
getDMRelayListFlow()
.map { normalizeDMRelayListWithBackup(it.note) }
.onStart { emit(normalizeDMRelayListWithBackup(getDMRelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeDMRelayListWithBackup(dmListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -96,7 +96,7 @@ class DmRelayListState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start")
getDMRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating DM Relay List for ${signer.pubKey}")

View File

@@ -25,10 +25,10 @@ import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
@@ -40,7 +40,10 @@ class PublicChatChannel(
) : Channel() {
var creator: User? = null
var event: ChannelCreateEvent? = null
var eventNote: Note? = null
// Important to keep this long-term reference because LocalCache uses WeakReferences.
var creationEventNote: Note? = null
var updateEventNote: Note? = null
var info = ChannelDataNorm(null, null, null, null)
var infoTags = EmptyTagList
@@ -71,7 +74,7 @@ class PublicChatChannel(
this.infoTags = event.tags.toImmutableListOfLists()
this.updatedMetadataAt = event.createdAt
this.eventNote = eventNote
this.creationEventNote = eventNote
updateChannelInfo()
}
@@ -79,12 +82,14 @@ class PublicChatChannel(
fun updateChannelInfo(
creator: User,
event: ChannelMetadataEvent,
eventNote: Note? = null,
) {
this.creator = creator
this.info = event.channelInfo()
this.infoTags = event.tags.toImmutableListOfLists()
this.updatedMetadataAt = event.createdAt
this.updateEventNote = eventNote
super.updateChannelInfo()
}

View File

@@ -20,9 +20,7 @@
*/
package com.vitorpamplona.amethyst.model.nip28PublicChats
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
@@ -30,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -51,13 +50,14 @@ class PublicChatListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val publicChatListNote = cache.getOrCreateAddressableNote(getChannelListAddress())
fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey)
fun getChannelListNote(): AddressableNote = cache.getOrCreateAddressableNote(getChannelListAddress())
fun getChannelListFlow(): StateFlow<NoteState> = publicChatListNote.flow().metadata.stateFlow
fun getChannelListFlow(): StateFlow<NoteState> = getChannelListNote().flow().metadata.stateFlow
fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent
fun getChannelList(): ChannelListEvent? = publicChatListNote.event as? ChannelListEvent
suspend fun publicChatListWithBackup(note: Note): Set<ChannelTag> {
val event = note.event as? ChannelListEvent ?: settings.backupChannelList
@@ -70,8 +70,8 @@ class PublicChatListState(
.transformLatest { noteState ->
emit(publicChatListWithBackup(noteState.note))
}.onStart {
emit(publicChatListWithBackup(getChannelListNote()))
}.flowOn(Dispatchers.Default)
emit(publicChatListWithBackup(publicChatListNote))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -85,7 +85,7 @@ class PublicChatListState(
it.mapTo(mutableSetOf()) { it.eventId }
}.onStart {
emit(flow.value.mapTo(mutableSetOf()) { it.eventId })
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -132,7 +132,7 @@ class PublicChatListState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Channel List Collector Start")
getChannelListFlow().collect {
Log.d("AccountRegisterObservers", "Channel List for ${signer.pubKey}")

View File

@@ -21,12 +21,11 @@
package com.vitorpamplona.amethyst.model.nip30CustomEmojis
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis
@@ -52,13 +51,14 @@ class EmojiPackState(
val link: MediaUrlImage,
)
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val emojiPackListNote = cache.getOrCreateAddressableNote(getEmojiPackSelectionAddress())
fun getEmojiPackSelectionAddress() = EmojiPackSelectionEvent.createAddress(signer.pubKey)
fun getEmojiPackSelection(): EmojiPackSelectionEvent? = getEmojiPackSelectionNote().event as? EmojiPackSelectionEvent
fun getEmojiPackSelection(): EmojiPackSelectionEvent? = emojiPackListNote.event as? EmojiPackSelectionEvent
fun getEmojiPackSelectionFlow(): StateFlow<NoteState> = getEmojiPackSelectionNote().flow().metadata.stateFlow
fun getEmojiPackSelectionNote(): AddressableNote = cache.getOrCreateAddressableNote(getEmojiPackSelectionAddress())
fun getEmojiPackSelectionFlow(): StateFlow<NoteState> = emojiPackListNote.flow().metadata.stateFlow
fun convertEmojiSelectionPack(selection: EmojiPackSelectionEvent?): List<StateFlow<NoteState>>? =
selection?.taggedAddresses()?.map {
@@ -75,7 +75,7 @@ class EmojiPackState(
emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent))
}.onStart {
emit(convertEmojiSelectionPack(getEmojiPackSelection()))
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -120,7 +120,7 @@ class EmojiPackState(
)?.map { it.value }?.toTypedArray() ?: emptyArray(),
),
)
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.model.nip47WalletConnect
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
@@ -62,6 +62,7 @@ import kotlinx.coroutines.launch
*/
class NwcSignerState(
val signer: NostrSigner,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
@@ -75,7 +76,7 @@ class NwcSignerState(
settings.zapPaymentRequest
.map {
buildSigner(it) ?: signer
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -90,7 +91,7 @@ class NwcSignerState(
nip47Signer
.map {
NostrWalletConnectRequestCache(it)
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectRequestCache(nip47Signer.value))
/**
@@ -101,7 +102,7 @@ class NwcSignerState(
nip47Signer
.map {
NostrWalletConnectResponseCache(it)
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectResponseCache(nip47Signer.value))
fun buildSigner(uri: Nip47WalletConnect.Nip47URINorm?) =
@@ -163,13 +164,11 @@ class NwcSignerState(
relay = walletService.relayUri,
)
Amethyst.instance.sources.nwc
.subscribe(filter)
nwcFilterAssembler.subscribe(filter)
scope.launch(Dispatchers.IO) {
delay(60000) // waits 1 minute to complete payment.
Amethyst.instance.sources.nwc
.unsubscribe(filter)
nwcFilterAssembler.unsubscribe(filter)
}
cache.consume(event, zappedNote, true, walletService.relayUri) {

View File

@@ -51,13 +51,14 @@ class BookmarkListState(
val private: List<Note> = emptyList(),
)
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val bookmarkList = cache.getOrCreateAddressableNote(getBookmarkListAddress())
fun getBookmarkListAddress() = BookmarkListEvent.createBookmarkAddress(signer.pubKey)
fun getBookmarkListNote() = cache.getOrCreateAddressableNote(getBookmarkListAddress())
fun getBookmarkListFlow(): StateFlow<NoteState> = bookmarkList.flow().metadata.stateFlow
fun getBookmarkListFlow(): StateFlow<NoteState> = getBookmarkListNote().flow().metadata.stateFlow
fun getBookmarkList(): BookmarkListEvent? = getBookmarkListNote().event as? BookmarkListEvent
fun getBookmarkList(): BookmarkListEvent? = bookmarkList.event as? BookmarkListEvent
suspend fun publicBookmarks(note: Note): List<BookmarkIdTag> {
val noteEvent = note.event as? BookmarkListEvent
@@ -75,9 +76,9 @@ class BookmarkListState(
.map { noteState ->
publicBookmarks(noteState.note)
}.onStart {
emit(publicBookmarks(getBookmarkListNote()))
emit(publicBookmarks(bookmarkList))
}.debounce(100)
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -90,9 +91,9 @@ class BookmarkListState(
.map { noteState ->
privateBookmarks(noteState.note)
}.onStart {
emit(privateBookmarks(getBookmarkListNote()))
emit(privateBookmarks(bookmarkList))
}.debounce(100)
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -106,7 +107,7 @@ class BookmarkListState(
.mapNotNull {
if (it is EventBookmark) it.eventId else null
}.toSet()
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -120,7 +121,7 @@ class BookmarkListState(
.mapNotNull {
if (it is AddressBookmark) it.address else null
}.toSet()
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -134,7 +135,7 @@ class BookmarkListState(
.mapNotNull {
if (it is EventBookmark) it.eventId else null
}.toSet()
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -148,7 +149,7 @@ class BookmarkListState(
.mapNotNull {
if (it is AddressBookmark) it.address else null
}.toSet()
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -184,7 +185,7 @@ class BookmarkListState(
emit(bookmarkList(private, public))
}.onStart {
emit(bookmarkList(privateBookmarks.value, publicBookmarks.value))
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -93,7 +93,7 @@ class HiddenUsersState(
settings.syncedSettings.security.showSensitiveContent.value,
),
)
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
@@ -43,13 +44,14 @@ class BlockPeopleListState(
val decryptionCache: PeopleListDecryptionCache,
val scope: CoroutineScope,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val blockListNote = cache.getOrCreateAddressableNote(getBlockListAddress())
fun getBlockListAddress() = PeopleListEvent.createBlockAddress(signer.pubKey)
fun getBlockListNote() = LocalCache.getOrCreateAddressableNote(getBlockListAddress())
fun getBlockListFlow(): StateFlow<NoteState> = blockListNote.flow().metadata.stateFlow
fun getBlockListFlow(): StateFlow<NoteState> = getBlockListNote().flow().metadata.stateFlow
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
fun getBlockList(): PeopleListEvent? = blockListNote.event as? PeopleListEvent
suspend fun blockListWithBackup(note: Note): List<MuteTag> {
val event = note.event as? PeopleListEvent
@@ -59,8 +61,8 @@ class BlockPeopleListState(
val flow =
getBlockListFlow()
.map { blockListWithBackup(it.note) }
.onStart { emit(blockListWithBackup(getBlockListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(blockListWithBackup(blockListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -20,9 +20,7 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
@@ -30,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -49,13 +48,14 @@ class BlockedRelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val blockedListNote = cache.getOrCreateAddressableNote(getBlockedRelayListAddress())
fun getBlockedRelayListAddress() = BlockedRelayListEvent.createAddress(signer.pubKey)
fun getBlockedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlockedRelayListAddress())
fun getBlockedRelayListFlow(): StateFlow<NoteState> = blockedListNote.flow().metadata.stateFlow
fun getBlockedRelayListFlow(): StateFlow<NoteState> = getBlockedRelayListNote().flow().metadata.stateFlow
fun getBlockedRelayList(): BlockedRelayListEvent? = getBlockedRelayListNote().event as? BlockedRelayListEvent
fun getBlockedRelayList(): BlockedRelayListEvent? = blockedListNote.event as? BlockedRelayListEvent
suspend fun normalizeBlockedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? BlockedRelayListEvent ?: settings.backupBlockedRelayList
@@ -66,8 +66,8 @@ class BlockedRelayListState(
getBlockedRelayListFlow()
.map {
normalizeBlockedRelayListWithBackup(it.note)
}.onStart { emit(normalizeBlockedRelayListWithBackup(getBlockedRelayListNote())) }
.flowOn(Dispatchers.Default)
}.onStart { emit(normalizeBlockedRelayListWithBackup(blockedListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -99,7 +99,7 @@ class BlockedRelayListState(
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Blocked Relay List Collector Start")
getBlockedRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Blocked Relay List for ${signer.pubKey}")

View File

@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
@@ -44,13 +43,14 @@ class BroadcastRelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val broadcastListNote = cache.getOrCreateAddressableNote(getBroadcastRelayListAddress())
fun getBroadcastRelayListAddress() = BroadcastRelayListEvent.createAddress(signer.pubKey)
fun getBroadcastRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getBroadcastRelayListAddress())
fun getBroadcastRelayListFlow(): StateFlow<NoteState> = broadcastListNote.flow().metadata.stateFlow
fun getBroadcastRelayListFlow(): StateFlow<NoteState> = getBroadcastRelayListNote().flow().metadata.stateFlow
fun getBroadcastRelayList(): BroadcastRelayListEvent? = getBroadcastRelayListNote().event as? BroadcastRelayListEvent
fun getBroadcastRelayList(): BroadcastRelayListEvent? = broadcastListNote.event as? BroadcastRelayListEvent
suspend fun normalizeBroadcastRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? BroadcastRelayListEvent
@@ -60,8 +60,8 @@ class BroadcastRelayListState(
val flow =
getBroadcastRelayListFlow()
.map { normalizeBroadcastRelayListWithBackup(it.note) }
.onStart { emit(normalizeBroadcastRelayListWithBackup(getBroadcastRelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeBroadcastRelayListWithBackup(broadcastListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -72,5 +72,5 @@ class GeohashListDecryptionCache(
emit(EmptyGeohashListCard)
}
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
}

View File

@@ -20,14 +20,13 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -48,17 +47,18 @@ class GeohashListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val geohashListNote = cache.getOrCreateAddressableNote(getGeohashListAddress())
fun getGeohashListAddress() = GeohashListEvent.createAddress(signer.pubKey)
fun getGeohashListNote(): AddressableNote = cache.getOrCreateAddressableNote(getGeohashListAddress())
fun getGeohashListFlow(): StateFlow<NoteState> = geohashListNote.flow().metadata.stateFlow
fun getGeohashListFlow(): StateFlow<NoteState> = getGeohashListNote().flow().metadata.stateFlow
fun getGeohashList(): GeohashListEvent? = getGeohashListNote().event as? GeohashListEvent
fun getGeohashList(): GeohashListEvent? = geohashListNote.event as? GeohashListEvent
suspend fun geohashListWithBackup(note: Note): Set<String> {
val event = note.event as? GeohashListEvent ?: settings.backupGeohashList
return event?.let { decryptionCache.geohashes(it) } ?: emptySet()
return event?.let { decryptionCache.geohashes(it).mapTo(mutableSetOf()) { it.lowercase() } } ?: emptySet()
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -67,8 +67,8 @@ class GeohashListState(
.transformLatest { noteState ->
emit(geohashListWithBackup(noteState.note))
}.onStart {
emit(geohashListWithBackup(getGeohashListNote()))
}.flowOn(Dispatchers.Default)
emit(geohashListWithBackup(geohashListNote))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -114,7 +114,7 @@ class GeohashListState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Geohash List Collector Start")
getGeohashListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}")

View File

@@ -20,14 +20,13 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -48,17 +47,18 @@ class HashtagListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val hashtagListNote = cache.getOrCreateAddressableNote(getHashtagListAddress())
fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey)
fun getHashtagListNote(): AddressableNote = cache.getOrCreateAddressableNote(getHashtagListAddress())
fun getHashtagListFlow(): StateFlow<NoteState> = hashtagListNote.flow().metadata.stateFlow
fun getHashtagListFlow(): StateFlow<NoteState> = getHashtagListNote().flow().metadata.stateFlow
fun getHashtagList(): HashtagListEvent? = getHashtagListNote().event as? HashtagListEvent
fun getHashtagList(): HashtagListEvent? = hashtagListNote.event as? HashtagListEvent
suspend fun hashtagListWithBackup(note: Note): Set<String> {
val event = note.event as? HashtagListEvent ?: settings.backupHashtagList
return event?.let { decryptionCache.hashtags(it) } ?: emptySet()
return event?.let { decryptionCache.hashtags(it).mapTo(mutableSetOf()) { it.lowercase() } } ?: emptySet()
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -67,11 +67,11 @@ class HashtagListState(
.transformLatest { noteState ->
emit(hashtagListWithBackup(noteState.note))
}.onStart {
emit(hashtagListWithBackup(getHashtagListNote()))
}.flowOn(Dispatchers.Default)
emit(hashtagListWithBackup(hashtagListNote))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Companion.Eagerly,
SharingStarted.Eagerly,
emptySet(),
)
@@ -79,9 +79,9 @@ class HashtagListState(
val hashtagList = getHashtagList()
return if (hashtagList == null) {
HashtagListEvent.Companion.create(hashtags, true, signer)
HashtagListEvent.create(hashtags, true, signer)
} else {
HashtagListEvent.Companion.add(hashtagList, hashtags, true, signer)
HashtagListEvent.add(hashtagList, hashtags, true, signer)
}
}
@@ -89,9 +89,9 @@ class HashtagListState(
val hashtagList = getHashtagList()
return if (hashtagList == null) {
HashtagListEvent.Companion.create(hashtag, true, signer)
HashtagListEvent.create(hashtag, true, signer)
} else {
HashtagListEvent.Companion.add(hashtagList, hashtag, true, signer)
HashtagListEvent.add(hashtagList, hashtag, true, signer)
}
}
@@ -99,7 +99,7 @@ class HashtagListState(
val hashtagList = getHashtagList()
return if (hashtagList != null) {
HashtagListEvent.Companion.remove(hashtagList, hashtag, signer)
HashtagListEvent.remove(hashtagList, hashtag, signer)
} else {
null
}
@@ -114,7 +114,7 @@ class HashtagListState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Hashtag List Collector Start")
getHashtagListFlow().collect {
Log.d("AccountRegisterObservers", "Hashtag List for ${signer.pubKey}")

View File

@@ -21,22 +21,25 @@
package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class IndexerRelayListState(
val signer: NostrSigner,
@@ -45,24 +48,37 @@ class IndexerRelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val indexerListNote = cache.getOrCreateAddressableNote(getIndexerRelayListAddress())
fun getIndexerRelayListAddress() = IndexerRelayListEvent.createAddress(signer.pubKey)
fun getIndexerRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getIndexerRelayListAddress())
fun getIndexerRelayListFlow(): StateFlow<NoteState> = indexerListNote.flow().metadata.stateFlow
fun getIndexerRelayListFlow(): StateFlow<NoteState> = getIndexerRelayListNote().flow().metadata.stateFlow
fun getIndexerRelayList(): IndexerRelayListEvent? = indexerListNote.event as? IndexerRelayListEvent
fun getIndexerRelayList(): IndexerRelayListEvent? = getIndexerRelayListNote().event as? IndexerRelayListEvent
fun indexListEvent(note: Note) = note.event as? IndexerRelayListEvent ?: settings.backupIndexRelayList
suspend fun normalizeIndexerRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? IndexerRelayListEvent
return event?.let { decryptionCache.relays(it) } ?: emptySet()
}
suspend fun normalizeIndexerRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = indexListEvent(note)?.let { decryptionCache.relays(it) }?.ifEmpty { null } ?: DefaultIndexerRelayList
suspend fun normalizeIndexerRelayListWithBackupNoDefaults(note: Note): Set<NormalizedRelayUrl> = indexListEvent(note)?.let { decryptionCache.relays(it) } ?: emptySet()
val flow =
getIndexerRelayListFlow()
.map { normalizeIndexerRelayListWithBackup(it.note) }
.onStart { emit(normalizeIndexerRelayListWithBackup(getIndexerRelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeIndexerRelayListWithBackup(indexerListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
val flowNoDefaults =
getIndexerRelayListFlow()
.map { normalizeIndexerRelayListWithBackupNoDefaults(it.note) }
.onStart { emit(normalizeIndexerRelayListWithBackupNoDefaults(indexerListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -85,4 +101,22 @@ class IndexerRelayListState(
)
}
}
init {
settings.backupIndexRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved index relay list ${it.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Index Relay List Collector Start")
getIndexerRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Index Relay List for ${signer.pubKey}")
(it.note.event as? IndexerRelayListEvent)?.let {
settings.updateIndexRelayList(it)
}
}
}
}
}

View File

@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists.muteList
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@@ -30,6 +29,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -49,13 +49,14 @@ class MuteListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val muteListNote = cache.getOrCreateAddressableNote(getMuteListAddress())
fun getMuteListAddress() = MuteListEvent.createAddress(signer.pubKey)
fun getMuteListNote() = cache.getOrCreateAddressableNote(getMuteListAddress())
fun getMuteListFlow(): StateFlow<NoteState> = muteListNote.flow().metadata.stateFlow
fun getMuteListFlow(): StateFlow<NoteState> = getMuteListNote().flow().metadata.stateFlow
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
fun getMuteList(): MuteListEvent? = muteListNote.event as? MuteListEvent
suspend fun muteListWithBackup(note: Note): List<MuteTag> {
val event = note.event as? MuteListEvent ?: settings.backupMuteList
@@ -65,8 +66,8 @@ class MuteListState(
val flow =
getMuteListFlow()
.map { muteListWithBackup(it.note) }
.onStart { emit(muteListWithBackup(getMuteListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(muteListWithBackup(muteListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -148,7 +149,7 @@ class MuteListState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Mute List Collector Start")
getMuteListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Mute List for ${signer.pubKey}")

View File

@@ -0,0 +1,313 @@
/**
* Copyright (c) 2025 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.nip51Lists.peopleList
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.anyNotNullEvent
import com.vitorpamplona.amethyst.model.eventIdSet
import com.vitorpamplona.amethyst.model.events
import com.vitorpamplona.amethyst.model.filter
import com.vitorpamplona.amethyst.model.updateFlow
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.update
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.followList.description
import com.vitorpamplona.quartz.nip51Lists.followList.image
import com.vitorpamplona.quartz.nip51Lists.followList.person
import com.vitorpamplona.quartz.nip51Lists.followList.personFirst
import com.vitorpamplona.quartz.nip51Lists.followList.removePerson
import com.vitorpamplona.quartz.nip51Lists.followList.title
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
import com.vitorpamplona.quartz.utils.flattenToSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import java.util.UUID
/**
* Maintains several stateflows for each step in processing PeopleLists
*
* This class must receive updates from Large Cache as it receives new events.
*/
class FollowListsState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
val user = cache.getOrCreateUser(signer.pubKey)
fun existingPeopleListNotes() = cache.addressables.filter(FollowListEvent.KIND, user.pubkeyHex)
val followListVersions = MutableStateFlow(0)
val followListNotes =
followListVersions
.map { existingPeopleListNotes() }
.onStart { emit(existingPeopleListNotes()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
val followListsEventIds =
followListNotes
.map { it.eventIdSet() }
.onStart { emit(followListNotes.value.eventIdSet()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Companion.Eagerly, emptySet())
@OptIn(ExperimentalCoroutinesApi::class)
val latestLists: StateFlow<List<FollowListEvent>> =
followListNotes
.transformLatest { emitAll(it.updateFlow<FollowListEvent>()) }
.onStart { emit(followListNotes.value.events()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
val allPeopleListProfiles: StateFlow<Set<HexKey>> =
latestLists
.map { it.mapToUserIdSet() }
.onStart { emit(latestLists.value.mapToUserIdSet()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Companion.Eagerly, emptySet())
fun FollowListEvent.toUI() =
PeopleList(
identifierTag = this.dTag(),
title = this.title() ?: this.dTag(),
description = this.description(),
image = this.image(),
privateMembers = emptySet(),
publicMembers = cache.load(this.followIdSet()),
)
fun List<FollowListEvent>.toUI() = this.map { it.toUI() }
val uiListFlow =
latestLists
.map { it.toUI() }
.onStart { emit(latestLists.value.toUI()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
fun List<PeopleList>.select(dTag: String) =
this.firstOrNull {
it.identifierTag == dTag
}
fun selectList(dTag: String) = uiListFlow.value.select(dTag)
fun selectListFlow(dTag: String) =
uiListFlow
.map { it.select(dTag) }
.onStart { emit(selectList(dTag)) }
fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex)
fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.Companion.KIND) || deletesAnyEventIn(followListsEventIds.value)
fun hasItemInNoteList(notes: Set<Note>): Boolean =
notes.anyNotNullEvent { event ->
if (event.pubKey == signer.pubKey) {
event is FollowListEvent || (event is DeletionEvent && event.hasDeletedAnyFollowList())
} else {
false
}
}
fun newNotes(newNotes: Set<Note>) {
if (hasItemInNoteList(newNotes)) {
forceRefresh()
}
}
fun deletedNotes(deletedNotes: Set<Note>) {
if (hasItemInNoteList(deletedNotes)) {
forceRefresh()
}
}
fun forceRefresh() {
followListVersions.update { it + 1 }
}
// --------------
// Updating Lists
// --------------
fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier }
fun getPeopleList(noteIdentifier: String): FollowListEvent = getPeopleListNote(noteIdentifier)?.event as FollowListEvent
fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint())
fun Set<User>.toUserTags() = map { it.toUserTag() }
suspend fun addFollowList(
name: String,
desc: String?,
image: String?,
member: User? = null,
isPrivate: Boolean = false,
account: Account,
): String {
val dTag = UUID.randomUUID().toString()
val newListTemplate =
FollowListEvent.build(
name = name,
people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
dTag = dTag,
) {
if (desc != null) description(desc)
if (image != null) image(image)
}
val newList = signer.sign(newListTemplate)
account.sendMyPublicAndPrivateOutbox(newList)
return dTag
}
suspend fun updateMetadata(
name: String?,
desc: String?,
image: String?,
peopleList: PeopleList,
account: Account,
) {
val listEvent = getPeopleList(peopleList.identifierTag)
val template =
listEvent.update {
if (name != null) title(name)
if (desc != null) description(desc)
if (image != null) image(image)
}
val newList = signer.sign(template)
account.sendMyPublicAndPrivateOutbox(newList)
}
suspend fun cloneFollowSet(
currentFollowPack: PeopleList,
customCloneName: String?,
customCloneDescription: String?,
account: Account,
) {
val listEvent = getPeopleList(currentFollowPack.identifierTag)
val template =
listEvent.update {
// new list
dTag(UUID.randomUUID().toString())
// updates names
if (customCloneName != null) title(customCloneName)
if (customCloneDescription != null) description(customCloneDescription)
}
val newList = signer.sign(template)
account.sendMyPublicAndPrivateOutbox(newList)
}
suspend fun deleteFollowSet(
identifierTag: String,
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent)))
account.sendMyPublicAndPrivateOutbox(deletionEvent)
}
suspend fun addUserToSet(
user: User,
identifierTag: String,
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val template =
followListEvent.update {
person(user.pubkeyHex, user.bestRelayHint())
}
val newList = signer.sign(template)
account.sendMyPublicAndPrivateOutbox(newList)
}
suspend fun addUserFirstToSet(
user: User,
identifierTag: String,
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val template =
followListEvent.update {
personFirst(user.pubkeyHex, user.bestRelayHint())
}
val newList = signer.sign(template)
account.sendMyPublicAndPrivateOutbox(newList)
}
suspend fun removeUserFromSet(
user: User,
identifierTag: String,
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val template =
followListEvent.update {
removePerson(user.pubkeyHex)
}
val newList = signer.sign(template)
account.sendMyPublicAndPrivateOutbox(newList)
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2025 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.nip51Lists.peopleList
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.User
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Stable
data class PeopleList(
val identifierTag: String,
val title: String,
val description: String?,
val image: String?,
val privateMembers: Set<User> = emptySet(),
val publicMembers: Set<User> = emptySet(),
) {
val privateMembersList: ImmutableList<User> = privateMembers.toPersistentList()
val publicMembersList: ImmutableList<User> = publicMembers.toPersistentList()
}

View File

@@ -18,7 +18,7 @@
* 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.nip51Lists.blockPeopleList
package com.vitorpamplona.amethyst.model.nip51Lists.peopleList
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
@@ -53,4 +53,8 @@ class PeopleListDecryptionCache(
suspend fun words(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords()
suspend fun wordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet()
suspend fun privateUsers(event: PeopleListEvent) = cachedPrivateLists.privateTags(event)?.mutedUsers() ?: emptyList()
suspend fun privateUserIdSet(event: PeopleListEvent) = cachedPrivateLists.privateTags(event)?.mutedUserIdSet() ?: emptySet()
}

View File

@@ -0,0 +1,316 @@
/**
* Copyright (c) 2025 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.nip51Lists.peopleList
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.anyNotNullEvent
import com.vitorpamplona.amethyst.model.eventIdSet
import com.vitorpamplona.amethyst.model.events
import com.vitorpamplona.amethyst.model.filter
import com.vitorpamplona.amethyst.model.updateFlow
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.update
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip51Lists.followList.description
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
import com.vitorpamplona.quartz.utils.flattenToSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import java.util.UUID
/**
* Maintains several stateflows for each step in processing PeopleLists
*
* This class must receive updates from Large Cache as it receives new events.
*/
class PeopleListsState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: PeopleListDecryptionCache,
val scope: CoroutineScope,
) {
val user = cache.getOrCreateUser(signer.pubKey)
fun existingPeopleListNotes() = cache.addressables.filter(PeopleListEvent.KIND, user.pubkeyHex)
val peopleListVersions = MutableStateFlow(0)
val peopleListNotes =
peopleListVersions
.map { existingPeopleListNotes() }
.onStart { emit(existingPeopleListNotes()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
val peopleListsEventIds =
peopleListNotes
.map { it.eventIdSet() }
.onStart { emit(peopleListNotes.value.eventIdSet()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptySet())
@OptIn(ExperimentalCoroutinesApi::class)
val latestLists: StateFlow<List<PeopleListEvent>> =
peopleListNotes
.transformLatest { emitAll(it.updateFlow<PeopleListEvent>()) }
.onStart { emit(peopleListNotes.value.events()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
suspend fun PeopleListEvent.userIdSet() = decryptionCache.userIdSet(this)
suspend fun List<PeopleListEvent>.mapToUserIdSet() = this.map { it.userIdSet() }.flattenToSet()
suspend fun List<PeopleListEvent>.mapGoodUsersToIdSet() =
this
.mapNotNull {
if (it.dTag() != PeopleListEvent.BLOCK_LIST_D_TAG) {
it.userIdSet()
} else {
null
}
}.flattenToSet()
val allGoodPeopleListProfiles: StateFlow<Set<HexKey>> =
latestLists
.map { it.mapGoodUsersToIdSet() }
.onStart { emit(latestLists.value.mapGoodUsersToIdSet()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptySet())
suspend fun PeopleListEvent.toUI() =
PeopleList(
identifierTag = this.dTag(),
title = this.nameOrTitle() ?: this.dTag(),
description = this.description(),
image = this.image(),
privateMembers = cache.load(decryptionCache.privateUserIdSet(this)),
publicMembers = cache.load(this.publicUsersIdSet()),
)
suspend fun List<PeopleListEvent>.toUI() = this.map { it.toUI() }.sortedBy { it.title }
val uiListFlow =
latestLists
.map { it.toUI() }
.onStart { emit(latestLists.value.toUI()) }
.flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.Eagerly, emptyList())
fun List<PeopleList>.select(dTag: String) =
this.firstOrNull {
it.identifierTag == dTag
}
fun selectList(dTag: String) = uiListFlow.value.select(dTag)
fun selectListFlow(dTag: String) =
uiListFlow
.map { it.select(dTag) }
.onStart { emit(selectList(dTag)) }
fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
fun hasItemInNoteList(notes: Set<Note>): Boolean =
notes.anyNotNullEvent { event ->
if (event.pubKey == signer.pubKey) {
event is PeopleListEvent || (event is DeletionEvent && event.hasDeletedAnyPeopleList())
} else {
false
}
}
fun newNotes(newNotes: Set<Note>) {
if (hasItemInNoteList(newNotes)) {
forceRefresh()
}
}
fun deletedNotes(deletedNotes: Set<Note>) {
if (hasItemInNoteList(deletedNotes)) {
forceRefresh()
}
}
fun forceRefresh() {
peopleListVersions.update { it + 1 }
}
// --------------
// Updating Lists
// --------------
fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier }
fun getPeopleList(noteIdentifier: String): PeopleListEvent = getPeopleListNote(noteIdentifier)?.event as PeopleListEvent
fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint())
fun Set<User>.toUserTags() = map { it.toUserTag() }
suspend fun addFollowList(
listName: String,
listDescription: String?,
listImage: String?,
member: User? = null,
isPrivate: Boolean = false,
account: Account,
): String {
val dTag = UUID.randomUUID().toString()
val newListTemplate =
PeopleListEvent.build(
dTag = dTag,
name = listName,
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
privateMembers = if (isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
signer = account.signer,
) {
if (listDescription != null) description(listDescription)
if (listImage != null) image(listImage)
}
val newList = signer.sign(newListTemplate)
account.sendMyPublicAndPrivateOutbox(newList)
return dTag
}
suspend fun updateMetadata(
listName: String?,
listDescription: String?,
listImage: String?,
peopleList: PeopleList,
account: Account,
) {
val listEvent = getPeopleList(peopleList.identifierTag)
val template =
listEvent.update {
if (listName != null) name(listName)
if (listDescription != null) description(listDescription)
if (listImage != null) image(listImage)
}
val newList = signer.sign(template)
account.sendMyPublicAndPrivateOutbox(newList)
}
suspend fun cloneFollowSet(
currentPeopleList: PeopleList,
customCloneName: String?,
customCloneDescription: String?,
account: Account,
) {
val newList =
PeopleListEvent.createListWithDescription(
dTag = UUID.randomUUID().toString(),
title = customCloneName ?: currentPeopleList.title,
description = customCloneDescription ?: currentPeopleList.description,
publicMembers = currentPeopleList.publicMembers.toUserTags(),
privateMembers = currentPeopleList.privateMembers.toUserTags(),
signer = account.signer,
)
account.sendMyPublicAndPrivateOutbox(newList)
}
suspend fun deleteFollowSet(
identifierTag: String,
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent)))
account.sendMyPublicAndPrivateOutbox(deletionEvent)
}
suspend fun addUserToSet(
user: User,
identifierTag: String,
shouldBePrivateMember: Boolean,
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val newList =
PeopleListEvent.addUser(
earlierVersion = followListEvent,
pubKeyHex = user.pubkeyHex,
relayHint = user.bestRelayHint(),
isPrivate = shouldBePrivateMember,
signer = account.signer,
)
account.sendMyPublicAndPrivateOutbox(newList)
}
suspend fun addUserFirstToSet(
user: User,
identifierTag: String,
shouldBePrivateMember: Boolean,
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val newList =
PeopleListEvent.addUserFirst(
earlierVersion = followListEvent,
pubKeyHex = user.pubkeyHex,
relayHint = user.bestRelayHint(),
isPrivate = shouldBePrivateMember,
signer = account.signer,
)
account.sendMyPublicAndPrivateOutbox(newList)
}
suspend fun removeUserFromSet(
user: User,
isPrivate: Boolean,
identifierTag: String,
account: Account,
) {
val followListEvent = getPeopleList(identifierTag)
val newList =
PeopleListEvent.removeUser(
earlierVersion = followListEvent,
pubKeyHex = user.pubkeyHex,
isUserPrivate = isPrivate,
signer = account.signer,
)
account.sendMyPublicAndPrivateOutbox(newList)
}
}

View File

@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
@@ -44,13 +43,14 @@ class ProxyRelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val proxyListNote = cache.getOrCreateAddressableNote(getProxyRelayListAddress())
fun getProxyRelayListAddress() = ProxyRelayListEvent.createAddress(signer.pubKey)
fun getProxyRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getProxyRelayListAddress())
fun getProxyRelayListFlow(): StateFlow<NoteState> = proxyListNote.flow().metadata.stateFlow
fun getProxyRelayListFlow(): StateFlow<NoteState> = getProxyRelayListNote().flow().metadata.stateFlow
fun getProxyRelayList(): ProxyRelayListEvent? = getProxyRelayListNote().event as? ProxyRelayListEvent
fun getProxyRelayList(): ProxyRelayListEvent? = proxyListNote.event as? ProxyRelayListEvent
suspend fun normalizeProxyRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? ProxyRelayListEvent
@@ -60,8 +60,8 @@ class ProxyRelayListState(
val flow =
getProxyRelayListFlow()
.map { normalizeProxyRelayListWithBackup(it.note) }
.onStart { emit(normalizeProxyRelayListWithBackup(getProxyRelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeProxyRelayListWithBackup(proxyListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -72,5 +72,5 @@ open class GenericRelayListCache<T : PrivateTagArrayEvent>(
emit(EmptyRelayListCard)
}
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
}

View File

@@ -20,9 +20,7 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@@ -30,6 +28,7 @@ import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -49,13 +48,14 @@ class SearchRelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val searchListNote = cache.getOrCreateAddressableNote(getSearchRelayListAddress())
fun getSearchRelayListAddress() = SearchRelayListEvent.Companion.createAddress(signer.pubKey)
fun getSearchRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getSearchRelayListAddress())
fun getSearchRelayListFlow(): StateFlow<NoteState> = searchListNote.flow().metadata.stateFlow
fun getSearchRelayListFlow(): StateFlow<NoteState> = getSearchRelayListNote().flow().metadata.stateFlow
fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent
fun getSearchRelayList(): SearchRelayListEvent? = searchListNote.event as? SearchRelayListEvent
fun searchListEvent(note: Note) = note.event as? SearchRelayListEvent ?: settings.backupSearchRelayList
@@ -66,8 +66,8 @@ class SearchRelayListState(
val flow =
getSearchRelayListFlow()
.map { normalizeSearchRelayListWithBackup(it.note) }
.onStart { emit(normalizeSearchRelayListWithBackup(getSearchRelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeSearchRelayListWithBackup(searchListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Companion.Eagerly,
@@ -77,11 +77,11 @@ class SearchRelayListState(
val flowNoDefaults =
getSearchRelayListFlow()
.map { normalizeSearchRelayListWithBackupNoDefaults(it.note) }
.onStart { emit(normalizeSearchRelayListWithBackupNoDefaults(getSearchRelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeSearchRelayListWithBackupNoDefaults(searchListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Companion.Eagerly,
SharingStarted.Eagerly,
emptySet(),
)
@@ -89,13 +89,13 @@ class SearchRelayListState(
val relayListForSearch = getSearchRelayList()
return if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) {
SearchRelayListEvent.Companion.updateRelayList(
SearchRelayListEvent.updateRelayList(
earlierVersion = relayListForSearch,
relays = searchRelays,
signer = signer,
)
} else {
SearchRelayListEvent.Companion.create(
SearchRelayListEvent.create(
relays = searchRelays,
signer = signer,
)
@@ -109,7 +109,7 @@ class SearchRelayListState(
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Search Relay List Collector Start")
getSearchRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Search Relay List for ${signer.pubKey}")

View File

@@ -20,15 +20,14 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -48,13 +47,14 @@ class TrustedRelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getTrustedRelayListAddress() = TrustedRelayListEvent.Companion.createAddress(signer.pubKey)
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val trustedListNote = cache.getOrCreateAddressableNote(getTrustedRelayListAddress())
fun getTrustedRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getTrustedRelayListAddress())
fun getTrustedRelayListAddress() = TrustedRelayListEvent.createAddress(signer.pubKey)
fun getTrustedRelayListFlow(): StateFlow<NoteState> = getTrustedRelayListNote().flow().metadata.stateFlow
fun getTrustedRelayListFlow(): StateFlow<NoteState> = trustedListNote.flow().metadata.stateFlow
fun getTrustedRelayList(): TrustedRelayListEvent? = getTrustedRelayListNote().event as? TrustedRelayListEvent
fun getTrustedRelayList(): TrustedRelayListEvent? = trustedListNote.event as? TrustedRelayListEvent
suspend fun normalizeTrustedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? TrustedRelayListEvent ?: settings.backupTrustedRelayList
@@ -64,8 +64,8 @@ class TrustedRelayListState(
val flow =
getTrustedRelayListFlow()
.map { normalizeTrustedRelayListWithBackup(it.note) }
.onStart { emit(normalizeTrustedRelayListWithBackup(getTrustedRelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeTrustedRelayListWithBackup(trustedListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Companion.Eagerly,
@@ -96,7 +96,7 @@ class TrustedRelayListState(
GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Trusted Relay List Collector Start")
getTrustedRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Trusted Relay List for ${signer.pubKey}")

View File

@@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
@@ -36,11 +36,13 @@ class LiveActivitiesChannel(
) : Channel() {
var creator: User? = null
var info: LiveActivitiesEvent? = null
// Important to keep this long-term reference because LocalCache uses WeakReferences.
var infoNote: Note? = null
fun address() = address
override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays()
override fun relays() = info?.allRelayUrls()?.toSet()?.ifEmpty { null } ?: super.relays()
fun relayHintUrl() = relays().firstOrNull()
@@ -57,7 +59,7 @@ class LiveActivitiesChannel(
super.updateChannelInfo()
}
override fun toBestDisplayName(): String = info?.title() ?: toNAddr().toShortDisplay()
override fun toBestDisplayName(): String = info?.title() ?: creatorName() ?: toNAddr().toShortDisplay()
fun creatorName(): String? = creator?.toBestDisplayName()

View File

@@ -20,9 +20,7 @@
*/
package com.vitorpamplona.amethyst.model.nip65RelayList
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@@ -31,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -49,19 +48,24 @@ class Nip65RelayListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val nip65ListNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress())
fun getNIP65RelayListAddress() = AdvertisedRelayListEvent.createAddress(signer.pubKey)
fun getNIP65RelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress())
fun getNIP65RelayListFlow(): StateFlow<NoteState> = nip65ListNote.flow().metadata.stateFlow
fun getNIP65RelayListFlow(): StateFlow<NoteState> = getNIP65RelayListNote().flow().metadata.stateFlow
fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent
fun getNIP65RelayList(): AdvertisedRelayListEvent? = nip65ListNote.event as? AdvertisedRelayListEvent
fun nip65Event(note: Note) = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList
fun normalizeNIP65WriteRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.writeRelaysNorm()?.toSet() ?: Constants.eventFinderRelays
fun normalizeNIP65ReadRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.readRelaysNorm()?.toSet() ?: Constants.eventFinderRelays
fun normalizeNIP65ReadRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.readRelaysNorm()?.toSet() ?: Constants.bootstrapInbox
fun normalizeNIP65WriteRelayListNoDefaults(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.writeRelaysNorm()?.toSet() ?: emptySet()
fun normalizeNIP65ReadRelayListNoDefaults(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.readRelaysNorm()?.toSet() ?: emptySet()
fun normalizeNIP65AllRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = nip65Event(note)?.relays()?.map { it.relayUrl }?.toSet() ?: Constants.eventFinderRelays
@@ -70,8 +74,8 @@ class Nip65RelayListState(
val outboxFlow =
getNIP65RelayListFlow()
.map { normalizeNIP65WriteRelayListWithBackup(it.note) }
.onStart { emit(normalizeNIP65ReadRelayListWithBackup(getNIP65RelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeNIP65WriteRelayListWithBackup(nip65ListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -81,19 +85,30 @@ class Nip65RelayListState(
val inboxFlow =
getNIP65RelayListFlow()
.map { normalizeNIP65ReadRelayListWithBackup(it.note) }
.onStart { emit(normalizeNIP65ReadRelayListWithBackup(getNIP65RelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeNIP65ReadRelayListWithBackup(nip65ListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
val allFlow =
val outboxFlowNoDefaults =
getNIP65RelayListFlow()
.map { normalizeNIP65AllRelayListWithBackup(it.note) }
.onStart { emit(normalizeNIP65AllRelayListWithBackup(getNIP65RelayListNote())) }
.flowOn(Dispatchers.Default)
.map { normalizeNIP65WriteRelayListNoDefaults(it.note) }
.onStart { emit(normalizeNIP65WriteRelayListNoDefaults(nip65ListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
val inboxFlowNoDefaults =
getNIP65RelayListFlow()
.map { normalizeNIP65ReadRelayListNoDefaults(it.note) }
.onStart { emit(normalizeNIP65ReadRelayListNoDefaults(nip65ListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -103,8 +118,8 @@ class Nip65RelayListState(
val allFlowNoDefaults =
getNIP65RelayListFlow()
.map { normalizeNIP65AllRelayListWithBackupNoDefaults(it.note) }
.onStart { emit(normalizeNIP65AllRelayListWithBackupNoDefaults(getNIP65RelayListNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeNIP65AllRelayListWithBackupNoDefaults(nip65ListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -135,7 +150,7 @@ class Nip65RelayListState(
GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "NIP-65 Relay List Collector Start")
getNIP65RelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating NIP-65 List for ${signer.pubKey}")

View File

@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.model.nip72Communities
import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
@@ -30,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -51,13 +51,14 @@ class CommunityListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val communityListNote = cache.getOrCreateAddressableNote(getCommunityListAddress())
fun getCommunityListAddress() = CommunityListEvent.createAddress(signer.pubKey)
fun getCommunityListNote(): AddressableNote = cache.getOrCreateAddressableNote(getCommunityListAddress())
fun getCommunityListFlow(): StateFlow<NoteState> = communityListNote.flow().metadata.stateFlow
fun getCommunityListFlow(): StateFlow<NoteState> = getCommunityListNote().flow().metadata.stateFlow
fun getCommunityList(): CommunityListEvent? = getCommunityListNote().event as? CommunityListEvent
fun getCommunityList(): CommunityListEvent? = communityListNote.event as? CommunityListEvent
suspend fun communityListWithBackup(note: Note): Set<CommunityTag> {
val event = note.event as? CommunityListEvent ?: settings.backupCommunityList
@@ -70,14 +71,30 @@ class CommunityListState(
.transformLatest { noteState ->
emit(communityListWithBackup(noteState.note))
}.onStart {
emit(communityListWithBackup(getCommunityListNote()))
}.flowOn(Dispatchers.Default)
emit(communityListWithBackup(communityListNote))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val flowNotes: StateFlow<List<AddressableNote>> =
flow
.map { hint ->
hint.map {
cache.getOrCreateAddressableNote(it.address)
}
}.onStart {
emit(flow.value.map { cache.getOrCreateAddressableNote(it.address) })
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val flowSet: StateFlow<Set<String>> =
flow
@@ -85,7 +102,7 @@ class CommunityListState(
hint.mapTo(mutableSetOf()) { it.address.toValue() }
}.onStart {
emit(flow.value.mapTo(mutableSetOf()) { it.address.toValue() })
}.flowOn(Dispatchers.Default)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -146,7 +163,7 @@ class CommunityListState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Community List Collector Start")
getCommunityListFlow().collect {
Log.d("AccountRegisterObservers", "Community List for ${signer.pubKey}")

View File

@@ -20,15 +20,14 @@
*/
package com.vitorpamplona.amethyst.model.nip78AppSpecific
import android.util.Log
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -47,17 +46,18 @@ class AppSpecificState(
const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings"
}
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val amethystSettingsNote = cache.getOrCreateAddressableNote(getAppSpecificDataAddress())
fun getAppSpecificDataAddress() = AppSpecificDataEvent.createAddress(signer.pubKey, APP_SPECIFIC_DATA_D_TAG)
fun getAppSpecificDataNote() = cache.getOrCreateAddressableNote(getAppSpecificDataAddress())
fun getAppSpecificDataFlow(): StateFlow<NoteState> = getAppSpecificDataNote().flow().metadata.stateFlow
fun getAppSpecificDataFlow(): StateFlow<NoteState> = amethystSettingsNote.flow().metadata.stateFlow
suspend fun saveNewAppSpecificData(): AppSpecificDataEvent {
val toInternal = settings.syncedSettings.toInternal()
return AppSpecificDataEvent.create(
dTag = APP_SPECIFIC_DATA_D_TAG,
description = signer.nip44Encrypt(JsonMapper.mapper.writeValueAsString(toInternal), signer.pubKey),
description = signer.nip44Encrypt(JsonMapper.toJson(toInternal), signer.pubKey),
otherTags = emptyArray(),
signer = signer,
)
@@ -72,7 +72,7 @@ class AppSpecificState(
LocalCache.justConsumeMyOwnEvent(event)
try {
val decrypted = signer.decrypt(event.content, event.pubKey)
val syncedSettings = JsonMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
val syncedSettings = JsonMapper.fromJson<AccountSyncedSettingsInternal>(decrypted)
settings.syncedSettings.updateFrom(syncedSettings)
} catch (e: Throwable) {
if (e is CancellationException) throw e
@@ -81,7 +81,7 @@ class AppSpecificState(
}
}
scope.launch(Dispatchers.Default) {
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "AppSpecificData Collector Start")
getAppSpecificDataFlow().collect {
try {
@@ -89,7 +89,7 @@ class AppSpecificState(
(it.note.event as? AppSpecificDataEvent)?.let {
val decrypted = signer.decrypt(it.content, it.pubKey)
try {
val syncedSettings = JsonMapper.mapper.readValue<AccountSyncedSettingsInternal>(decrypted)
val syncedSettings = JsonMapper.fromJson<AccountSyncedSettingsInternal>(decrypted)
settings.updateAppSpecificData(it, syncedSettings)
} catch (e: Throwable) {
if (e is CancellationException) throw e

View File

@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.model.nip96FileStorage
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
@@ -42,13 +41,14 @@ class FileStorageServerListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val fileStorageListNote = cache.getOrCreateAddressableNote(getFileServersAddress())
fun getFileServersAddress() = FileServersEvent.createAddress(signer.pubKey)
fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getFileServersAddress())
fun getFileServersListFlow(): StateFlow<NoteState> = fileStorageListNote.flow().metadata.stateFlow
fun getFileServersListFlow(): StateFlow<NoteState> = getFileServersNote().flow().metadata.stateFlow
fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent
fun getFileServersList(): FileServersEvent? = fileStorageListNote.event as? FileServersEvent
fun normalizeServers(note: Note): List<String> {
val event = note.event as? FileServersEvent
@@ -58,8 +58,8 @@ class FileStorageServerListState(
val flow =
getFileServersListFlow()
.map { normalizeServers(it.note) }
.onStart { emit(normalizeServers(getFileServersNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeServers(fileStorageListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,

View File

@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.model.nipB7Blossom
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
@@ -44,13 +43,14 @@ class BlossomServerListState(
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val blossomListNote = cache.getOrCreateAddressableNote(getBlossomServersAddress())
fun getBlossomServersAddress() = BlossomServersEvent.createAddress(signer.pubKey)
fun getBlossomServersNote(): AddressableNote = cache.getOrCreateAddressableNote(getBlossomServersAddress())
fun getBlossomServersListFlow(): StateFlow<NoteState> = blossomListNote.flow().metadata.stateFlow
fun getBlossomServersListFlow(): StateFlow<NoteState> = getBlossomServersNote().flow().metadata.stateFlow
fun getBlossomServersList(): BlossomServersEvent? = getBlossomServersNote().event as? BlossomServersEvent
fun getBlossomServersList(): BlossomServersEvent? = blossomListNote.event as? BlossomServersEvent
fun normalizeServers(note: Note): List<String> {
val event = note.event as? BlossomServersEvent
@@ -60,8 +60,8 @@ class BlossomServerListState(
val flow =
getBlossomServersListFlow()
.map { normalizeServers(it.note) }
.onStart { emit(normalizeServers(getBlossomServersNote())) }
.flowOn(Dispatchers.Default)
.onStart { emit(normalizeServers(blossomListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
@@ -74,7 +74,7 @@ class BlossomServerListState(
return if (serverList != null && serverList.tags.isNotEmpty()) {
BlossomServersEvent.updateRelayList(
earlierVersion = serverList,
relays = servers,
servers = servers,
signer = signer,
)
} else {

View File

@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model.observables
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

View File

@@ -24,7 +24,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import com.vitorpamplona.quartz.utils.LargeCache
import com.vitorpamplona.quartz.utils.cache.LargeCache
import java.io.File
class AccountPreferenceStores(
@@ -75,8 +75,9 @@ class AccountPreferenceStores(
)
}
fun removeAccount(npub: String) {
file(npub).delete()
fun removeAccount(npub: String): Boolean {
val deleted = file(npub).delete()
storeCache.remove(npub)
return deleted
}
}

View File

@@ -24,7 +24,7 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.stringPreferencesKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.utils.LargeCache
import com.vitorpamplona.quartz.utils.cache.LargeCache
import kotlinx.coroutines.CoroutineScope
import java.io.File
@@ -69,8 +69,9 @@ class AccountSecretsEncryptedStores(
serializer = Nip47WalletConnect.Nip47URI::serializer,
)
fun removeAccount(npub: String) {
file(npub).delete()
fun removeAccount(npub: String): Boolean {
val deleted = file(npub).delete()
storeCache.remove(npub)
return deleted
}
}

View File

@@ -29,16 +29,16 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import java.io.IOException
import java.util.Base64
import kotlin.io.encoding.Base64
class EncryptedDataStore(
private val store: DataStore<Preferences>,
private val encryption: KeyStoreEncryption = KeyStoreEncryption(),
private val scope: CoroutineScope,
) {
private fun decode(str: String): ByteArray = Base64.getDecoder().decode(str)
private fun decode(str: String): ByteArray = Base64.decode(str)
private fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes)
private fun encode(bytes: ByteArray): String = Base64.encode(bytes)
private fun encrypt(value: String): String = encode(encryption.encrypt(value.toByteArray()))

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) 2025 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.preferences
import android.content.Context
import androidx.compose.runtime.Stable
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.cancellation.CancellationException
@Stable
class TorSharedPreferences(
val context: Context,
val scope: CoroutineScope,
) {
companion object {
// loads faster when individualized
val TOR_TYPE_KEY = stringPreferencesKey("tor.torType")
val EXTERNAL_SOCKS_PORT_KEY = intPreferencesKey("tor.externalSocksPort")
val ONION_RELAYS_VIA_TOR_KEY = booleanPreferencesKey("tor.onionRelaysViaTor")
val DM_RELAYS_VIA_TOR_KEY = booleanPreferencesKey("tor.dmRelaysViaTor")
val NEW_RELAYS_VIA_TOR_KEY = booleanPreferencesKey("tor.newRelaysViaTor")
val TRUSTED_RELAYS_VIA_TOR_KEY = booleanPreferencesKey("tor.trustedRelaysViaTor")
val URL_PREVIEWS_VIA_TOR_KEY = booleanPreferencesKey("tor.urlPreviewsViaTor")
val PROFILE_PICS_VIA_TOR_KEY = booleanPreferencesKey("tor.profilePicsViaTor")
val IMAGES_VIA_TOR_KEY = booleanPreferencesKey("tor.imagesViaTor")
val VIDEOS_VIA_TOR_KEY = booleanPreferencesKey("tor.videosViaTor")
val MONEY_OPERATIONS_VIA_TOR_KEY = booleanPreferencesKey("tor.moneyOperationsViaTor")
val NIP05_VERIFICATIONS_VIA_TOR_KEY = booleanPreferencesKey("tor.nip05VerificationsViaTor")
val MEDIA_UPLOADS_VIA_TOR_KEY = booleanPreferencesKey("tor.mediaUploadsViaTor")
}
// Tor Preferences. Makes sure to wait for it to avoid connecting with random IPs
val value =
runBlocking {
TorSettingsFlow.build(torPreferences() ?: TorSettings())
}
@OptIn(FlowPreview::class)
val saving =
value.propertyWatchFlow
.debounce(1000)
.distinctUntilChanged()
.onEach(::save)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
value.toSettings(),
)
suspend fun torPreferences(): TorSettings? =
try {
// Get the preference flow and take the first value.
val preferences = context.sharedPreferencesDataStore.data.first()
TorSettings(
torType = preferences[TOR_TYPE_KEY]?.let { TorType.valueOf(it) } ?: TorType.INTERNAL,
externalSocksPort = preferences[EXTERNAL_SOCKS_PORT_KEY] ?: 9050,
onionRelaysViaTor = preferences[ONION_RELAYS_VIA_TOR_KEY] ?: true,
dmRelaysViaTor = preferences[DM_RELAYS_VIA_TOR_KEY] ?: true,
newRelaysViaTor = preferences[NEW_RELAYS_VIA_TOR_KEY] ?: true,
trustedRelaysViaTor = preferences[TRUSTED_RELAYS_VIA_TOR_KEY] ?: false,
urlPreviewsViaTor = preferences[URL_PREVIEWS_VIA_TOR_KEY] ?: false,
profilePicsViaTor = preferences[PROFILE_PICS_VIA_TOR_KEY] ?: false,
imagesViaTor = preferences[IMAGES_VIA_TOR_KEY] ?: false,
videosViaTor = preferences[VIDEOS_VIA_TOR_KEY] ?: false,
moneyOperationsViaTor = preferences[MONEY_OPERATIONS_VIA_TOR_KEY] ?: false,
nip05VerificationsViaTor = preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] ?: false,
nip96UploadsViaTor = preferences[MEDIA_UPLOADS_VIA_TOR_KEY] ?: false,
)
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}")
null
}
suspend fun save(torSettings: TorSettings) {
try {
context.sharedPreferencesDataStore.edit { preferences ->
preferences[TOR_TYPE_KEY] = torSettings.torType.name
preferences[EXTERNAL_SOCKS_PORT_KEY] = torSettings.externalSocksPort
preferences[ONION_RELAYS_VIA_TOR_KEY] = torSettings.onionRelaysViaTor
preferences[DM_RELAYS_VIA_TOR_KEY] = torSettings.dmRelaysViaTor
preferences[NEW_RELAYS_VIA_TOR_KEY] = torSettings.newRelaysViaTor
preferences[TRUSTED_RELAYS_VIA_TOR_KEY] = torSettings.trustedRelaysViaTor
preferences[URL_PREVIEWS_VIA_TOR_KEY] = torSettings.urlPreviewsViaTor
preferences[PROFILE_PICS_VIA_TOR_KEY] = torSettings.profilePicsViaTor
preferences[IMAGES_VIA_TOR_KEY] = torSettings.imagesViaTor
preferences[VIDEOS_VIA_TOR_KEY] = torSettings.videosViaTor
preferences[MONEY_OPERATIONS_VIA_TOR_KEY] = torSettings.moneyOperationsViaTor
preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] = torSettings.nip05VerificationsViaTor
preferences[MEDIA_UPLOADS_VIA_TOR_KEY] = torSettings.nip96UploadsViaTor
}
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}")
}
}
}

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2025 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.preferences
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Stable
import androidx.core.os.LocaleListCompat
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.model.BooleanType
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.ProfileGalleryType
import com.vitorpamplona.amethyst.model.ThemeType
import com.vitorpamplona.amethyst.model.UiSettings
import com.vitorpamplona.amethyst.model.UiSettingsFlow
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.cancellation.CancellationException
val Context.sharedPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(name = "shared_settings")
@Stable
class UiSharedPreferences(
val context: Context,
val scope: CoroutineScope,
) {
companion object {
// loads faster when individualized
val UI_THEME = stringPreferencesKey("ui.theme")
val UI_LANGUAGE = stringPreferencesKey("ui.language")
val UI_SHOW_IMAGES = stringPreferencesKey("ui.show_images")
val UI_START_PLAYBACK = stringPreferencesKey("ui.start_playback")
val UI_SHOW_URL_PREVIEW = stringPreferencesKey("ui.show_url_preview")
val UI_HIDE_NAVIGATION_BARS = stringPreferencesKey("ui.hide_navigation_bars")
val UI_SHOW_PROFILE_PICTURES = stringPreferencesKey("ui.show_profile_pictures")
val UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR = booleanPreferencesKey("ui.dont_show_push_notification_selector")
val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions")
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set")
}
// UI Preferences. Makes sure to wait for it to avoid blinking themes and language preferences
val value =
runBlocking {
UiSettingsFlow.build(uiPreferences() ?: UiSettings())
}
val languageUpdate =
value.preferredLanguage
.onEach { language ->
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(language),
)
}.flowOn(Dispatchers.Main)
.stateIn(
scope,
SharingStarted.Eagerly,
value.toSettings(),
)
@OptIn(FlowPreview::class)
val saving =
value.propertyWatchFlow
.debounce(1000)
.distinctUntilChanged()
.onEach(::save)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
value.toSettings(),
)
suspend fun uiPreferences(): UiSettings? =
try {
// Get the preference flow and take the first value.
val preferences = context.sharedPreferencesDataStore.data.first()
UiSettings(
theme = preferences[UI_THEME]?.let { ThemeType.valueOf(it) } ?: ThemeType.SYSTEM,
preferredLanguage = preferences[UI_LANGUAGE]?.ifBlank { null },
automaticallyShowImages = preferences[UI_SHOW_IMAGES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyStartPlayback = preferences[UI_START_PLAYBACK]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyShowUrlPreview = preferences[UI_SHOW_URL_PREVIEW]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyHideNavigationBars = preferences[UI_HIDE_NAVIGATION_BARS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
automaticallyShowProfilePictures = preferences[UI_SHOW_PROFILE_PICTURES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
dontShowPushNotificationSelector = preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] ?: false,
dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false,
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC,
)
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}")
try {
val oldVersion = LocalPreferences.loadSharedSettings()
if (oldVersion != null) {
save(oldVersion)
}
oldVersion
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
}
suspend fun save(sharedSettings: UiSettings) {
try {
context.sharedPreferencesDataStore.edit { preferences ->
preferences[UI_THEME] = sharedSettings.theme.name
preferences[UI_LANGUAGE] = sharedSettings.preferredLanguage ?: ""
preferences[UI_SHOW_IMAGES] = sharedSettings.automaticallyShowImages.name
preferences[UI_START_PLAYBACK] = sharedSettings.automaticallyStartPlayback.name
preferences[UI_SHOW_URL_PREVIEW] = sharedSettings.automaticallyShowUrlPreview.name
preferences[UI_HIDE_NAVIGATION_BARS] = sharedSettings.automaticallyHideNavigationBars.name
preferences[UI_SHOW_PROFILE_PICTURES] = sharedSettings.automaticallyShowProfilePictures.name
preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] = sharedSettings.dontShowPushNotificationSelector
preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
}
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}")
}
}
}

View File

@@ -34,7 +34,7 @@ class UpdatablePropertyFlow<T>(
) {
val stateFlow =
flow
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2025 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.privacyOptions
import okhttp3.OkHttpClient
import java.net.InetSocketAddress
class EmptyRoleBasedHttpClientBuilder : IRoleBasedHttpClientBuilder {
val rootOkHttpClient by lazy {
OkHttpClient
.Builder()
.followRedirects(true)
.followSslRedirects(true)
.build()
}
override fun proxyPortForVideo(url: String) = (rootOkHttpClient.proxy?.address() as? InetSocketAddress)?.port
override fun okHttpClientForNip05(url: String) = rootOkHttpClient
override fun okHttpClientForUploads(url: String) = rootOkHttpClient
override fun okHttpClientForImage(url: String) = rootOkHttpClient
override fun okHttpClientForVideo(url: String) = rootOkHttpClient
override fun okHttpClientForMoney(url: String) = rootOkHttpClient
override fun okHttpClientForPreview(url: String) = rootOkHttpClient
override fun okHttpClientForPushRegistration(url: String) = rootOkHttpClient
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2025 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.privacyOptions
import okhttp3.OkHttpClient
interface IRoleBasedHttpClientBuilder {
fun proxyPortForVideo(url: String): Int?
fun okHttpClientForNip05(url: String): OkHttpClient
fun okHttpClientForUploads(url: String): OkHttpClient
fun okHttpClientForImage(url: String): OkHttpClient
fun okHttpClientForVideo(url: String): OkHttpClient
fun okHttpClientForMoney(url: String): OkHttpClient
fun okHttpClientForPreview(url: String): OkHttpClient
fun okHttpClientForPushRegistration(url: String): OkHttpClient
}

View File

@@ -20,18 +20,21 @@
*/
package com.vitorpamplona.amethyst.model.privacyOptions
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import okhttp3.OkHttpClient
class PrivacyState(
val settings: AccountSettings,
) {
class RoleBasedHttpClientBuilder(
val okHttpClient: DualHttpClientManager,
val torSettings: TorSettingsFlow,
) : IRoleBasedHttpClientBuilder {
fun shouldUseTorForImageDownload(url: String) =
shouldUseTorFor(
url,
settings.torSettings.torType.value,
settings.torSettings.imagesViaTor.value,
torSettings.torType.value,
torSettings.imagesViaTor.value,
)
fun shouldUseTorFor(
@@ -57,37 +60,37 @@ class PrivacyState(
}
fun shouldUseTorForVideoDownload() =
when (settings.torSettings.torType.value) {
when (torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> settings.torSettings.videosViaTor.value
TorType.EXTERNAL -> settings.torSettings.videosViaTor.value
TorType.INTERNAL -> torSettings.videosViaTor.value
TorType.EXTERNAL -> torSettings.videosViaTor.value
}
fun shouldUseTorForVideoDownload(url: String) =
when (settings.torSettings.torType.value) {
when (torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.videosViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.videosViaTor.value)
}
fun shouldUseTorForPreviewUrl(url: String) =
when (settings.torSettings.torType.value) {
when (torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.urlPreviewsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.urlPreviewsViaTor.value)
}
fun shouldUseTorForTrustedRelays() =
when (settings.torSettings.torType.value) {
when (torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value
TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value
TorType.INTERNAL -> torSettings.trustedRelaysViaTor.value
TorType.EXTERNAL -> torSettings.trustedRelaysViaTor.value
}
private fun checkLocalHostOnionAndThen(
url: String,
final: Boolean,
): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final)
): Boolean = checkLocalHostOnionAndThen(url, torSettings.onionRelaysViaTor.value, final)
private fun checkLocalHostOnionAndThen(
normalizedUrl: String,
@@ -103,23 +106,39 @@ class PrivacyState(
}
fun shouldUseTorForMoneyOperations(url: String) =
when (settings.torSettings.torType.value) {
when (torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.moneyOperationsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.moneyOperationsViaTor.value)
}
fun shouldUseTorForNIP05(url: String) =
when (settings.torSettings.torType.value) {
when (torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip05VerificationsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip05VerificationsViaTor.value)
}
fun shouldUseTorForUploads(url: String) =
when (settings.torSettings.torType.value) {
when (torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value)
}
override fun proxyPortForVideo(url: String): Int? = okHttpClient.getCurrentProxyPort(shouldUseTorForVideoDownload(url))
override fun okHttpClientForNip05(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForNIP05(url))
override fun okHttpClientForUploads(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForUploads(url))
override fun okHttpClientForImage(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForImageDownload(url))
override fun okHttpClientForVideo(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForVideoDownload(url))
override fun okHttpClientForMoney(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForMoneyOperations(url))
override fun okHttpClientForPreview(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForPreviewUrl(url))
override fun okHttpClientForPushRegistration(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForTrustedRelays())
}

View File

@@ -18,20 +18,27 @@
* 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.quartz.nip03Timestamp.ots.okhttp
package com.vitorpamplona.amethyst.model.privacyOptions
import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarBuilder
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit
import okhttp3.OkHttpClient
import java.net.InetSocketAddress
class OkHttpCalendarBuilder(
val clientFn: (url: String) -> OkHttpClient,
) : CalendarBuilder {
override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url, clientFn(url))
class SingleRoleBasedHttpClientBuilder(
val okHttpClient: OkHttpClient,
) : IRoleBasedHttpClientBuilder {
override fun proxyPortForVideo(url: String) = (okHttpClient.proxy?.address() as? InetSocketAddress)?.port
override fun newAsyncCalendar(
url: String,
digest: ByteArray,
): ICalendarAsyncSubmit = OkHttpCalendarAsyncSubmit(url, digest, clientFn(url))
override fun okHttpClientForNip05(url: String) = okHttpClient
override fun okHttpClientForUploads(url: String) = okHttpClient
override fun okHttpClientForImage(url: String) = okHttpClient
override fun okHttpClientForVideo(url: String) = okHttpClient
override fun okHttpClientForMoney(url: String) = okHttpClient
override fun okHttpClientForPreview(url: String) = okHttpClient
override fun okHttpClientForPushRegistration(url: String) = okHttpClient
}

View File

@@ -70,14 +70,14 @@ class Chatroom : NotesGatherer {
}
}
val createdAt = msg.createdAt() ?: 0
if (createdAt > (newestMessage?.createdAt() ?: 0)) {
val createdAt = msg.createdAt() ?: 0L
if (createdAt > (newestMessage?.createdAt() ?: 0L)) {
newestMessage = msg
}
val newSubject = msg.event?.subject()
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
if (newSubject != null && (msg.createdAt() ?: 0L) > (subjectCreatedAt ?: 0)) {
subject.tryEmit(newSubject)
subjectCreatedAt = msg.createdAt()
}
@@ -96,7 +96,7 @@ class Chatroom : NotesGatherer {
msg.removeGatherer(this)
if (msg == newestMessage) {
newestMessage = messages.maxByOrNull { it.createdAt() ?: 0 }
newestMessage = messages.maxByOrNull { it.createdAt() ?: 0L }
}
if (msg.event?.subject() == subject.value) {
@@ -127,7 +127,7 @@ class Chatroom : NotesGatherer {
val sorted = messages.sortedWith(DefaultFeedOrder)
val toKeep =
if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
if ((sorted.firstOrNull()?.createdAt() ?: 0L) > TimeUtils.oneWeekAgo()) {
// Recent messages, keep last 100
sorted.take(100).toSet()
} else {

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