Compare commits

...

2385 Commits

Author SHA1 Message Date
Vitor Pamplona
d2214889b6 Version 1.01.0 2025-08-27 09:10:11 -04:00
Vitor Pamplona
d4da14e4fa Moves draft deletion calculations to the default thread 2025-08-27 08:57:10 -04:00
Vitor Pamplona
56dec11901 Intercepts backhandler to save drafts 2025-08-27 08:56:40 -04:00
Vitor Pamplona
8624dcf2d4 Merge pull request #1437 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-27 08:15:39 -04:00
Crowdin Bot
7edccdb236 New Crowdin translations by GitHub Action 2025-08-27 12:02:53 +00:00
Vitor Pamplona
d8fb49bd52 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-27 08:00:35 -04:00
Vitor Pamplona
108d8a2e57 Improves draft deletes in the feeds 2025-08-27 08:00:08 -04:00
Vitor Pamplona
7b6bd3579c Merge pull request #1436 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-26 20:33:52 -04:00
Crowdin Bot
9f398b5ef8 New Crowdin translations by GitHub Action 2025-08-27 00:20:01 +00:00
Vitor Pamplona
568a175735 fixes fdroid build 2025-08-26 20:18:10 -04:00
Vitor Pamplona
aa80d1b84a - Upgrades NIP-72 communities to NIP-22 comments
- Adds moderation queue feed
- Adds in-app approval procedures
- Adds drafts for community posts
- Redesigns About us section of communities.
2025-08-26 20:10:04 -04:00
Vitor Pamplona
fd752a4997 Adds a restart of the registration once the Post_notification permission changes 2025-08-25 15:43:56 -04:00
Vitor Pamplona
7302d260d9 avoids sending expired events to the cloud 2025-08-25 15:43:12 -04:00
Vitor Pamplona
823540160a Fixes some DM notification issues 2025-08-25 15:42:57 -04:00
Vitor Pamplona
0187529f8d Removes default and bootstrapping lists from the "Trusted Relay set". This should fix the unwanted NOTIFY requests. 2025-08-25 12:16:40 -04:00
Vitor Pamplona
98453c7b65 Fixes unable to delete relays from some lists. 2025-08-25 10:26:39 -04:00
Vitor Pamplona
f43e88eee7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-25 10:10:06 -04:00
Vitor Pamplona
4baa5bf659 Fixes https://github.com/vitorpamplona/amethyst/issues/1070 2025-08-25 10:09:39 -04:00
David Kaspar
ce1db52abb Merge pull request #1433 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-25 14:25:14 +01:00
Crowdin Bot
cc6a9f1818 New Crowdin translations by GitHub Action 2025-08-25 13:14:18 +00:00
Vitor Pamplona
f11ab08f92 Merge pull request #1428 from davotoula/1403-accessibility-talkback-issues
accessibility talkback issues
2025-08-25 09:12:41 -04:00
davotoula
c93cc04409 updated imports 2025-08-23 22:58:16 +01:00
davotoula
56f299569e added translations CS, DE, PT, SV 2025-08-23 22:24:48 +01:00
davotoula
4006aebf35 added I18n for new strings 2025-08-23 18:24:10 +01:00
davotoula
b0aa2a8c31 added accessibility Description
TODO: add translations
2025-08-23 17:12:23 +01:00
davotoula
03483c932c reduce warnings:
remove unused nav parameter
remove unnecessary ? operator
replace .filter().first() with .first
2025-08-23 17:08:17 +01:00
David Kaspar
f3f8beeebe Added semantics to the invisible box
contentDescription - provides current state info
TODO: add translations
2025-08-23 17:06:26 +01:00
David Kaspar
0381e22198 Corrected TextSpinner package 2025-08-23 17:06:05 +01:00
Vitor Pamplona
d90618f7f3 Corrected version 1.00.5 2025-08-23 11:06:45 -04:00
Vitor Pamplona
41ca6d281e Revert "Version 1.00.5"
This reverts commit 83870cd06b.
2025-08-23 11:06:11 -04:00
Vitor Pamplona
1f4f1d4822 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-23 11:05:50 -04:00
Vitor Pamplona
83870cd06b Version 1.00.5 2025-08-23 11:05:22 -04:00
Vitor Pamplona
0d4689b064 Merge pull request #1427 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-23 10:58:18 -04:00
Crowdin Bot
d2e7e171a1 New Crowdin translations by GitHub Action 2025-08-23 14:57:56 +00:00
Vitor Pamplona
aea8573693 Merge pull request #1425 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-23 10:56:53 -04:00
Vitor Pamplona
36b14f93cb Removing multiple relays in the same line. 2025-08-23 10:49:40 -04:00
Vitor Pamplona
356a805681 Adds a child safety standards section 2025-08-22 18:15:12 -04:00
Crowdin Bot
c9e5646df2 New Crowdin translations by GitHub Action 2025-08-22 20:46:42 +00:00
Vitor Pamplona
f50a3c5bbb Merge pull request #1424 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-22 16:45:32 -04:00
Vitor Pamplona
46fe3c2f4a Merge pull request #1423 from newrizen/New-translation
Update pt_rbr strings.xml
2025-08-22 16:45:08 -04:00
Crowdin Bot
5ed0bf48a6 New Crowdin translations by GitHub Action 2025-08-22 20:31:42 +00:00
Vitor Pamplona
0f59befb03 Version 1.00.4 2025-08-22 16:29:56 -04:00
Vitor Pamplona
c1d8aac030 Fixes https://github.com/vitorpamplona/amethyst/issues/1422 2025-08-22 16:29:49 -04:00
Vitor Pamplona
0d3c60b9c2 Fixes the disappearance of drafts. 2025-08-22 16:11:30 -04:00
Vitor Pamplona
fa80912db2 Uses a separate okhttp for DM relays 2025-08-22 13:50:28 -04:00
Vitor Pamplona
f9ea1270fa creates a separate okhttp for relays 2025-08-22 13:35:46 -04:00
Vitor Pamplona
e0ffa6d4fb In case a user does not have an outbox list, defaults to all hints seen for that user. 2025-08-22 12:42:40 -04:00
Vitor Pamplona
3df7129f00 Deletes unused state class 2025-08-22 12:41:03 -04:00
Neil Rizen
692f77dcd1 Update pt_rbr strings.xml
More compact translation, with corrected spelling (there is no "automaticamente-traduzido" with dash in Portuguese, so I changed it to 'autotraduzido' and moved the dash away to use as a ':' ) and visually more pleasing.
2025-08-22 07:56:00 -03:00
Vitor Pamplona
273186ef35 version 1.00.3 2025-08-21 20:13:52 -04:00
Vitor Pamplona
b64d6c24c8 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-21 20:11:54 -04:00
Vitor Pamplona
1f2a153f47 Fixes lack of feed updates for those that didn't follow any communities 2025-08-21 20:11:44 -04:00
Vitor Pamplona
a36f2cc93f Merge pull request #1421 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-21 18:29:13 -04:00
Vitor Pamplona
499316687f Avoids parsing bad NIP-28 objects 2025-08-21 17:19:22 -04:00
Vitor Pamplona
693a75b9ec Don't try to decrypt appData unless it is a writeable account 2025-08-21 17:18:47 -04:00
Crowdin Bot
d0b3c9b019 New Crowdin translations by GitHub Action 2025-08-21 21:12:11 +00:00
Vitor Pamplona
d810aaa2d0 streamlines function calls on AccountViewModel 2025-08-21 17:10:10 -04:00
Vitor Pamplona
965db8b91b trying to remove HEAD from the release build name 2025-08-21 15:13:53 -04:00
Vitor Pamplona
5f7088b7bb Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-21 14:33:27 -04:00
Vitor Pamplona
d7c80ca138 Merge pull request #1420 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-21 14:33:04 -04:00
Vitor Pamplona
75da18a30b Version 1.00.2 2025-08-21 14:32:48 -04:00
Crowdin Bot
897e9d84cb New Crowdin translations by GitHub Action 2025-08-21 18:31:11 +00:00
Vitor Pamplona
fe141d1ee3 Fixes crash when typing a new post without permissions to do a draft post. 2025-08-21 14:29:34 -04:00
Vitor Pamplona
4a7a09c43a Tries to avoid crash when lacking google services in the play version. 2025-08-21 14:20:55 -04:00
Vitor Pamplona
fd26ae5543 Fixes branch name 2025-08-21 14:11:56 -04:00
Vitor Pamplona
631270fc37 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-21 14:09:43 -04:00
Vitor Pamplona
816de5bf03 Fixes community join/leave issue 2025-08-21 14:06:47 -04:00
Vitor Pamplona
1ef9902ece Migrates top nav list to hashtag, geohash and community lists 2025-08-21 13:51:33 -04:00
Vitor Pamplona
704a08e7bb Fixes follow/unfollow from hashtags and geohashes 2025-08-21 13:51:10 -04:00
Vitor Pamplona
ffbf2b4024 Merge pull request #1419 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-21 12:25:23 -04:00
Crowdin Bot
a33ab5c1e9 New Crowdin translations by GitHub Action 2025-08-21 16:22:29 +00:00
Vitor Pamplona
b699b30954 Version 1.00.1 to fix app name 2025-08-21 12:21:48 -04:00
Vitor Pamplona
1d1ee8670e Fixes app name 2025-08-21 12:20:16 -04:00
Vitor Pamplona
bcdf6e5ff7 Sends only play files to the zap store. 2025-08-21 12:17:31 -04:00
Vitor Pamplona
fbb50f4147 version 1.0.0 2025-08-21 11:08:23 -04:00
Vitor Pamplona
4e13c5ab9c Improves timeouts to download and upload files 2025-08-21 08:49:25 -04:00
Vitor Pamplona
e1a5b9c079 Waits for the player to load and sets the current position of the audio 2025-08-20 21:26:59 -04:00
Vitor Pamplona
a27acd9dc6 YakBak in PictureInPicture mode
New audio player UI for YakBak
2025-08-20 21:00:09 -04:00
Vitor Pamplona
2f82233b25 Higher quality audio recordings 2025-08-20 17:37:44 -04:00
Vitor Pamplona
b232f484bf Saves all NIP-51 lists as nip-44 2025-08-20 17:32:58 -04:00
Vitor Pamplona
679f9b66ed Allows the height of the preview to adjust to the width of the preview image 2025-08-20 17:22:04 -04:00
Vitor Pamplona
da015e3298 Enabling the display for amplitudes in an array of floats for YakBak 2025-08-20 16:00:06 -04:00
Vitor Pamplona
4065adac83 Correctly manages airplane mode changes: reconnects Tor and Relays 2025-08-20 12:16:01 -04:00
Vitor Pamplona
7339f0ea3b Merge pull request #1418 from davotoula/update-n71-video-events
Support updated video events (Kind 21/22)
2025-08-20 08:32:20 -04:00
David Kaspar
1bbc67fed7 Added VideoNormalEvent (21) and VideoShortEvent (22)
Updated Account to post events as new 21 / 22 kinds.
Keep showing old event kinds for backwards compatibility
2025-08-20 11:16:02 +01:00
Vitor Pamplona
64f46a55f9 Because the indexer processes Notes in bulk and after the fact, it could be the case where a new draft and a deletion event happen one after the other. If we don't re-check for deletions, the app will reindex the old draft that was already deleted by the LocalCache, creating a memory leak. 2025-08-19 18:54:28 -04:00
Vitor Pamplona
5662eda54c Finishes the routing for note edits for live, public and ephemeral chats as well as public messages. 2025-08-19 18:21:27 -04:00
Vitor Pamplona
f56d927f72 Adds 90 day expiration to all drafts. 2025-08-19 12:40:53 -04:00
Vitor Pamplona
5685f227f9 Fixes relay hints when they disagree with our findings. 2025-08-19 12:20:49 -04:00
Vitor Pamplona
d539eca8bb Avoids chats going behind the status bar up top 2025-08-19 10:42:42 -04:00
Vitor Pamplona
415bf9f522 Corrects some of the keys for Launched effects 2025-08-19 09:02:15 -04:00
Vitor Pamplona
667c15b3a1 Checks if the relay list in NIP-28 groups is empty and uses default when needed. 2025-08-18 19:24:57 -04:00
Vitor Pamplona
f31fa4a9c6 Adds notes to channels to facilitate their loading. 2025-08-18 19:24:23 -04:00
Vitor Pamplona
3bf6e80650 update kotlin, serialization plugin, tor 2025-08-18 19:22:56 -04:00
Vitor Pamplona
3bf2f858f7 Fixes the unable to recreate user after the GC deletes it. 2025-08-18 17:24:34 -04:00
Vitor Pamplona
5c0d71f034 Avoiding concurrent errors when deleting weak references 2025-08-18 16:52:17 -04:00
Vitor Pamplona
d4d60cb1ff Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-18 15:21:06 -04:00
Vitor Pamplona
b2932cd7e3 Updates agp 2025-08-18 15:07:35 -04:00
Vitor Pamplona
9d9b5f03db Merge pull request #1417 from greenart7c3/main
Change BasicOkHttpWebSocket to be like the docs
2025-08-18 13:27:52 -04:00
greenart7c3
e076f97714 Change BasicOkHttpWebSocket to be like the docs 2025-08-18 13:51:40 -03:00
Vitor Pamplona
f5247a5443 Moves reconnections to 1 minute not 10 seconds 2025-08-18 11:37:32 -04:00
Vitor Pamplona
92e9978859 Moves to parcelable.
Fixes: https://github.com/vitorpamplona/amethyst/issues/1413
2025-08-18 11:25:57 -04:00
Vitor Pamplona
3520fe89ae Merge pull request #1414 from davotoula/1413-use-pubkeyhex-for-lazy-column-key
Use pubkeyhex for lazy column key instead of Address
2025-08-18 11:13:23 -04:00
Vitor Pamplona
01ed0f3fc1 Merge pull request #1415 from davotoula/target-field-specifically-for-constructor-params-for-serialization
Use explicit field annotation target for constructor params
2025-08-18 11:13:08 -04:00
David Kaspar
41545a156b Use explicit annotation target for constructor params
See https://youtrack.jetbrains.com/issue/KT-73255 for more details
2025-08-18 10:44:42 +01:00
David Kaspar
b38e9247ce Use pubkeyhex for lazy column key instead of Address 2025-08-17 23:02:25 +01:00
Vitor Pamplona
45ec9e3a62 Merge pull request #1411 from davotoula/reduce-duplications-large-cache
Reduce duplications: large cache
2025-08-16 16:14:13 -04:00
David Kaspar
c1004dc643 Use .get( instead of index to keep to code base coding style 2025-08-16 18:56:48 +01:00
David Kaspar
a72fff5f8f Lifted out collectors from CacheCollectors object 2025-08-16 18:50:01 +01:00
Vitor Pamplona
fca4aee5f5 Adds a user list to each connected relay to know where this is coming from 2025-08-16 13:21:10 -04:00
David Kaspar
a82f4aacbe Use val for references that do not change (eg results) 2025-08-16 18:17:27 +01:00
David Kaspar
f21c7bd258 Lint issues: remove unused methods, use indexing operator instead of .get() 2025-08-16 11:21:19 +01:00
David Kaspar
533d8b18f0 Remove duplicate functionality from Cache implementations and use instead common methods in CacheOperations utility class
Create override for forEach and size as they are needed internally and do slightly different things
2025-08-16 10:45:37 +01:00
David Kaspar
b069f283ac Move common cache operations to new utility class 2025-08-16 10:38:57 +01:00
David Kaspar
eab2f96f02 remove duplicate colletors and Use collectors from new utility class 2025-08-16 10:20:52 +01:00
David Kaspar
c4b71a0aea Move common collectors to new utility class 2025-08-16 10:09:56 +01:00
Vitor Pamplona
88933fba8d Fixes the time to reconnect interval due to an error between seconds and milliseconds. 2025-08-15 18:07:12 -04:00
Vitor Pamplona
ef049e88ba Adds a new mechanism to wake up all connections when movement is made in the app. 2025-08-15 18:06:21 -04:00
Vitor Pamplona
6f27c96aed changes the sendEvent from forcing a connection to waiting for the time to connect. 2025-08-15 18:04:57 -04:00
Vitor Pamplona
5b605700a3 Adds 10 seconds ago 2025-08-15 18:03:57 -04:00
Vitor Pamplona
e867571ea6 Improves version finder 2025-08-15 14:39:53 -04:00
Vitor Pamplona
f4759c283e Adds a basic OkHttp builder to Quartz (new dependency :( )
Adds some instructions to the basic readme.md
2025-08-15 14:33:05 -04:00
Vitor Pamplona
4887e6f3a1 Sends a disconnect signal to the clients 2025-08-15 09:43:37 -04:00
Vitor Pamplona
1392833179 no need for the scope on the cache connector 2025-08-15 09:18:42 -04:00
Vitor Pamplona
529a1f403e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-15 08:41:22 -04:00
Vitor Pamplona
c9b16309e2 Clears the socket from memory upon closing. 2025-08-15 08:40:33 -04:00
Vitor Pamplona
81db455bfb Updates compose and fragment 2025-08-15 08:36:08 -04:00
Vitor Pamplona
c017b9cace Merge pull request #1410 from greenart7c3/main
Fix non ssl connections
2025-08-15 06:57:29 -04:00
greenart7c3
25f5551a98 Fix non ssl connections 2025-08-15 07:35:01 -03:00
Vitor Pamplona
d8461c2a74 Growing the number of possibilities to download profile information from 2025-08-14 19:47:20 -04:00
Vitor Pamplona
25dec49e97 Reactivates event log 2025-08-14 18:58:33 -04:00
Vitor Pamplona
424d5ccc9c Refactoring 2025-08-14 17:32:28 -04:00
Vitor Pamplona
422a018a1f Fixes when posts have published_at in milliseconds. 2025-08-14 17:32:15 -04:00
Vitor Pamplona
5b8afb6f60 Updates logging for auth 2025-08-14 17:06:26 -04:00
Vitor Pamplona
38827a4db6 Uses relays where the event was seen as a source of reactions and replies 2025-08-14 16:20:53 -04:00
Vitor Pamplona
97bad5707f Improves filter by last date of the feed 2025-08-14 16:09:26 -04:00
Vitor Pamplona
58c8ff646f Moves the feed update to 5 seconds. 2025-08-14 16:04:27 -04:00
Vitor Pamplona
613a3b4912 Adds a limit of 1 week to load videos and hashtags on shorts 2025-08-14 16:03:48 -04:00
Vitor Pamplona
a67155ac25 Only loads notifications from random relays after the notification feed is done 2025-08-14 16:03:19 -04:00
Vitor Pamplona
ba27b68c14 Minor refactoring 2025-08-14 15:43:43 -04:00
Vitor Pamplona
dfe5f86a23 Faster PublishedAt providers 2025-08-14 11:28:03 -04:00
Vitor Pamplona
abf0c4fd70 Removes broadcasting relays from the group of relays used to find events. 2025-08-14 10:49:30 -04:00
Vitor Pamplona
ed675a4973 Merge pull request #1408 from davotoula/reduce-duplication-resuable-zap-button
Deduplication of code: ZapTheDevsCard, DvmContentDiscoveryScreen
2025-08-14 09:51:38 -04:00
Vitor Pamplona
6514bacf69 Merge pull request #1409 from davotoula/1406-display-app-picture-when-clicked-on-app-picture
Display app picture when clicked on app picture
2025-08-14 09:51:06 -04:00
David Kaspar
e682a95822 Changed ZoomableImageDialog to show app picture rather than banner
Moved it inside picture?.let to guard against null picture
2025-08-14 14:34:11 +01:00
David Kaspar
d367a1f281 Specify return type as non-nullable asJackson will either return a non-null value or throw an exception. 2025-08-14 11:23:23 +01:00
Vitor Pamplona
f5881d4248 avoids using broadcasting relays to build filters to download home data 2025-08-13 19:11:54 -04:00
Vitor Pamplona
4c0f223673 Draft event refactoring 2025-08-13 19:11:29 -04:00
David Kaspar
aae520d2e4 Created ReusableZapButton with common zap button logic
ZapDVMButton refactored to use ReusableZapButton with DVM-specific configuration
2025-08-13 20:59:56 +01:00
Vitor Pamplona
251e5535d0 Removes hash logs 2025-08-12 18:57:11 -04:00
Vitor Pamplona
a034b495e8 Removes checks 2025-08-12 18:57:01 -04:00
Vitor Pamplona
41c2097f3c reduces logs 2025-08-12 18:55:55 -04:00
Vitor Pamplona
22dc037bf7 Moves to disable tor if on the background 2025-08-12 18:55:47 -04:00
Vitor Pamplona
346fb61507 Small refactoring 2025-08-12 17:55:24 -04:00
Vitor Pamplona
c17d66b7e9 Small optimization 2025-08-12 17:28:22 -04:00
Vitor Pamplona
478b2d12eb Speeding up the parsing of relay urls 2025-08-12 17:16:14 -04:00
Vitor Pamplona
c14d976ad1 Minor refactoring 2025-08-12 17:15:23 -04:00
Vitor Pamplona
d605eb3b0a Fixes lack of update on chats and channels due to the new indexer being decoupled form LocalCache. 2025-08-12 16:37:32 -04:00
Vitor Pamplona
25aaed80b0 Better name for the newestMessage of a chat 2025-08-12 11:43:57 -04:00
Vitor Pamplona
14582fe9b6 Recalculating subject and last message when deletes happen 2025-08-12 11:29:58 -04:00
Vitor Pamplona
7a38269449 removing unused libs 2025-08-12 11:29:34 -04:00
Vitor Pamplona
77bb34eaf8 Updates tor to 16KB-page files and firebase 2025-08-12 08:47:51 -04:00
Vitor Pamplona
d0f3dde08b Migrates to a more obvious open/close subscription model for NostrClient 2025-08-11 10:34:10 -04:00
Vitor Pamplona
d5b9d5e80b Closes subscription when finished by timeout. 2025-08-11 10:33:21 -04:00
Vitor Pamplona
549074ef13 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-07 18:04:42 -04:00
Vitor Pamplona
2315051504 - Makes zap calculation a sync routine
- Optimizes the display of the yellow zap when the zap event comes before the pay payment confirmation
- Fixes animations for a new zap over an existing zap.
2025-08-07 18:02:31 -04:00
Vitor Pamplona
8ba96ca527 Simplifies Zap animation sizes 2025-08-07 16:39:44 -04:00
Vitor Pamplona
10c5db8281 Fixes the encryption of NWC 2025-08-07 16:07:15 -04:00
Vitor Pamplona
86b4957067 Removes NWC events from notificatuions 2025-08-07 15:35:59 -04:00
Vitor Pamplona
fe19b8b4af Merge pull request #1405 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-07 14:37:59 -04:00
Crowdin Bot
3532a0cadd New Crowdin translations by GitHub Action 2025-08-07 18:27:02 +00:00
Vitor Pamplona
58277d9697 Moves build tools of github actions to 36 2025-08-07 14:24:45 -04:00
Vitor Pamplona
cf76ab9f0e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-07 10:13:18 -04:00
Vitor Pamplona
51d7fa3aac Fixes missing method on build 2025-08-07 10:09:43 -04:00
Vitor Pamplona
10832e10c8 Fixes the background color behind the show more button on light mode 2025-08-07 09:50:20 -04:00
David Kaspar
1fce054c9f Merge pull request #1404 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-07 15:27:14 +02:00
Crowdin Bot
f7b3dada05 New Crowdin translations by GitHub Action 2025-08-07 13:25:23 +00:00
Vitor Pamplona
f35376a7da Small refactoring 2025-08-06 19:13:15 -04:00
Vitor Pamplona
d0f3630abb Changes to show the user name before the text on public messages. 2025-08-06 18:26:11 -04:00
Vitor Pamplona
d22fc2874b Don't show public messages on the feed, just on notifications 2025-08-06 18:25:20 -04:00
Vitor Pamplona
23b3ff9fe8 Adds missing methods to display as Public message 2025-08-06 18:09:14 -04:00
Vitor Pamplona
52ac0c184d Passes reply preview to the new Public message screen 2025-08-06 18:07:30 -04:00
Vitor Pamplona
923ddf1c83 Shows all public message events on conversations 2025-08-06 18:05:14 -04:00
Vitor Pamplona
cc186ef91b Fixes route maker 2025-08-06 18:04:44 -04:00
Vitor Pamplona
02def3e0b1 Doesn't pass deleted drafts ahead to the indexer anymore. It's not needed. 2025-08-06 18:02:54 -04:00
Vitor Pamplona
036b385f38 Adds a check for deletion before processing inner posts 2025-08-06 18:02:26 -04:00
Vitor Pamplona
fdff92dff9 New Public Message screen: to field should get focus if it is empty, otherwise the message field should have the focus 2025-08-06 18:01:19 -04:00
Vitor Pamplona
8c854c43be Adds p-tags to the display of public messages so that users know they are being cited 2025-08-06 17:58:51 -04:00
Vitor Pamplona
d707a951c3 Adds an option to pass a request focus parameter to the MessageField in new post screens 2025-08-06 17:58:18 -04:00
Vitor Pamplona
b1566d8f8b Update log descriptions to match the new deleted flow 2025-08-06 17:57:46 -04:00
Vitor Pamplona
481db7ebeb Loads and shows public messages in the user's profile as replies to others 2025-08-06 17:57:06 -04:00
Vitor Pamplona
a18c6c0bc8 Shows public messages in the profile's mutual feed 2025-08-06 17:56:26 -04:00
Vitor Pamplona
4d6f6d0ed5 removes a launch call from an already launched coroutine 2025-08-06 17:55:52 -04:00
Vitor Pamplona
1b043603f9 Adds a method to create nprofiles without creating a relay list first 2025-08-06 17:54:34 -04:00
Vitor Pamplona
dddc9e1346 Simplifies the startWith check 2025-08-06 17:13:37 -04:00
Vitor Pamplona
b5662fd05a normalizes the parameter name across all adaptive filters 2025-08-06 14:09:06 -04:00
Vitor Pamplona
726f9e2f4f Moves target compatibility to java 21 2025-08-06 14:07:51 -04:00
Vitor Pamplona
ca55472010 Increases the limit for notification events to fill up a week's worth of inbox 2025-08-06 13:20:00 -04:00
Vitor Pamplona
71bbae2f9b Simple refactoring 2025-08-06 13:18:45 -04:00
Vitor Pamplona
1971956443 Setting up the boundary for one week ago for notifications 2025-08-06 13:18:13 -04:00
Vitor Pamplona
282935c5f3 Adds search relays as fallback for event finding activities 2025-08-06 08:55:35 -04:00
Vitor Pamplona
d4d7d58378 Maybe separate assemblies work 2025-08-05 17:42:57 -04:00
Vitor Pamplona
5d1e3e804f Trying to see if we can get benchmark flavors out of github actions 2025-08-05 17:20:25 -04:00
Vitor Pamplona
9475978612 Fixes background of transparent posts. 2025-08-05 16:42:13 -04:00
Vitor Pamplona
c32a64fbfe avoids indexing via DraftTags and removes notes from the reply list when they get updated. 2025-08-05 16:30:36 -04:00
Vitor Pamplona
da40de49cd Refactors the DecryptAndIndex to avoid a service class 2025-08-05 16:30:00 -04:00
Vitor Pamplona
e237853a8f Making the new signer work for readonly accounts 2025-08-05 16:06:52 -04:00
Vitor Pamplona
63f62167a4 Simplifying the refactoring of the DecryptAndIndexProcessor 2025-08-05 15:04:23 -04:00
Vitor Pamplona
4675212e1d Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-05 12:06:17 -04:00
Vitor Pamplona
f1b15cc3c5 Cleaning up the saving draft concurrency issues. 2025-08-05 11:48:02 -04:00
Vitor Pamplona
a747f25906 Adds exception handlers for managed coroutine scopes 2025-08-05 11:47:03 -04:00
Vitor Pamplona
e72ae565d3 Adds an extra catch in case there is a decryption error 2025-08-05 11:46:29 -04:00
Vitor Pamplona
be41627c2b Fixes geohash cache saving 2025-08-05 10:45:20 -04:00
Vitor Pamplona
426378a7ee Merge pull request #1402 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-05 09:55:06 -04:00
Crowdin Bot
f9efc0ae2d New Crowdin translations by GitHub Action 2025-08-05 13:51:50 +00:00
Vitor Pamplona
09dfa79dea Merge pull request #1395 from davotoula/refactor-decrypt-and-index-processor-light
Refactor decrypt and index processor
2025-08-05 09:50:12 -04:00
Vitor Pamplona
8af955ce20 Adds an extra layer on the url normalization cache to avoid recomputing failed attempts before. 2025-08-04 18:18:39 -04:00
Vitor Pamplona
5b0fccb9e0 Merge pull request #1401 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-04 11:44:47 -04:00
Crowdin Bot
bef607ec41 New Crowdin translations by GitHub Action 2025-08-04 15:14:07 +00:00
Vitor Pamplona
bfbdc65bef Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-08-04 11:09:24 -04:00
Vitor Pamplona
cfb2f98f66 Merge pull request #1396 from davotoula/dynamic-version-number-based-on-branch-name
Dynamic version number based on branch name
2025-08-03 12:29:56 -04:00
David Kaspar
fb0597b2f4 Merge pull request #1399 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-03 18:24:23 +02:00
Crowdin Bot
c663fd0f5a New Crowdin translations by GitHub Action 2025-08-03 16:22:02 +00:00
David Kaspar
2a8b258f80 Merge pull request #1398 from davotoula/update-translations
update DE
2025-08-03 18:20:53 +02:00
David Kaspar
9c0663db69 update DE 2025-08-03 18:16:24 +02:00
David Kaspar
8d8edbaf90 executes a Git command to get the current branch name.
If the branch is "main" or "master", returns the base version unchanged
For feature branches, appends the branch name to the version
2025-08-03 11:47:26 +02:00
David Kaspar
5420ddd338 lets pass LocalCache instead of account.cache, as it was before 2025-08-03 10:35:40 +02:00
David Kaspar
fa80cb297d reduce lint warnings 2025-08-03 10:32:04 +02:00
David Kaspar
cdfb1db253 refactor DecryptAndIndexProcessor
EventProcessor: Orchestrates the processing flow
DecryptionService: Handles all decryption operations
IndexingService: Manages event indexing and relationships
ChatroomService: Manages chatroom operations
2025-08-03 10:29:05 +02:00
David Kaspar
52b87fa17a Merge pull request #1394 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-03 10:11:31 +02:00
Crowdin Bot
f209b17811 New Crowdin translations by GitHub Action 2025-08-03 07:36:52 +00:00
David Kaspar
a3d38aab1b Merge pull request #1391 from davotoula/update-translations
Update translations. CZ, SE, PT, DE
2025-08-03 09:35:39 +02:00
Vitor Pamplona
52df3cba02 Updates junit, benchmark, media3, expresso, compose, and crypto 2025-08-01 18:48:31 -04:00
David Kaspar
bb8a71f47b Update translations. CZ, SE, PT, DE 2025-08-01 23:37:14 +02:00
Vitor Pamplona
d78d3aeab3 Merge pull request #1390 from davotoula/upgrade-agp
Upgrade agp to 8.12.0
2025-08-01 17:30:59 -04:00
Vitor Pamplona
7f6435ba57 Updates build tools to java 21 2025-08-01 17:07:31 -04:00
Vitor Pamplona
1a58603477 Merge pull request #1389 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-08-01 16:58:20 -04:00
David Kaspar
4b36da59ec Upgrade agp to 8.12.0 2025-08-01 22:55:28 +02:00
Crowdin Bot
9257305820 New Crowdin translations by GitHub Action 2025-08-01 20:40:51 +00:00
Vitor Pamplona
e414c4be98 Merge pull request #1388 from vitorpamplona/full-outbox
Full outbox
2025-08-01 16:39:30 -04:00
Vitor Pamplona
340760513f Better handling index cleanup when draft events get deleted 2025-08-01 16:11:24 -04:00
Vitor Pamplona
eed4350bca Simplifying badge composables 2025-08-01 16:10:34 -04:00
Vitor Pamplona
6b946a57c2 Moves the exceptions to our own signing exceptions 2025-08-01 16:09:52 -04:00
Vitor Pamplona
26cadfee88 Fixes the index after deletions 2025-08-01 11:40:19 -04:00
Vitor Pamplona
4cd24e5363 No need for not null checks 2025-08-01 09:57:33 -04:00
Vitor Pamplona
e715a74bc4 Better indexes notes in channels and chatrooms 2025-08-01 08:30:44 -04:00
Vitor Pamplona
42738e72f7 fixes test case 2025-07-31 19:14:43 -04:00
Vitor Pamplona
add8f8eba2 Improves the rendering of the node id 2025-07-31 19:12:10 -04:00
Vitor Pamplona
e82c09e1c3 Finishes the migration of adressable ids on the local cache. 2025-07-31 19:11:55 -04:00
Vitor Pamplona
e1b5f0f7b5 Renames chatroom and message fields 2025-07-31 19:11:08 -04:00
Vitor Pamplona
7e9d5d6063 Uses new tag model on the ZapEvent 2025-07-31 19:06:34 -04:00
Vitor Pamplona
141f0ed63b Removes unused items from Node 2025-07-31 19:06:09 -04:00
Vitor Pamplona
c5645b0ae7 Removes suspend of NostrSignerSync-based functions 2025-07-31 17:45:44 -04:00
Vitor Pamplona
fe8f519dbf Correctly logs off from the current account when creating new accounts from inside an existing account. 2025-07-31 17:45:21 -04:00
Vitor Pamplona
14e860928b Improves names of the repost methods 2025-07-31 17:44:04 -04:00
Vitor Pamplona
ba460e011e Adds a lower boundary for inbox from random relays. 2025-07-31 12:22:22 -04:00
Vitor Pamplona
12f793ad51 The gentle shutdown doesn't really work. There could be too many send messages waiting to be processed. 2025-07-31 12:21:44 -04:00
Vitor Pamplona
504d582d54 Tuning limits for Global feeds 2025-07-31 11:31:36 -04:00
Vitor Pamplona
a59cd590bc Deletes these very confusing mocks into account viewmodels for the test 2025-07-30 20:20:56 -04:00
Vitor Pamplona
2e9696745a Avoids sending filters on new connections if the client is not active 2025-07-30 18:23:02 -04:00
Vitor Pamplona
5d4b16641e Fixes count call 2025-07-30 18:11:11 -04:00
Vitor Pamplona
1ddc9c216b Marks which relays have been sending unsupported events. 2025-07-30 18:06:24 -04:00
Vitor Pamplona
349dfe3d42 Slowdown updates to random relay requests. 2025-07-30 18:05:53 -04:00
Vitor Pamplona
521a41d81b avoids logging with sub ms times 2025-07-30 17:50:21 -04:00
Vitor Pamplona
c6e103ff87 Delete unnecessary logs 2025-07-30 17:33:56 -04:00
Vitor Pamplona
e70f1fedc6 Adds a coroutine to the relay class so that we can defer processing events 2025-07-30 17:33:12 -04:00
Vitor Pamplona
651ca9a3ef No need to update status on EOSE 2025-07-30 17:31:38 -04:00
Vitor Pamplona
bd7dba1a31 Fixes more random relay hint errors 2025-07-30 17:31:24 -04:00
Vitor Pamplona
cf1d66bc01 new parameter to only log when a given amount of ms have passed. 2025-07-30 16:50:36 -04:00
Vitor Pamplona
414e933686 Only trigger new filters if this has changed 2025-07-30 16:49:48 -04:00
Vitor Pamplona
fa53ff2c78 Adds an iMeta cache for Picture and Video kinds and speeds up the procedure to verify if a video or picture is supported. 2025-07-30 12:47:31 -04:00
Vitor Pamplona
2c6a084808 Makes sure the NostrClient stays off in the background 2025-07-30 10:37:30 -04:00
Vitor Pamplona
8abba9246c Adds support to reply to Yaks with another voice message. 2025-07-29 20:07:28 -04:00
Vitor Pamplona
c9de32c3fa fixes voice track bugs 2025-07-29 15:08:47 -04:00
Vitor Pamplona
35a0b04020 Adds view support for public messages 2025-07-29 15:03:44 -04:00
Vitor Pamplona
4cc5aaa49d Adds view support for YakBak 2025-07-29 15:02:16 -04:00
Vitor Pamplona
bbf6e02609 Beginnings for the migration to DataStore 2025-07-28 17:13:57 -04:00
Vitor Pamplona
7ba2810423 Quick event store using SQLite directly. 2025-07-28 16:49:39 -04:00
Vitor Pamplona
88f9beafd3 Faster expiration calculation and tag array serialization 2025-07-28 16:48:42 -04:00
Vitor Pamplona
0116bc30e4 Creates a TagArray deserializer that interns all strings to avoid duplicated memory 2025-07-28 10:53:55 -04:00
Vitor Pamplona
a59fe59657 Sending eventDB companion to avoid recreating it all the time 2025-07-27 16:52:28 -04:00
Vitor Pamplona
e6dfa07526 Fixes expiration builder 2025-07-27 16:51:59 -04:00
Vitor Pamplona
8e3594cd75 Adds a way to delete events by address only 2025-07-27 16:25:24 -04:00
Vitor Pamplona
d5bbf4b00b Adds basic events to the seacheable interface 2025-07-27 16:25:09 -04:00
Vitor Pamplona
dc68d6025e Adds support for the Request to Vanish NIP - 62 2025-07-27 16:24:10 -04:00
Vitor Pamplona
f8f59abdf1 Small refactoring 2025-07-25 14:33:14 -04:00
Vitor Pamplona
3a37d5e42d Fixes the benchmark testing cases 2025-07-25 14:29:14 -04:00
Vitor Pamplona
ca9777be83 Creates a kind type alias 2025-07-25 12:42:19 -04:00
Vitor Pamplona
e7a9890eb1 Fixes quartz test cases for the new structure 2025-07-25 12:41:43 -04:00
Vitor Pamplona
39152d4e90 Makes the metadata event and contact list addressables 2025-07-25 12:40:55 -04:00
Vitor Pamplona
96e26cfb7d Adds a parser and assembler for NIP47 2025-07-25 12:39:55 -04:00
Vitor Pamplona
8844bb73ea Defines a searcheable interface for events to implement and provide content for a full text search 2025-07-25 12:39:38 -04:00
Vitor Pamplona
180a1eb1db Refactors the use of Gzip 2025-07-25 12:39:11 -04:00
Vitor Pamplona
b77127df77 Minor adjustment 2025-07-24 11:05:08 -04:00
Vitor Pamplona
803b231aaa Moves firebase to the main module 2025-07-24 10:16:56 -04:00
Vitor Pamplona
b27a12b9b5 Finally solved the issue with @aar in gradle 2025-07-24 10:11:36 -04:00
Vitor Pamplona
e7f04ad38d updates spotless 2025-07-24 10:01:20 -04:00
Vitor Pamplona
25e92666e0 Adjusts the gradle DSL language to the latest deprecations 2025-07-24 09:53:00 -04:00
Vitor Pamplona
e76bffd048 Updates coil, compose, firebase, jackson, lifecyle, mockk, navigation 2025-07-24 09:37:40 -04:00
Vitor Pamplona
558a060994 Merge remote-tracking branch 'origin/HEAD' into full-outbox
# Conflicts:
#	gradle/libs.versions.toml
2025-07-24 09:27:01 -04:00
Vitor Pamplona
14055894dc Refactoring in the relay url normalizer 2025-07-24 09:22:24 -04:00
Vitor Pamplona
a494257a27 Adds Proxy relay list and adds support for fixed list of relays. 2025-07-23 19:09:04 -04:00
Vitor Pamplona
1084d2a063 Adds an indexer relay list 2025-07-23 13:19:46 -04:00
Vitor Pamplona
57e4d76da0 Adds broadcasting relay section 2025-07-23 11:35:25 -04:00
Vitor Pamplona
5ac4314e2a Improvements to the online check 2025-07-23 11:33:43 -04:00
Vitor Pamplona
a7dbd787f4 Creates a garbage collector-friendly note cache. 2025-07-23 10:50:51 -04:00
Vitor Pamplona
3a0d7ddac1 Removes unnecessary spec in Channel 2025-07-23 09:26:07 -04:00
Vitor Pamplona
c221506e78 Fixes square loading image on Follow packs 2025-07-23 08:50:14 -04:00
Vitor Pamplona
064a732ec8 Fixes leaving and joining channels 2025-07-23 08:44:41 -04:00
Vitor Pamplona
95f463b0aa Showing ephemeral chats in the messages list.
Fixing a bug of the last message with unloaded event
2025-07-22 20:12:43 -04:00
Vitor Pamplona
6d4708bbeb Implements a partial page system to load feeds. 2025-07-22 19:02:23 -04:00
Vitor Pamplona
1d9b2cffa2 Filtering Addressable notes from Notification's note stack 2025-07-22 15:05:17 -04:00
Vitor Pamplona
6c77bf94a8 Fixing mark as read 2025-07-22 14:09:47 -04:00
Vitor Pamplona
37ff0070ee Solves the hanging AccountViewModel scope when Amethyst is forced closed. 2025-07-22 13:38:30 -04:00
Vitor Pamplona
0f13202f84 Corrects a bug of not showing the user's own messages on chat 2025-07-22 12:46:05 -04:00
Vitor Pamplona
a3ec4d4260 Completing Intent writing implementation 2025-07-22 10:54:24 -04:00
Vitor Pamplona
e6b5d51cda Fixes NIP-55 return as a list of results on the intent. 2025-07-22 10:34:42 -04:00
Vitor Pamplona
8fe13717dd Fixes trying to parse empty addresses 2025-07-22 10:34:09 -04:00
Vitor Pamplona
c7e81d1378 Refactoring Channel modules 2025-07-22 10:33:50 -04:00
Vitor Pamplona
8e6d71e422 Minimizes the dependency on the Account further by moving Tor settings to a tor Privacy State 2025-07-21 19:09:06 -04:00
Vitor Pamplona
95c6dcce5e Moves Ots to its own state class 2025-07-21 17:34:48 -04:00
Vitor Pamplona
6066c7d06d Removes relay lists from notifications 2025-07-21 17:33:31 -04:00
Vitor Pamplona
1c5acc00f3 More appropriate limits 2025-07-21 10:11:03 -04:00
David Kaspar
d8fd9f127d Merge pull request #1387 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-07-20 16:12:05 +02:00
Crowdin Bot
0037d642b0 New Crowdin translations by GitHub Action 2025-07-20 14:11:35 +00:00
Vitor Pamplona
3b0c1b6d4f Merge pull request #1386 from davotoula/upgrade-agp
Upgrade agp to 8.11.1
2025-07-20 10:10:29 -04:00
David Kaspar
4855e36e4c Upgrade agp to 8.11.1 2025-07-20 11:46:26 +02:00
Vitor Pamplona
8a3bb6e1ae Significantly reduces the limit in our filters to reduce the amount of duplicated events coming from multiple relays. 2025-07-18 18:35:52 -04:00
Vitor Pamplona
13ce0e7311 Breaks down the amount of reactions for each post to download from the amount of reactions to communities and chats themselves (which is much larger) 2025-07-18 18:34:54 -04:00
Vitor Pamplona
ee67f2ded9 Adds a subscription to download notifications from random relays in case users are not outbox ready yet. 2025-07-18 18:33:50 -04:00
Vitor Pamplona
19327b682c Improves external signer logs 2025-07-18 17:51:43 -04:00
Vitor Pamplona
85dd9dd5a4 Fixes screen clean up in Login 2025-07-18 17:47:56 -04:00
Vitor Pamplona
9d9963d324 Fixes saving old version of event in to the cache. 2025-07-18 17:47:11 -04:00
Vitor Pamplona
58e353fb40 Moves NIP-55 calls to be suspending functions.
Moves NIP-55 calls to include an ID per call, not per event.
Adds error handling facilities to the Signer functions.
Moves the indexing of the decrypted objects to outside the LocalCache
Migrates Signers to become suspending functions.
Migrates Decryption caching systems to outside the Events themselves.
Migrates all NIP-51 lists to the new structure.
Migrates Drafts and NIP-04 and NIP-17 DMs to the new structure
Migrates Bookmarks to the new structure.
Changes the Room route to avoid using hashcode.
2025-07-18 17:17:27 -04:00
Vitor Pamplona
7bdb3a1a18 Fixing method name 2025-07-14 11:50:15 -04:00
Vitor Pamplona
850277fdc1 Optimize imports 2025-07-14 11:29:21 -04:00
Vitor Pamplona
597fec5c43 Removes lazy StateFlows due to inconsistent starting values. 2025-07-14 11:19:12 -04:00
Vitor Pamplona
909eb62db6 Moves private zap decryption to async, all at once. 2025-07-14 11:18:56 -04:00
Vitor Pamplona
510583e72e Rewrites NIP-55 to start offering better error handling. 2025-07-14 11:17:59 -04:00
Vitor Pamplona
f93b08a272 Refactors master serializer name from EventMapper to JsonMapper 2025-07-11 17:10:51 -04:00
Vitor Pamplona
bb5ae5d09e Fixes F-droid compilation due to a duplicated class from a unifiedpush crypto bug 2025-07-11 14:55:13 -04:00
Vitor Pamplona
02a9563de6 Updates spotless 2025-07-11 14:33:43 -04:00
Vitor Pamplona
15efd671ec Reorganizes navigation and unifies topbar designs 2025-07-11 14:05:31 -04:00
Vitor Pamplona
e37d6ebd8b Moves media server from Dialog to Screen with full route. 2025-07-11 09:22:27 -04:00
Vitor Pamplona
2ecfeb6e7f Moves subscription management from Ammolite to Quatrz 2025-07-11 08:51:02 -04:00
Vitor Pamplona
a94c8673d5 Fixing Ephemeral Chat interface 2025-07-10 19:42:18 -04:00
Vitor Pamplona
e3acf4c250 Fixes some of the live stream flags when the event doesn't update. 2025-07-10 18:22:38 -04:00
Vitor Pamplona
dc61c5a8e4 Keeps cache of the content inside Public chat forums 2025-07-10 18:08:32 -04:00
Vitor Pamplona
f3e08bd297 No need for this OkHttp log anymore 2025-07-10 18:08:09 -04:00
Vitor Pamplona
62253d8601 Finish the migration to separate channels 2025-07-10 18:07:57 -04:00
Vitor Pamplona
5daaa6364d Fixes issue with unable to parse text-based content in the event. 2025-07-10 18:07:01 -04:00
Vitor Pamplona
a706005cfd Fixes recent label reuse 2025-07-10 18:06:30 -04:00
Vitor Pamplona
4eb348654f Providing a delete fix for empty drafts. 2025-07-10 17:04:19 -04:00
Vitor Pamplona
191f1344db Starts diskcache together with video cache 2025-07-10 17:02:31 -04:00
Vitor Pamplona
a207d80d55 Separating Reports from Metadata watching 2025-07-10 15:51:57 -04:00
Vitor Pamplona
94a7d9cf14 Makes sure the list is not empty before building the filter 2025-07-10 14:52:27 -04:00
Vitor Pamplona
f1d39b8517 Logs the relays used for deletion events and checks if it hasn't sent the deleted event to each relay already 2025-07-10 14:36:31 -04:00
Vitor Pamplona
7a14d95973 Adds more relays to the DVM inbox.
Sends the new EVENT only after the screen updates.
2025-07-10 14:34:09 -04:00
Vitor Pamplona
8dbb1a889e Avoids displaying loading symbol on our cached AsyncImage implementation 2025-07-10 11:49:16 -04:00
Vitor Pamplona
ca41d4e10a Refactoring 2025-07-10 11:13:52 -04:00
Vitor Pamplona
ee8ffcd03d Fixes some borders of preview cards 2025-07-10 11:08:39 -04:00
Vitor Pamplona
f9189e9ec1 Further separates channels by kind in the UI 2025-07-10 10:57:42 -04:00
Vitor Pamplona
ec985d58e7 Fixes matching image borders in the loading step of preview cards 2025-07-10 09:43:36 -04:00
Vitor Pamplona
a2d16c173c Fixing crash when the last : is not present in an address 2025-07-10 09:38:53 -04:00
Vitor Pamplona
123803edd3 Fixes reply count 2025-07-09 17:48:19 -04:00
Vitor Pamplona
35005dddad Adds checks in the filter to highlight issues with empty or invalid filters 2025-07-09 17:20:59 -04:00
Vitor Pamplona
accc8d4439 Brute forces the finding of events. 2025-07-09 16:57:26 -04:00
Vitor Pamplona
267d4c505a Moves OkHttp to the coroutines version and turns all their usage into suspend functions 2025-07-09 15:14:58 -04:00
Vitor Pamplona
b502ab7de8 Fixes issue on saving NIP47 address 2025-07-09 15:14:25 -04:00
Vitor Pamplona
7eb39b3c4c Readjusts relay view on profile 2025-07-09 09:03:56 -04:00
Vitor Pamplona
17e33c61e7 Refactoring base EOSE managers to provide better logging 2025-07-09 09:03:45 -04:00
Vitor Pamplona
6851b1bb51 Only closes after removing the subscriptions to avoid resubscribing while closing. 2025-07-09 08:37:21 -04:00
Vitor Pamplona
b12e675b3a Refactors the name of the simple relay client 2025-07-09 08:36:50 -04:00
Vitor Pamplona
cc02806390 Fixing weird version number on the left drawer 2025-07-08 19:27:43 -04:00
Vitor Pamplona
2ee1285ea1 Removes old follows in Contact lists. 2025-07-08 19:16:28 -04:00
Vitor Pamplona
c5ed0cfa60 Improves Image load screens in the preview cards 2025-07-08 18:43:10 -04:00
Vitor Pamplona
4084a72178 Minor refactoring in Account 2025-07-08 18:17:18 -04:00
Vitor Pamplona
2c3e641639 Merge branch 'full-outbox' of nostr://vitorpamplona.com/relay.ngit.dev/amethyst into full-outbox
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
2025-07-08 18:06:38 -04:00
Vitor Pamplona
8e0b94646a Includes Trusted Relays in all my relays 2025-07-08 18:05:54 -04:00
Vitor Pamplona
8c4ec1dc7d Includes Trusted Relays in all my relays 2025-07-08 17:52:56 -04:00
Vitor Pamplona
e89a04eb56 Makes sure the metadata and the Advertised Relay list goes to every connected relay. 2025-07-08 17:21:59 -04:00
Vitor Pamplona
8d14f3615d Improves Bech32 parser to consider only the 58 chars of nsec1, npub1 and note1. 2025-07-08 15:22:55 -04:00
Vitor Pamplona
dbb9ee5379 Removing all the default since logic 2025-07-08 15:22:31 -04:00
Vitor Pamplona
0be9447bee Reusing the parsed event to reuse cache when computing relay hints. 2025-07-08 15:22:09 -04:00
Vitor Pamplona
0d174aae30 Refining the outbox relay selection with all hints. 2025-07-08 14:34:52 -04:00
Vitor Pamplona
994363602d Turns the relay information dialog into a route 2025-07-08 12:00:17 -04:00
Vitor Pamplona
296fa201f7 Delete Relay Selection dialog from all new post screens. 2025-07-08 11:28:30 -04:00
Vitor Pamplona
a05b2681ad 1. Fixes crash when opening the relay selection dialog
2. Fixes color and size of ping labels on relays
3. Removes delete button for connected relay view.
2025-07-08 10:20:54 -04:00
Vitor Pamplona
1b86c3d85b Fixes the sending of multiple old events back to the relays that send old events to Amethyst. 2025-07-08 09:22:33 -04:00
Vitor Pamplona
2c90aa4b1e Continues the migration to use full parsed Addresses instead of just addressIds on the Local Cache. Moves the community route to take a full address. 2025-07-08 08:54:13 -04:00
Vitor Pamplona
0fe8f6c78c Improves debouncing when changing relays. 2025-07-07 20:40:29 -04:00
Vitor Pamplona
b800b5a664 Early support for Blocked and Trusted Relay Lists 2025-07-07 19:49:23 -04:00
Vitor Pamplona
d084714a21 Avoids logging all REQs by default. 2025-07-07 17:19:37 -04:00
Vitor Pamplona
ed2d7f57c5 Adds the incoming relay as a hint to all related keys, addresses and event ids. 2025-07-07 17:02:42 -04:00
Vitor Pamplona
eb5d113298 new map of set api 2025-07-07 13:48:39 -04:00
Vitor Pamplona
cf48516602 Makes sure Filters are not updated if the since is the only change 2025-07-07 13:37:57 -04:00
Vitor Pamplona
28d182cc55 Updates tor, okhttp and navigation compose 2025-07-03 18:26:57 -04:00
Vitor Pamplona
7eeb2ed66a Fixes count call mistake 2025-07-03 18:26:34 -04:00
Vitor Pamplona
1e8ca6c5f0 Merge remote-tracking branch 'origin/HEAD' into full-outbox 2025-07-03 17:22:38 -04:00
Vitor Pamplona
9e72f5d55e Fixes icon size cache on the User Profile 2025-07-03 17:14:07 -04:00
David Kaspar
1db628719b Merge pull request #1384 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-07-03 22:04:11 +02:00
Vitor Pamplona
e4ebca29f4 Improves RelayHint normalization to make sure the NormalizedRelayUrls are indeed relays 2025-07-03 15:45:46 -04:00
Crowdin Bot
67ae61e5d4 New Crowdin translations by GitHub Action 2025-07-03 19:13:48 +00:00
Vitor Pamplona
64bad2b476 Better logs for the basic relay 2025-07-03 13:08:50 -04:00
Vitor Pamplona
cfbf113910 Fixes crash on lazy flows when uploading images before the relay list is loaded. 2025-07-03 13:08:27 -04:00
Vitor Pamplona
47e69ce045 Improves caching on the allRelays call on the Nostr Client 2025-07-03 10:30:53 -04:00
Vitor Pamplona
da3dbb0321 Fixes the Account switching issue that was calling onDispose AFTER a new user composition was run and thus closing all connections on switching 2025-07-03 10:30:19 -04:00
Vitor Pamplona
1f4ad32d23 Fixes the clearing of the mapping function between users and subscription ids when subscriptions end 2025-07-03 10:29:17 -04:00
Vitor Pamplona
69db9bdcd5 Reviews onDispose calls to make sure the onDispose is disposing the data from the previous composable, since it can happen AFTER a new composition already took place. So the references must match the old composition. 2025-07-03 10:27:47 -04:00
Vitor Pamplona
5bcaf065a3 Fixing logs for the new list of connected relays. 2025-07-03 10:19:33 -04:00
Vitor Pamplona
1fa2592563 Improves the performance and logging structures for the anti-spam filter. 2025-07-02 19:36:33 -04:00
Vitor Pamplona
ad481e1928 Removes another local relay from the list 2025-07-02 17:59:14 -04:00
Vitor Pamplona
5525bc4bd9 Increasing the thread pool for OkHttp in order to sustain more connections. 2025-07-02 17:42:46 -04:00
Vitor Pamplona
0b102df117 Removes local relays from NIP65 lists. 2025-07-02 15:50:50 -04:00
Vitor Pamplona
826718d688 Fixes author id on the outbox loader filter. 2025-07-02 15:35:47 -04:00
Vitor Pamplona
3b260d98ad Marks if it is using Tor in each relay of the list. 2025-07-02 15:15:19 -04:00
Vitor Pamplona
e499410ddb Fixes bug when the string has emojis and is trying to check if it is a hex or not 2025-07-02 15:12:48 -04:00
Vitor Pamplona
11d9af41bd Normalizes the click to open relay info dialog 2025-07-02 14:52:16 -04:00
Vitor Pamplona
67dc396588 Displays all available and connected relays in the relay screen 2025-07-02 14:52:03 -04:00
Vitor Pamplona
d0e66504e4 Improves Relay Icon flow 2025-07-02 14:16:26 -04:00
Vitor Pamplona
8c11ae5c87 Avoids adding local relays to the hint index. 2025-07-02 14:13:36 -04:00
Vitor Pamplona
abd895480d adds local servers to the "local definition" of the Normalized Url 2025-07-02 14:13:09 -04:00
Vitor Pamplona
a1a2e86c94 Reports don't offer hints 2025-07-02 14:12:24 -04:00
Vitor Pamplona
83899bda5a Improves RelayInformation document with an empty one 2025-07-02 14:11:45 -04:00
Vitor Pamplona
29b70a52d4 Improves logs 2025-07-02 14:11:26 -04:00
Vitor Pamplona
dd5c2ca704 Making sure the order of params is right 2025-07-02 13:35:01 -04:00
Vitor Pamplona
ce7965d00e Improves starting value of flows 2025-07-02 12:51:08 -04:00
Vitor Pamplona
bb55aea4a4 Fixing log messages 2025-07-02 12:50:17 -04:00
Vitor Pamplona
546d238db3 Reformats due to new lint 2025-07-02 10:51:13 -04:00
Vitor Pamplona
774ee20a20 Updates all dependencies 2025-07-02 10:51:03 -04:00
Vitor Pamplona
1c4b2e9d1a Recover Send and Wait 2025-07-02 08:59:12 -04:00
Vitor Pamplona
b236938ef5 Simplify the unregistering instead of relying on errors and other states that might come from separate filters to the same relay 2025-07-02 08:59:03 -04:00
Vitor Pamplona
1615337a2c Simplifies new try 2025-07-02 08:58:16 -04:00
Vitor Pamplona
936b17f54a Merge remote-tracking branch 'origin/HEAD' into full-outbox
# Conflicts:
#	ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt
#	gradle/libs.versions.toml
2025-07-02 08:23:54 -04:00
Vitor Pamplona
0cfcfaf899 3rd large migration to outbox. 2025-07-01 20:38:18 -04:00
Vitor Pamplona
f788d031c9 Merge pull request #1380 from davotoula/684-add-auto-translate-config-to-settings
Add auto translate config to new user specific settings
2025-07-01 08:43:46 -04:00
Vitor Pamplona
ddc50f379d Merge pull request #1381 from greenart7c3/main
Propagate Auth event to `sendAndWaitForResponse`
2025-06-30 08:11:58 -04:00
greenart7c3
bfe5fcf0b1 - Propagate Auth event to sendAndWaitForResponse
- Pass feed types to `sendSingle` when sending signed event
2025-06-30 08:11:10 -03:00
David Kaspar
fb9c070ee6 One more line for description 2025-06-29 18:33:30 +01:00
David Kaspar
285603cc0f added trailing x icon to menu items to clarify removal 2025-06-29 18:28:57 +01:00
David Kaspar
65cca8aa34 Don't show users own languages
TODO:
Add red x to items
Rename Settings to AppSettings (routes, classes)
Add explainer
2025-06-29 17:30:02 +01:00
David Kaspar
6015ea04ce remove selected language from dropdown
TODO:
Rename Settings to AppSettings (routes, classes)
Don't show users own languages
Add explainer
2025-06-26 13:27:32 +02:00
David Kaspar
fdf11d3ca3 revert dropdown
TODO:
Rename Settings to AppSettings (routes, classes)
Don't show users own languages
Remove language from drop down
Add explainer
2025-06-26 09:14:58 +02:00
David Kaspar
5620eb023b added preview for UserSettingsScreen.kt
TODO:
Rename Settings to AppSettings (routes, classes)
Don't show users own languages
Remove language from drop down
Add explainer
2025-06-26 08:56:23 +02:00
David Kaspar
759ef44440 Remove previous implementation
TODO:
Rename Settings to AppSettings (routes, classes)
Don't show users own languages
Remove language from drop down
Add explainer
Change to dropdown with Xs
2025-06-26 08:16:09 +02:00
David Kaspar
f0670971ef Wrap update call in IO scope
TODO:
Don't show users own languages
Remove language from drop down
Add explainer
2025-06-25 20:51:11 +02:00
David Kaspar
c59086068b Move dont translate settings to the dedicated user settings page 2025-06-25 20:42:29 +02:00
David Kaspar
3524f7e412 Added support for multiple icons for a single row (backwards compatible) 2025-06-25 20:18:11 +02:00
David Kaspar
2e2a9bb917 Added user preferences screen 2025-06-25 18:13:43 +02:00
David Kaspar
a5c370133c Change to show excluded languages in dropdown, no rows below
TODO:
Load actually excluded languages
Optimise code
Improve UI
2025-06-25 17:34:14 +02:00
David Kaspar
0495dc88fc Add dropdown with auto translate language excludes to app settings
Persist changes to settings

TODO:
Load actually excluded languages
Optimise code
Improve UI
2025-06-25 17:34:14 +02:00
Vitor Pamplona
f536abb22a Merge pull request #1378 from davotoula/main
Upgrade AGP and Gradle
2025-06-25 11:22:33 -04:00
David Kaspar
0bae8adebc Upgrade AGP to 8.11.0
Upgrade Gradle to 8.13
2025-06-25 17:18:05 +02:00
Vitor Pamplona
3270254f47 Merge pull request #1377 from davotoula/strip-codec-from-mime-type-before-saving-video
bugfix: only user part before ";" in media-type before passing to ContentResolver
2025-06-25 10:53:16 -04:00
David Kaspar
dacc9f236b strip anything after ";" in media-type before passing to ContentResolver 2025-06-25 16:33:28 +02:00
David Kaspar
cd316fb52f Merge pull request #1376 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-06-25 14:20:16 +02:00
Crowdin Bot
bf7638eaa9 New Crowdin translations by GitHub Action 2025-06-25 12:02:19 +00:00
Vitor Pamplona
3f049a0536 Merge pull request #1375 from davotoula/fix-build-issue-android-kotlin-geohash
fix build: change to version b481c6a64e of android-kotlin-geohash
2025-06-25 08:00:55 -04:00
David Kaspar
86bfca99e6 change to version b481c6a64e of android-kotlin-geohash as 1.0 has disappeared from jitpack.io 2025-06-25 11:41:28 +02:00
Vitor Pamplona
e7dec1702d Merge pull request #1369 from alltheseas/patch-2
Update README.md with deepwiki link
2025-06-19 09:48:04 -04:00
Vitor Pamplona
4a848b3296 Merge pull request #1372 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-06-19 09:47:52 -04:00
Crowdin Bot
2d9348b640 New Crowdin translations by GitHub Action 2025-06-19 13:46:40 +00:00
Vitor Pamplona
44108c7410 Merge pull request #1371 from greenart7c3/main
Add a UnsignedEventDeserializer
2025-06-19 09:45:19 -04:00
greenart7c3
9e58c2d3b9 Use EventTemplate instead of Event in BunkerRequestSign 2025-06-18 17:24:43 -03:00
greenart7c3
acf426ccdc Add a UnsignedEventDeserializer 2025-06-18 13:28:43 -03:00
alltheseas
2889c5dae5 Update README.md with deepwiki link
added link to amethyst deepwiki
2025-06-09 17:13:35 -05:00
Vitor Pamplona
8276156f0a Merge pull request #1367 from davotoula/upgrade-agp
Upgrade AGP to 8.10.1
2025-06-05 13:02:06 -04:00
David Kaspar
6a544340cb Upgrade AGP to 8.10.1 2025-06-05 18:52:36 +02:00
Vitor Pamplona
5ee64b31d2 Merge pull request #1366 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-06-02 11:36:03 -04:00
Crowdin Bot
ec1dd3cc6e New Crowdin translations by GitHub Action 2025-06-02 15:28:22 +00:00
Vitor Pamplona
ef6313696d Merge pull request #1365 from davotoula/remove-unused-resource-ids-from-translated-files
remove unused string ids from translations
2025-06-02 11:27:07 -04:00
David Kaspar
ffeeebfe1e remove unused string ids from translations 2025-06-02 16:12:42 +02:00
Vitor Pamplona
fcde05c91e Merge pull request #1363 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-30 14:42:48 -04:00
Crowdin Bot
6b1662da24 New Crowdin translations by GitHub Action 2025-05-30 18:40:22 +00:00
Vitor Pamplona
5925a91e62 Merge pull request #1360 from davotoula/update-string-ids
update string ids to force new translations
2025-05-30 14:39:03 -04:00
David Kaspar
d83e8007dc Merge pull request #1362 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-28 08:29:43 +02:00
Crowdin Bot
173a27f085 New Crowdin translations by GitHub Action 2025-05-28 06:20:54 +00:00
David Kaspar
a970080478 Merge pull request #1361 from davotoula/remove-unused-imports
remove unused imports amethyst
2025-05-28 08:19:40 +02:00
David Kaspar
293bd2557b remove unused imports amethyst 2025-05-28 07:42:20 +02:00
David Kaspar
dac1c3717a update string ids to force new translations 2025-05-27 18:49:35 +02:00
Vitor Pamplona
e10332c957 - Only saves draft after the first change in the UI.
- Adjusts coroutine scopes to make sure it cannot be cancelled while posting
2025-05-20 18:21:19 -04:00
Vitor Pamplona
de881c23ec Migrates button objects out of the new post screen file 2025-05-20 17:01:55 -04:00
Vitor Pamplona
fcfa2bb05a Making NewPost Screen to be about Kind 1 and polls only 2025-05-20 16:30:35 -04:00
Vitor Pamplona
00048606fa Small refactoring 2025-05-20 15:22:53 -04:00
Vitor Pamplona
65bab50a6d Slight improvements on paddings and spacings 2025-05-20 15:18:32 -04:00
Vitor Pamplona
6d0b05e34d Activates NIP-22 reply for everything but kind 1s. 2025-05-20 15:00:00 -04:00
Vitor Pamplona
195e268865 Normalizes NIP-22 viewmodel to its own entity 2025-05-20 14:23:51 -04:00
Vitor Pamplona
bb1dfee35b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-05-20 11:54:29 -04:00
Vitor Pamplona
63340d79e2 - Adds support for creating and replying to NIP-22 geo scope posts
- Adds support for creating and replying to NIP-22 hashtag scope posts
- Refactors NIP-73 and NIP-22 on quartz to better support scoped replies
- Creates New post screens for both hashtags and geolocated posts.
2025-05-20 11:54:21 -04:00
Vitor Pamplona
041aa1c461 Fixes hashtag search i18n and padding 2025-05-20 11:50:24 -04:00
Vitor Pamplona
1594971a60 Merge pull request #1359 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-19 18:45:48 -04:00
Crowdin Bot
12b7cf410d New Crowdin translations by GitHub Action 2025-05-19 22:16:05 +00:00
Vitor Pamplona
a5e8bdd43a Fixes NIP-22 identifier for geotags 2025-05-19 17:42:16 -04:00
Vitor Pamplona
ba71c95bec Removes youtube links from the video feed 2025-05-19 17:41:50 -04:00
Vitor Pamplona
e5230d734c Merge pull request #1358 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-17 08:18:40 -04:00
Crowdin Bot
1a45e26eb8 New Crowdin translations by GitHub Action 2025-05-17 12:17:19 +00:00
Vitor Pamplona
1b81dfd832 Merge pull request #1357 from davotoula/remove-unused-imports
remove unused imports
2025-05-17 08:16:03 -04:00
David Kaspar
7ee2a02227 remove unused imports 2025-05-17 08:50:04 +02:00
Vitor Pamplona
8a59904bfc Fixes local cache strategy to create an option and not verify events from Seals. 2025-05-16 19:27:39 -04:00
Vitor Pamplona
049e1d15ce Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-05-16 17:03:04 -04:00
Vitor Pamplona
203b5e5f39 Align specific inner events with the new verification procedures inside the consume function 2025-05-16 17:02:35 -04:00
David Kaspar
0bb8349a46 Update README.md
Long form content view only
2025-05-16 21:33:05 +02:00
David Kaspar
725fb11ec5 Merge pull request #1356 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-16 20:29:04 +02:00
Crowdin Bot
c429a97bb4 New Crowdin translations by GitHub Action 2025-05-16 18:15:50 +00:00
Vitor Pamplona
ad12f80679 Upgrade Media3 to 1.7.1 2025-05-16 14:14:11 -04:00
Vitor Pamplona
d35e3f8daf Removing deprecated functions 2025-05-16 14:11:12 -04:00
Vitor Pamplona
780a1ef65e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-05-16 14:05:20 -04:00
David Kaspar
c413659c87 Merge pull request #1355 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-16 19:48:21 +02:00
Crowdin Bot
23f42a025a New Crowdin translations by GitHub Action 2025-05-16 17:40:35 +00:00
David Kaspar
ef1ac76839 Update strings.xml
Fixed typo
2025-05-16 18:39:14 +01:00
Vitor Pamplona
141713bd35 Minor refactoring 2025-05-16 12:55:29 -04:00
Vitor Pamplona
b5a1b84fa8 Fixes logging of the memory cache usage 2025-05-16 12:48:01 -04:00
Vitor Pamplona
1a681e1b53 Deletes encrypted component of the GiftWrap after unwrapping to avoid consuming memory needlessly 2025-05-16 12:47:35 -04:00
Vitor Pamplona
07434eb4b6 Fixes newStack bug that was only resetting the stack until the home and not to its own route 2025-05-15 17:02:15 -04:00
Vitor Pamplona
ac7084ce84 Removing debug methods from the main binary 2025-05-15 16:40:44 -04:00
Vitor Pamplona
dc6f982ae9 Adds a byte count in the event per second logger 2025-05-15 15:33:53 -04:00
Vitor Pamplona
ea9d25d13b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-05-15 11:49:34 -04:00
Vitor Pamplona
afe04969ee Improves edge to edge margins and colors for the RelayInfoDialog 2025-05-15 11:49:25 -04:00
Vitor Pamplona
3823acef0b Merge pull request #1354 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-15 08:38:34 -04:00
Crowdin Bot
12ac1c48bb New Crowdin translations by GitHub Action 2025-05-15 12:37:54 +00:00
Vitor Pamplona
b345eca8ad Merge pull request #1350 from davotoula/share-media-to-other-apps
Share images to other apps
2025-05-15 08:36:23 -04:00
David Kaspar
c3c8b56a39 Merge branch 'main' into share-media-to-other-apps 2025-05-15 07:41:28 +02:00
Vitor Pamplona
42e20efaa5 removing unused imports 2025-05-14 20:39:01 -04:00
Vitor Pamplona
2ef1cb5fea Merge pull request #1353 from kirpy0x/NostrLinks
Open Njump, Yakihonne and Primal links in Amethyst
2025-05-14 20:33:55 -04:00
Vitor Pamplona
df2f0feef8 Improvement to the logs of RelaySpeedLogger 2025-05-14 20:32:33 -04:00
Vitor Pamplona
700813dab7 Moves LocalCache bundler to the default thread 2025-05-14 19:27:57 -04:00
kirpy
b925b12278 Open Njump, Yakihonne and Primal links in Amethyst
fixes: 1217
2025-05-14 17:23:58 -06:00
Vitor Pamplona
6367e9ddc9 Moving logs from SubscriptionController to the Manager to be able to distinguish between the multiple managers 2025-05-14 19:19:58 -04:00
Vitor Pamplona
dfdee1ae96 - Migrating a mix of tags from the old Tag array methods to the new Tag Object parsers.
- Starting to use fast methods for Tag array loops instead of the native kotlin ones.
2025-05-14 19:19:29 -04:00
Vitor Pamplona
c0ecda94a3 Uses fastAny to avoid creating an iterator 2025-05-14 18:29:03 -04:00
Vitor Pamplona
acf944ebac Avoids item drawing mishandling of multiple incognitos in the same chat screen 2025-05-14 18:28:49 -04:00
Vitor Pamplona
3c5b9dbca6 Fixes an issue with two home screens interfering with flow collections. For some reason it was only a problem because this route matches the starting route. 2025-05-14 17:00:36 -04:00
Vitor Pamplona
e0449a9d44 Adds a list state saver to the messages screen 2025-05-14 16:52:19 -04:00
Vitor Pamplona
4ae86861f8 Removes unnecessary labels 2025-05-14 15:17:31 -04:00
Vitor Pamplona
9e13e88ae9 removes log 2025-05-14 15:00:35 -04:00
Vitor Pamplona
1ed9aa64bf Gets rid of the update bundler for Notes, Users and Channel. 2025-05-14 15:00:06 -04:00
Vitor Pamplona
803decf5ff Move the EOSE cache from Note to the sub assembler 2025-05-14 14:59:27 -04:00
Vitor Pamplona
6a91b68b4b Migrates missing User filter to the new no EOSE cache manager 2025-05-14 14:56:47 -04:00
Vitor Pamplona
0f018140f0 Minor refactoring 2025-05-14 14:56:08 -04:00
Vitor Pamplona
979e02e7fa Migrates event finder and channel finder to the new No Eose cache manager 2025-05-14 14:55:53 -04:00
Vitor Pamplona
4fc2d3a7d1 Adds a new EOSE manager that keeps one sub but no EOSE cache 2025-05-14 14:53:54 -04:00
Vitor Pamplona
d0f138e992 Generalizes the FastEOSE cache for Users 2025-05-14 14:53:30 -04:00
Vitor Pamplona
b7ac32d768 Removes unused since param for missing events in thread assemblers 2025-05-14 14:50:52 -04:00
David Kaspar
3b5d12b477 make helper method suspend, use different scope in View 2025-05-14 19:15:16 +02:00
Vitor Pamplona
6f7f8e296b Removes the time logger for note rendering 2025-05-14 12:53:56 -04:00
David Kaspar
d6ee09ecdb Merge branch 'main' into share-media-to-other-apps 2025-05-14 18:20:57 +02:00
David Kaspar
61970b8ce5 Update README.md
Split video and image capture from phone. Ticket image capture
2025-05-14 17:17:27 +01:00
Vitor Pamplona
b045d0e122 Migrates lint to the newer default version and copyright to 2025 2025-05-14 11:59:15 -04:00
Vitor Pamplona
47de821f6d Updates coil, compose, immutable, lifecycle, navigation and zoomable 2025-05-14 11:14:33 -04:00
Vitor Pamplona
2c06df7e50 Simple refactoring for the name of the subscription cache 2025-05-14 10:43:42 -04:00
Vitor Pamplona
52d31502e6 Migrates the Subscription controller to use filters from the NostrClient instead of the past version in the controller to avoid out of sync comparisons 2025-05-14 10:08:56 -04:00
Vitor Pamplona
d7d8f02a80 Moves Content Sensitivity setting to the Security filter screen 2025-05-13 20:09:14 -04:00
Vitor Pamplona
f3e2327676 Modifies the behavior of new posts to clear the background color after 5 seconds 2025-05-13 17:21:22 -04:00
Vitor Pamplona
dba51acbd5 Merge pull request #1351 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-13 15:32:53 -04:00
Crowdin Bot
20c775913a New Crowdin translations by GitHub Action 2025-05-13 19:24:27 +00:00
Vitor Pamplona
a42afdc78b merge 2025-05-13 14:53:00 -04:00
Vitor Pamplona
6ecb25b8e1 - Refactors the entire EOSE caching structure.
- Refactors all Datasource classes into a 3 layer design with composeSubscriptions being registered, passed to a group of EOSE managers that can change eose caching capabilities based on needs, and migrates the old subscription Orchestrator into a simpler controller
- Breaks down all Datasource into multiple filter functions
- Improves EOSE support for many of the base filters like on NIP-04 DMs, notifications, etc
- Moves EOSE cache for public, non-user-dependent items like channels, hashtags, etc to their own caching system.
- Improves the Relay speed logger to better track how many events are being received, and check for duplications
2025-05-13 14:52:26 -04:00
Vitor Pamplona
0e68031227 Removes the need to track EOSE at the base user class 2025-05-13 14:47:20 -04:00
Vitor Pamplona
466f52245b Adds an isDifferent function to avoid resending NormalFilters 2025-05-13 14:46:09 -04:00
Vitor Pamplona
09946c19b9 Turns all base Hint classes into data classes 2025-05-13 14:44:03 -04:00
Vitor Pamplona
4279ad004c Only updates EOSE log if the time is bigger 2025-05-13 14:43:44 -04:00
Vitor Pamplona
50cde1bc3e - Bugfix to mark relay connections as AfterEOSE when the relay client or server closes the subscription
- Adds an arrival time to log the time EOSEs were received in case the listener is running async processes later.
2025-05-13 14:43:28 -04:00
Vitor Pamplona
2d5732bcce Normalizes hashtag transformations for filters. 2025-05-13 14:40:03 -04:00
David Kaspar
a848ea6135 add content param when making call using a MediaPreloadedContent. Just in case 2025-05-13 09:43:23 +02:00
David Kaspar
095d054d62 add support for gifs 2025-05-13 09:39:33 +02:00
David Kaspar
2e4c9f77e6 refactor 2025-05-13 09:39:33 +02:00
David Kaspar
7efec03056 Return file extension from ShareHelper just in case mimetype is null
Use content to determine whether it's an image
2025-05-12 22:26:24 +02:00
David Kaspar
91b6bf6e88 localisation 2025-05-12 21:03:17 +02:00
David Kaspar
35001c0b00 moved sharing intent back to view 2025-05-12 20:56:16 +02:00
David Kaspar
ce96ab873f added todos 2025-05-12 18:42:56 +02:00
David Kaspar
8c1888b4d5 added error handling
.use {} for automatic resource management with snapshots
2025-05-12 17:40:28 +02:00
David Kaspar
0192a4eb20 Add test to only share images. Other media not supported atm 2025-05-12 17:13:29 +02:00
David Kaspar
fee85a6111 remove scope launch 2025-05-12 17:06:05 +02:00
David Kaspar
13ee0a23de Use disk cache reference from app
Detect image type based on magic number
2025-05-12 16:44:31 +02:00
David Kaspar
ebf1571b92 share images from coil cache to other apps 2025-05-08 18:05:06 +02:00
Vitor Pamplona
a5c63dd311 Merge pull request #1349 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-08 10:58:07 -04:00
Crowdin Bot
245b767dc8 New Crowdin translations by GitHub Action 2025-05-08 14:56:55 +00:00
Vitor Pamplona
be62d5ad7f Merge pull request #1347 from davotoula/use-updated-markdown-to-sanitise-url-labels
Use new version of Markdown to sanitise url labels from # and @
2025-05-08 10:55:33 -04:00
Vitor Pamplona
dc132de18d Merge pull request #1348 from davotoula/upgrade-agp-to-8.10.0
Upgrade agp to 8.10.0
2025-05-08 10:52:52 -04:00
David Kaspar
5d1515659e remove unused imports 2025-05-08 13:13:02 +02:00
David Kaspar
6206d550e6 Upgrade agp to 8.10.0 2025-05-08 13:09:08 +02:00
David Kaspar
d7055062f3 Use new version of Markdown to sanitise url labels from # and @ 2025-05-08 11:58:02 +02:00
Vitor Pamplona
bddd8914ae Adds an EOSE manager to Channel Filters 2025-05-07 13:54:56 -04:00
Vitor Pamplona
2fc7c37270 Adds a watcher of MetadataChanges for NIP-28 that includes a since parameter for the last metadata time + 1 2025-05-07 13:48:54 -04:00
Vitor Pamplona
bc22b78f19 Refactors EOSE management for Chatroom List 2025-05-07 13:47:34 -04:00
Vitor Pamplona
e6b0b41e80 Adds a Normal Nostr filter as a PerRelay filter 2025-05-07 13:46:39 -04:00
Vitor Pamplona
8a7ce8dc07 Makes EOSE cache per user cleanable 2025-05-07 13:46:14 -04:00
Vitor Pamplona
cb4e57d6f6 remove the filter invalidation after each EOSE 2025-05-06 19:42:23 -04:00
Vitor Pamplona
a54835e4f9 Adds Eose management for the UserProfile Screen 2025-05-06 19:42:09 -04:00
Vitor Pamplona
be0a559b7d Prints better logs for relay subscriptions 2025-05-06 19:38:13 -04:00
Vitor Pamplona
b801125ff5 Builds a speed logger to better understand data usage 2025-05-06 19:37:44 -04:00
Vitor Pamplona
4ea9d2db7d Updates AGP to 8.10 2025-05-06 18:21:15 -04:00
Vitor Pamplona
54532de88a Extending AsyncImage to correctly use pre-loaded aspect ratio and avoid screen jittering 2025-05-06 16:46:25 -04:00
Vitor Pamplona
ba6a7bb07f Fixes missing content alignment on the crossfade API migration 2025-05-06 16:18:21 -04:00
Vitor Pamplona
fcd70001bf Adds a Reads feed to the discovery tab 2025-05-06 15:48:03 -04:00
Vitor Pamplona
80d4971548 - Adds FollowSets to the discovery tabs.
- Refactors discovery screen, creating a package for each feature.
- Fixes Android studio previews
2025-05-06 13:23:43 -04:00
Vitor Pamplona
fbe20029f7 Move to a cache model where we defer the event's verification until the last moment before adding to cache 2025-05-06 09:45:01 -04:00
Vitor Pamplona
a05a465f69 Avoids sending an NWC filter when there are no keys subscribed to it 2025-05-06 08:56:47 -04:00
Vitor Pamplona
49c131a5cd Adding more constraints to the channel filter 2025-05-05 19:11:54 -04:00
Vitor Pamplona
1c23600c57 Assigns the first relay when a gift-wrapped message is received 2025-05-05 18:51:45 -04:00
Vitor Pamplona
7a8766551f unused checks 2025-05-05 18:36:13 -04:00
Vitor Pamplona
7accb5e2b9 Adds onStart calculator for live bubble authors 2025-05-05 18:34:54 -04:00
Vitor Pamplona
504f365ab3 Fixes code to mark messages as received by relays 2025-05-05 18:12:16 -04:00
Vitor Pamplona
d9b784bee5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-05-05 17:22:53 -04:00
Vitor Pamplona
aff95853d8 Merge pull request #1346 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-05 17:21:15 -04:00
Vitor Pamplona
adcbbec5be Removing unnecessary logs 2025-05-05 17:21:03 -04:00
Crowdin Bot
ceabf8184a New Crowdin translations by GitHub Action 2025-05-05 21:10:39 +00:00
Vitor Pamplona
7aa71fad3f Caches app icons as painters 2025-05-05 17:08:47 -04:00
Vitor Pamplona
15dfbb2f31 Fixes the test case for emojis 2025-05-03 16:33:28 -04:00
Vitor Pamplona
0d53a30371 Disable event logger for now 2025-05-03 16:33:08 -04:00
Vitor Pamplona
38bb39a407 Fixes bug when trying to detect the charset of the first 1024 bytes of the page. Since this is an invalid HTML, it was crashing here. 2025-05-03 16:15:20 -04:00
Vitor Pamplona
560a9ba471 Fixes some log messages 2025-05-02 21:23:44 -04:00
Vitor Pamplona
38d3194e87 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
2025-05-02 21:19:14 -04:00
Vitor Pamplona
462fda1b5c Updates accompanist, jackson, mockk, secp256, zoomable 2025-05-02 21:18:40 -04:00
Vitor Pamplona
63a009f36d - Adds support for Ephemeral Chats from coolr.chat
- Adds support for following ephemeral chats
- Adds support for live events at the top of the feed.
- Adds support for NIP-51, kind:10005 public chat lists
- Adds support for Channel feeds
- Moves following of NIP-28 chats from the Contact List to kind: 10005
- Disables following of events at the Contact list
- Improves gallery display to slightly override profile pictures when in list
- Starts the Account refactoring by moving custom Emoji, EphemeralList and PublicChat lists to their own packages
- Refactors NIP-51 lists to use common classes of private tags instead of general list classes.
- Starts to separate all Public chats into their own database.
- Removes old account upgrades from the local storage
- Refactors url NIP-11 loading and unifies icon
- Reduces the dependency of Relay classes in the LocalCache, Notes and User classes
2025-05-02 21:18:10 -04:00
David Kaspar
c3dc8104e9 Merge pull request #1340 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-05-02 15:27:21 +02:00
Crowdin Bot
23fbb53c96 New Crowdin translations by GitHub Action 2025-05-02 13:20:35 +00:00
Vitor Pamplona
1fe55ece5d Merge pull request #1339 from davotoula/missing-at-for-profile-links-in-markdown
Missing at for profile links in markdown and fix typo preventing opening of hashtags from markdown notes
2025-05-02 09:19:03 -04:00
David Kaspar
bb9435e637 Remove unused imports 2025-05-02 15:05:20 +02:00
David Kaspar
67e3a92d68 cleanup 2025-05-02 15:00:27 +02:00
David Kaspar
32439b5feb bugfix: fix typo preventing opening of hashtags from markdown notes 2025-05-02 15:00:06 +02:00
David Kaspar
136a4061d7 prepend clickable user with @ sign 2025-05-02 14:59:32 +02:00
Vitor Pamplona
b06e53f59d Fixes lack of update for Discovery filters when resuming 2025-04-30 15:24:00 -04:00
Vitor Pamplona
9c79ef08c7 Moves logging out of quartz Relay class 2025-04-30 14:00:08 -04:00
Vitor Pamplona
43b761c98c Slight improvement in the UI to view follow packs 2025-04-29 17:38:00 -04:00
Vitor Pamplona
626bfc95cb - Adds support for Follow packs
- Enables top filter by pack as well.
2025-04-29 17:24:03 -04:00
Vitor Pamplona
9df0f6c368 Moves Coil's image loader choice to a subscription on account. 2025-04-29 16:04:16 -04:00
Vitor Pamplona
7c68ffaf6e Delete unused flows 2025-04-29 15:55:57 -04:00
Vitor Pamplona
ee1d37dd5b Merge pull request #1338 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-29 14:39:17 -04:00
Crowdin Bot
ed6d0f6c0f New Crowdin translations by GitHub Action 2025-04-29 18:37:53 +00:00
Vitor Pamplona
533e7d661f Presubscribe to the bottom navigation items in the start up of the app. 2025-04-29 14:36:01 -04:00
Vitor Pamplona
932664c9f9 Avoids crashes when fetching base64 and blurhash images 2025-04-29 14:33:55 -04:00
David Kaspar
3d89ed567b Merge pull request #1337 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-29 06:11:07 +02:00
Crowdin Bot
62e2e2ac65 New Crowdin translations by GitHub Action 2025-04-29 01:16:39 +00:00
Vitor Pamplona
1c8a6d1234 - Separates new Product Screen from the generic new Post screen.
- Adds New product button on the Marketplace tab
- Adds imeta tags for images and urls inside the content of the Classifieds.
- Shows multiple images on the post and thread view.
- Removes the option for NIP-95 images on Classifieds.
- Creates a new route for new products
- Adjusts quarts to process images with iMeta tags
2025-04-28 21:13:41 -04:00
Vitor Pamplona
c07592f399 Migrates Relay selection dialog to full bleed layout 2025-04-28 20:58:23 -04:00
Vitor Pamplona
96b1565de7 Removes duplicated draft save on DM 2025-04-28 20:57:11 -04:00
Vitor Pamplona
ee675d06f6 Merge pull request #1336 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-26 11:05:50 -04:00
Crowdin Bot
7c02b5b58f New Crowdin translations by GitHub Action 2025-04-26 15:00:51 +00:00
Vitor Pamplona
4c7ef8ecff Caches the processing of preview urls for the new post screen
Fixes a bug on saving MediaType on Android's stack
2025-04-26 10:58:45 -04:00
Vitor Pamplona
99cef643e1 Improves the visual of multiple previews on the new post screen 2025-04-26 10:58:17 -04:00
Vitor Pamplona
08025f7df7 Adds a special case for loading previews with just 1 item in the new post screen 2025-04-25 17:41:06 -04:00
Vitor Pamplona
92eff02f0a Removes the hack to reduce memory usage by deleting the .content when saving the event to cache. 2025-04-25 17:04:16 -04:00
Vitor Pamplona
539d20b397 Adds an error log for the new on account view model processor 2025-04-25 17:03:26 -04:00
Vitor Pamplona
b51e5f4383 better relay reconnecting logic 2025-04-25 17:02:58 -04:00
Vitor Pamplona
0c54f9aa10 Renders the new comment tag on highlights. 2025-04-25 16:58:41 -04:00
Vitor Pamplona
c98e23add7 Merging 2025-04-25 15:07:52 -04:00
Vitor Pamplona
e4cc65969a Merge pull request #1332 from davotoula/reduce-compiler-warnings-amethyst
Reduce compiler warnings: amethyst module
2025-04-25 12:16:40 -04:00
Vitor Pamplona
aef7d91a6b Moves privacy option from dialog to route 2025-04-25 12:14:44 -04:00
David Kaspar
d874e798c6 remove unused imports 2025-04-25 17:10:25 +02:00
Vitor Pamplona
390e0615d1 Removing these disabled lock behavior code until we get it working 2025-04-25 11:10:04 -04:00
David Kaspar
e40ca09f7c Use default locale for capitalisation in UI strings (use ROOT for IDs/Hashes for consistency) 2025-04-25 17:00:16 +02:00
David Kaspar
96436912bb Safe checked casting 2025-04-25 16:03:16 +02:00
David Kaspar
97290cea48 Replace String.capitalize with replaceFirstChar 2025-04-25 16:02:59 +02:00
David Kaspar
ad2387f8e9 Add @OptIns
Change to auto mirrored icons
Remove unnecessary inline
2025-04-25 15:59:54 +02:00
Vitor Pamplona
d25e89f41d Removes pull to refresh hack since we can now fully migrate 2025-04-24 19:36:47 -04:00
Vitor Pamplona
04712468aa Removing unused nip-05 2025-04-24 19:14:35 -04:00
Vitor Pamplona
354fc34fbb New default banner 2025-04-24 19:08:58 -04:00
Vitor Pamplona
026943439a reverting x 2025-04-24 18:51:34 -04:00
Vitor Pamplona
46cb7eb640 Removes text select icon 2025-04-24 18:50:16 -04:00
Vitor Pamplona
f465844009 Removes old icons and migrates some to the Icons package 2025-04-24 18:49:05 -04:00
Vitor Pamplona
eebbda547d Trying again to fix the hyphen 2025-04-24 18:25:09 -04:00
Vitor Pamplona
122603080e Removed unused function 2025-04-24 18:21:30 -04:00
Vitor Pamplona
aff0369b04 updating firebase 2025-04-24 18:14:02 -04:00
Vitor Pamplona
1c1db6fa1e Removes unused copytext dialog 2025-04-24 18:10:52 -04:00
Vitor Pamplona
187d77c138 Marks GlobalScope as optin 2025-04-24 18:07:22 -04:00
Vitor Pamplona
950d0bcc93 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-04-24 18:06:49 -04:00
Vitor Pamplona
0d66ceb1a4 Merge pull request #1330 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-24 18:06:39 -04:00
Vitor Pamplona
8e0563ed3a Migrate animateItemPlacement to new API 2025-04-24 17:46:13 -04:00
Crowdin Bot
02870d0cd3 New Crowdin translations by GitHub Action 2025-04-24 21:41:05 +00:00
Vitor Pamplona
9d3a6d3f1d migrates compose bom and media3 2025-04-24 17:40:38 -04:00
Vitor Pamplona
2c85260ab1 Removes the old ic_verified 2025-04-24 17:39:00 -04:00
Vitor Pamplona
ecebc68e11 avoiding deprecated .put api 2025-04-24 17:36:27 -04:00
Vitor Pamplona
07c3a3ffe5 Fixes unchecked cast and other opt-in tags on observers 2025-04-24 17:35:34 -04:00
Vitor Pamplona
c0f486db82 Hyphen can be replaced with dash 2025-04-24 17:35:11 -04:00
Vitor Pamplona
e0990ae38e Merge pull request #1329 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-24 17:22:02 -04:00
Crowdin Bot
7005158821 New Crowdin translations by GitHub Action 2025-04-24 21:19:22 +00:00
Vitor Pamplona
9922fa117f Reverting svg sizes because of app icon issues 2025-04-24 17:16:31 -04:00
Vitor Pamplona
723b4b5e9f Removes dependency on a modifier for SettingsCategory 2025-04-24 16:58:40 -04:00
Vitor Pamplona
2d64eb0cb3 Refactors Add and Remove button to remove dependency on a specific modifier. 2025-04-24 16:58:22 -04:00
Vitor Pamplona
6a91ede54c Fixes the lack of locale when creating percentages on zap split screens. 2025-04-24 16:57:44 -04:00
Vitor Pamplona
8d91b97962 Migrates CircularProgresIndicator to the newer version 2025-04-24 16:57:24 -04:00
Vitor Pamplona
bf47c1b431 No need to check for API version in this function, since we check outside of it 2025-04-24 16:56:46 -04:00
Vitor Pamplona
f5a504077c Keeps the transparency settings for statusbar and navigation for now. 2025-04-24 16:55:21 -04:00
Vitor Pamplona
95afed5045 Only request for notification permissions after API 33 2025-04-24 16:54:52 -04:00
Vitor Pamplona
dcccc1de3a Last Uri.parse fix 2025-04-24 16:54:31 -04:00
Vitor Pamplona
5095c9f480 Migrating from ContextCompat.startActivity to context.startActivity 2025-04-24 16:54:17 -04:00
Vitor Pamplona
30c4e3c646 Fixes deprecated URI parse function warnings 2025-04-24 16:52:25 -04:00
Vitor Pamplona
d04f6c977e Migrates to the new capitalization function 2025-04-24 16:50:48 -04:00
Vitor Pamplona
7d97b73356 Fixes mutable state builders with primitive types warning. 2025-04-24 16:48:56 -04:00
Vitor Pamplona
be3eb4314b Fixes casting warnings on viewmodels 2025-04-24 16:46:34 -04:00
Vitor Pamplona
f7abc6e1ee Suppress modifiers on ColorScheme. 2025-04-24 16:44:30 -04:00
Vitor Pamplona
c16b93c06a Reduces viewport to 200dp for performance 2025-04-24 16:44:10 -04:00
Vitor Pamplona
82af2644a6 Removes unnecessary label to v26 2025-04-24 16:43:50 -04:00
Vitor Pamplona
7d2b2ee674 normalizing strings 2025-04-24 16:43:28 -04:00
Vitor Pamplona
fdc95a656f Ignore permissions to use our services. 2025-04-24 16:42:43 -04:00
Vitor Pamplona
7b040933d5 Merge 2025-04-24 14:35:57 -04:00
Vitor Pamplona
067f353e50 Finish the migration from LiveData to Flow
Moves most of the flow updates away from the Main thread.
Removes the need to use the Main thread to access Account.userProfile
2025-04-24 14:35:22 -04:00
Vitor Pamplona
2f3c282ef5 Merge pull request #1328 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-24 08:45:03 -04:00
Crowdin Bot
0c8564157b New Crowdin translations by GitHub Action 2025-04-24 12:43:11 +00:00
Vitor Pamplona
d6160ea6c2 Merge pull request #1327 from davotoula/upgrade-agp
upgrade agp to 8.9.2
2025-04-24 08:41:41 -04:00
David Kaspar
0fc04490fd upgrade agp to 8.9.2 2025-04-24 10:44:11 +02:00
Vitor Pamplona
e375b09a24 Fixes missing gallery of reactions 2025-04-23 18:52:43 -04:00
Vitor Pamplona
802e04445d Adds logging message to hunt down some slow screens 2025-04-23 17:15:21 -04:00
Vitor Pamplona
ca8faa062f Caches translations to avoid blinking text every time the user comes back to an older screen. 2025-04-23 16:44:35 -04:00
Vitor Pamplona
d6d34b2a88 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-04-23 16:24:57 -04:00
Vitor Pamplona
a84371a0b3 - Restructures the old static datasource model into dynamic filter assemblers.
- Moves filter assemblers, viewModels and DAL classes to their own packages.
- Creates Composable observers for Users and Notes
- Deletes most of the secondary LiveData objects in the move to Flow
- Manipulates nostr filters depending on the account of the current screen, not a global account.
- Unifies all FilterAssembly lifecycle watchers to a few classes.
- Prepares to separate The Nostr Client as an Engine of Amethyst.
- Moves the pre-caching processor of new events from the datasource to the accountViewModel
- Reorganizes search to be per-screen basis
- Moves authentication to a fixed Coordinator class for all accounts in all relays.
- Moves NOTIFY command to its own coordinator class for all accounts
- Moves the connection between filters and cache to its own class.
- Significantly reduces the dependency on a single ServiceManager class.
2025-04-23 16:24:41 -04:00
Vitor Pamplona
411b49d68f Merge pull request #1326 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-23 16:04:46 -04:00
Crowdin Bot
f8fd935a4e New Crowdin translations by GitHub Action 2025-04-23 19:45:34 +00:00
Vitor Pamplona
62be469cd8 Merge pull request #1325 from greenart7c3/main
Filter special characters when decoding bech32
2025-04-23 15:44:08 -04:00
greenart7c3
b42d05ae7a Filter unicode characters 2025-04-23 16:17:13 -03:00
Vitor Pamplona
e007a02d84 Merge pull request #1320 from davotoula/circle-around-profile-picture-in-drawer
contrasting circle around profile picture
2025-04-13 11:13:03 -04:00
David Kaspar
cbd5a8a7ee contrasting circle around profile picture
remove unused import
2025-04-13 09:59:43 +02:00
Vitor Pamplona
fd1c5d5887 Merge pull request #1319 from davotoula/1318-profile-qr-code-screen-changes
corrected profile picture border colour
2025-04-12 11:18:12 -04:00
David Kaspar
043196be10 Update bug_report.md
Added app flavour field (play vs fdroid)
bump versions
2025-04-12 15:56:51 +01:00
David Kaspar
66927ca70c corrected profile picture border colour 2025-04-12 16:02:35 +02:00
Vitor Pamplona
faf0f6dc1a Updates mockk, coroutines, zoomable and vico 2025-04-10 14:06:25 -04:00
Vitor Pamplona
5abb5f2f80 Merge pull request #1317 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-10 10:00:00 -04:00
Crowdin Bot
37b84108a7 New Crowdin translations by GitHub Action 2025-04-10 13:48:08 +00:00
Vitor Pamplona
5edc945341 Updates Compose, CoreKtx and SecurityKtx 2025-04-10 09:45:21 -04:00
Vitor Pamplona
3833f5f822 - Refactoring services to be part of the App Lifecycle,
- Migrated services to Flows that are active while their flows remain subscribed
- Improved OTS Decoupling
- Moved OTS verification procedures from the local cache to the data source
- Migrated the forceProxy options to lambdas that return an OkHttpClient
- Isolated Connectivity services, from Compose to Flow
- Isolated Tor services, from Compose to Tor (only starts if the Tor option is marked as internal)
- Isolated Memory trimming services, from Compose to Flow
- Isolated Image Caching services, from Compose to Flow
- Isolated Video Caching services
- Isolated Logging services
- Isolated NIP-95 Caching services
- Isolated Pokey receiver services
- Improved support for Tor in push notifications
- Isolated OkHttpClient services as flows
- Reduced the coupling of Context objects with singleton objects.
- Migrated UserFeedStates, StringFeedStates and ZapFeedStates to MutableStateFlow, avoiding feed updates on Main
- Forces a reconnect into relays that lost connection if any tor, connectivity or account changes
- Speeds up Base64 parser
2025-04-09 18:51:17 -04:00
Vitor Pamplona
652373bd01 Remove the warning saying the app name is being replaced on benchmark. 2025-04-09 16:28:18 -04:00
Vitor Pamplona
e883a46f53 Fixes deprecated warning 2025-04-09 16:20:14 -04:00
Vitor Pamplona
2b7cfa5560 Better name for the Blurhash class to avoid mistakes with its field. 2025-04-03 20:50:43 -04:00
Vitor Pamplona
ed860454a7 Moves activities to be singleInstance to avoid multiple startups when receiving share events from other apps. 2025-04-03 20:46:39 -04:00
Vitor Pamplona
ed0435061c Moves to cover all classes in the proguard. 2025-04-03 20:45:25 -04:00
Vitor Pamplona
223865827f Fixes tagging issue on new post. 2025-04-03 13:54:23 -04:00
Vitor Pamplona
ec25ef3f82 Adds flowerstr 2025-04-03 09:02:31 -04:00
Vitor Pamplona
7c4911f2dc Converts old ClickableTexts into new Compose Texts with clickable spans 2025-04-02 20:37:14 -04:00
Vitor Pamplona
4ac046fe39 Migrates Navigation to Compose Navigation. 2025-04-02 17:39:16 -04:00
Vitor Pamplona
bfd5845022 Updates Firebase and CBOR 2025-04-01 19:26:10 -04:00
Vitor Pamplona
d288010a83 Move the stringresource setup out of the Activity 2025-04-01 19:13:35 -04:00
Vitor Pamplona
834efe02bc Refactors SharedSettings state to:
- Minimize recompositions
- Improve code quality
2025-04-01 18:59:53 -04:00
Vitor Pamplona
1bd32c854c Makes sure the tryLogin function only runs if it is not already logged in (compostables might call it more times) 2025-04-01 18:58:15 -04:00
Vitor Pamplona
d08998736f Removes unecessary preview items from Cashu components. 2025-04-01 18:57:40 -04:00
Vitor Pamplona
a5f9b865ec Makes ZapAmount viewmodel stable 2025-04-01 18:57:17 -04:00
Vitor Pamplona
375931d4f9 Modernizing the edit access to LocalPreferences. 2025-04-01 18:53:30 -04:00
Vitor Pamplona
7e3f64fc61 Performance: Avoids saving the first collection on login 2025-04-01 18:51:29 -04:00
Vitor Pamplona
09024fbc61 Performance: Avoids creating OkHttp proxies directly on the OnCreate 2025-04-01 18:50:08 -04:00
Vitor Pamplona
a530ced594 Performance: Avoids creating a SharedSettings ViewModel on Galery 2025-04-01 18:49:26 -04:00
Vitor Pamplona
3089524a1f Cleaning up from the previous commit 2025-04-01 16:50:55 -04:00
Vitor Pamplona
5aa2aa4d54 Merge translations 2025-04-01 16:36:03 -04:00
Vitor Pamplona
12235216ff Moves The WIFI/Mobile data connection watcher to the compose.
Moves Relay Connection Manager to the Compose
Adds a 30 second delay to approve events on Amber
2025-04-01 16:35:46 -04:00
Vitor Pamplona
35b9648bab Merge pull request #1316 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-04-01 15:06:31 -04:00
Vitor Pamplona
cf2959d42e Refactors logged in page into its own package 2025-04-01 14:03:17 -04:00
Crowdin Bot
b5164d5dd0 New Crowdin translations by GitHub Action 2025-04-01 18:01:18 +00:00
Vitor Pamplona
05cb055b1d Migrates FDroid's push system to match Play services 2025-04-01 13:58:57 -04:00
Vitor Pamplona
f6bd12058f removes firebase logging 2025-04-01 11:56:55 -04:00
Vitor Pamplona
99ef1fc168 Removes Tor leaks when loading thumbnails for vides automatically from the ExoPlayer 2025-04-01 11:56:47 -04:00
Vitor Pamplona
bd02515209 Moves push registration notification into Compose after login to make sure we use the tor configuration of the active user. 2025-04-01 11:18:15 -04:00
Vitor Pamplona
b3f1b093ed Tor now listens to port changes and restart connections when it does change. 2025-03-31 20:32:15 -04:00
Vitor Pamplona
491d269f75 Merge pull request #1315 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-31 11:30:04 -04:00
Crowdin Bot
c79243a912 New Crowdin translations by GitHub Action 2025-03-31 15:11:49 +00:00
Vitor Pamplona
b762907d69 adds a mutex to make sure memory clearing only happens once at a time. 2025-03-31 11:09:23 -04:00
Vitor Pamplona
396831456c - Tor Cleanup:
- Only starts Tor when the "Internal" option is selected on the privacy settings
- Adds a mutex to make sure tor only starts once.
- Fixes bug when setting port before the service is running
- Moves tor start and stop to Compose actions, instead of activity
- Adds a 5 second delay before disconnecting Tor on the App's onPause
2025-03-31 11:09:04 -04:00
Vitor Pamplona
31e961c64f Fixes Video cropping due to rotated Exif metadata. 2025-03-30 10:51:32 -04:00
Vitor Pamplona
8baf9ad914 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-29 16:07:19 -04:00
Vitor Pamplona
c2b0154bb5 Fixes mark as seen when drafts are the last post in messages 2025-03-29 16:07:11 -04:00
Vitor Pamplona
c2b19c670b Merge pull request #1314 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-29 15:33:38 -04:00
Crowdin Bot
f97cfa81ca New Crowdin translations by GitHub Action 2025-03-29 19:22:34 +00:00
Vitor Pamplona
21643b781c Fixing language codes 2025-03-29 15:02:52 -04:00
Vitor Pamplona
5961db5c77 Testing the removal of generic Arabic translation to a language-country pair 2025-03-29 14:43:23 -04:00
Vitor Pamplona
99d5e9f1f7 Merge pull request #1313 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-29 14:38:55 -04:00
Crowdin Bot
818116d57b New Crowdin translations by GitHub Action 2025-03-29 18:36:37 +00:00
Vitor Pamplona
ef1e743fe2 removes language mapping for crowdin 2025-03-29 14:35:28 -04:00
Vitor Pamplona
df9568b667 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-29 12:25:54 -04:00
Vitor Pamplona
b5649e2607 Fixes: https://github.com/vitorpamplona/amethyst/issues/1309 2025-03-29 12:25:48 -04:00
Vitor Pamplona
92033b16d2 Updates Media, Camera, Compose and benchmark libs 2025-03-29 12:23:20 -04:00
Vitor Pamplona
9a440513ef Merge pull request #1312 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-29 11:11:05 -04:00
Crowdin Bot
89aa4a6f95 New Crowdin translations by GitHub Action 2025-03-29 15:02:23 +00:00
Vitor Pamplona
5ac73bbeba - Retries to reverse geolocate if fails due to the services long startup time.
- Adds support for the new and old ways to use reverse geolocation services.
- Hits all existing providers for location updates instead of just the network ones
- Uses past last known locations to kick start the cache.
- Improves logging to check potential failures in production.
2025-03-29 11:00:37 -04:00
Vitor Pamplona
76eee177a8 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-28 13:48:31 -04:00
Vitor Pamplona
4124724fb1 - Creates a new Group DM screen and moves DMs (NIP-17 and NIP-04) away from the New Post Screen.
- Separates all DM attachments as their own objects and holds them in cache until the message is sent.
- Adds previews for any number of urls, events and media uploads on new post screens.
- Adds zap split, zap raiser, geolocation symbols for DMs and channel messages
- Separates each component that can be added to a new post into its own package.
- Unifies processing for User Suggestions, Emoji Suggestions, Url Previews, location, draft versions, among text inputs
2025-03-28 13:48:22 -04:00
Vitor Pamplona
7921745fd9 Merge pull request #1311 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-27 17:54:50 -04:00
Crowdin Bot
c24c23f003 New Crowdin translations by GitHub Action 2025-03-27 21:41:53 +00:00
Vitor Pamplona
8a6b71e913 Fixing google services.json for benchmark app 2025-03-27 17:39:10 -04:00
Vitor Pamplona
29d046a094 Updates zoomable, vico, firebase, jackson and jna 2025-03-26 12:36:38 -04:00
Vitor Pamplona
c0423032c3 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-26 11:24:17 -04:00
Vitor Pamplona
fd7ff57a37 Merge pull request #1307 from davotoula/1301-use-note-renderer-for-file-header-and-video-events
Use CheckNewAndRenderNote method for FileHeaderEvent, FileStorageHeaderEvent and VideoEvent(s)
2025-03-26 11:23:41 -04:00
Vitor Pamplona
9a0a164ab5 Merge pull request #1308 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-26 11:22:51 -04:00
Vitor Pamplona
b35fd24a93 Fixes QR Code screen padding 2025-03-26 11:22:38 -04:00
Vitor Pamplona
aac366c690 - Migrates "Show Anyway" and "Show More" buttons to lighter version (Tonal)
- Improves tonal rendering on Light theme
- Lightens card surfaces
- Adjusts surfaceTint on Dark theme to match the elevation differences on the Light theme
2025-03-26 11:22:27 -04:00
Crowdin Bot
04a161dc47 New Crowdin translations by GitHub Action 2025-03-26 15:20:27 +00:00
David Kaspar
0d623b2ecc Use CheckNewAndRenderNote method for FileHeaderEvent, FileStorageHeaderEvent and VideoEvent(s) 2025-03-26 12:20:59 +00:00
Vitor Pamplona
aa9f1b6c4f Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-25 19:36:47 -04:00
Vitor Pamplona
8f77ecd891 Improves the rendering of the channel metadata changes
Redesigns the expandable top bar for NIP-28 chats
2025-03-25 19:36:39 -04:00
Vitor Pamplona
c4ab506c87 Merge pull request #1305 from davotoula/1292-url-decode-text-shared-to-app
use existing helper function that URL decodes message text
2025-03-25 18:06:42 -04:00
Vitor Pamplona
e75b451b79 Merge pull request #1306 from davotoula/remove-fdroid-from-readme
Update README.md
2025-03-25 18:05:44 -04:00
David Kaspar
7ad47bb137 Update README.md
remove fdroid from readme
2025-03-25 21:55:12 +00:00
David Kaspar
5518293813 use existing helper function that URL decodes message text 2025-03-25 20:38:08 +00:00
Vitor Pamplona
a23ad2dde7 Moves relay screens to it's own master package on the UI and reorganizes subpackages 2025-03-25 15:17:59 -04:00
Vitor Pamplona
b74bf44141 - Migrates NIP-28 creation to its own route.
- Adds picture upload for NIP-28 metadata
- Adds support for relay hints on NIP-28
- Improves edit field for chats
- Improves keyboard support for relay lists edit fields
2025-03-25 14:36:12 -04:00
Vitor Pamplona
84f10da006 Forces the root post in a thread to be observed at all times. That guarantees thread loading even if relays are slow and the user is scrolling. 2025-03-24 18:02:55 -04:00
Vitor Pamplona
d2c19cbbb4 Merge pull request #1300 from davotoula/sniff-content-type-from-extension-when-empty-in-response-headers
Sniff content type from extension when empty in response headers
2025-03-24 17:50:22 -04:00
Vitor Pamplona
ef582eceb6 Merge pull request #1303 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-24 17:49:27 -04:00
Crowdin Bot
258815d349 New Crowdin translations by GitHub Action 2025-03-24 21:36:42 +00:00
Vitor Pamplona
1b40e70058 Fixing the Notification order for multiple playbacks 2025-03-24 17:33:53 -04:00
Vitor Pamplona
43f1796058 Updates AGP 2025-03-24 17:33:17 -04:00
Vitor Pamplona
53803eb878 Merge pull request #1302 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-24 16:13:11 -04:00
Crowdin Bot
7b5a2d1312 New Crowdin translations by GitHub Action 2025-03-24 20:12:36 +00:00
Vitor Pamplona
8f9a4557b5 Enable Picture in Picture videos 2025-03-24 16:10:14 -04:00
David Kaspar
cdf2b10fd2 trim inline meta data to fix sniffing (required for DM messages) 2025-03-18 12:55:31 +00:00
David Kaspar
2f29a21d72 cleaner code saveContentDefault: use apply for clarity 2025-03-18 12:29:03 +00:00
David Kaspar
93ed81dedb cleaner code saveDownloadingIfNeeded: refactor to use when and remove unnecessary null-safe calls 2025-03-18 12:14:57 +00:00
David Kaspar
bcd636b46e sniff content type from extension when empty in response 2025-03-18 11:50:33 +00:00
Vitor Pamplona
6fd5f36ff5 Fix: Removes kind 30078 from search 2025-03-17 20:22:11 -04:00
Vitor Pamplona
4b79122899 Fixes the content type after image compression. 2025-03-17 20:07:25 -04:00
Vitor Pamplona
1df4d1db10 Removes debugging red border for full screen video 2025-03-17 14:30:17 -04:00
Vitor Pamplona
804194c3f5 Opens the fullscreen video playback with the right aspect ratio 2025-03-17 14:27:36 -04:00
Vitor Pamplona
fc159c4d1a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-17 14:27:20 -04:00
Vitor Pamplona
754b648877 Removing unnecessary playback logs 2025-03-17 14:27:13 -04:00
Vitor Pamplona
c1606c54be Merge pull request #1299 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-17 14:04:48 -04:00
Crowdin Bot
82864c2069 New Crowdin translations by GitHub Action 2025-03-17 17:58:07 +00:00
Vitor Pamplona
3dc2a4a2d3 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-17 13:55:50 -04:00
Vitor Pamplona
7d1a8652cf Merge pull request #1298 from davotoula/1297-trim-inline-meta-data-before-saving-file
Trim meta data from file name before saving file to disk
2025-03-17 13:55:40 -04:00
Vitor Pamplona
96ac674eaa Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-17 13:55:18 -04:00
Vitor Pamplona
664ea86e6c Adds tor classes to the proguard exceptions 2025-03-17 13:55:12 -04:00
Vitor Pamplona
5aa2eb4499 Makes the bechmark build its own package name to facilitate testing in real phones. 2025-03-17 13:54:57 -04:00
Vitor Pamplona
0b9a44ab9b adds icon for Android studio 2025-03-17 13:53:42 -04:00
Vitor Pamplona
e3e35fa1eb - Refactoring playback service and its client.
- Created a pool of ExoPlayers to avoid creating new instances when scrolling.
- More accurately tracks MediaSessions
- Splits Caching layers into their own files
- Splits VideoView into multiple files
- Specializes the old Playback Manager into pools.
- Reorganizes video playback dependencies inside a single package.
- Structures callback uris to be per media item as opposed to per controller.
2025-03-17 13:53:26 -04:00
David Kaspar
221c58126c moved the trimming of inline meta-data to where File is created 2025-03-16 21:36:13 +00:00
David Kaspar
2cdf5d6673 trim meta data from file name before saving file to disk 2025-03-16 21:05:09 +00:00
Vitor Pamplona
2d5697cc9a Merge pull request #1296 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-15 14:25:09 -04:00
Crowdin Bot
d4ae290364 New Crowdin translations by GitHub Action 2025-03-15 16:46:06 +00:00
Vitor Pamplona
a690359541 Merge pull request #1295 from davotoula/1294-failed-to-save-the-image
Failed to save the image
2025-03-15 12:44:50 -04:00
David Kaspar
b3fa6401f6 Remove accidental import of okio.use 2025-03-15 13:58:04 +00:00
David Kaspar
b58d74138d cleaner code 2025-03-15 13:27:46 +00:00
David Kaspar
e6c413b176 Get mime type from extension if mime type in response header is null for a application/octet-stream content type 2025-03-15 13:13:46 +00:00
Vitor Pamplona
c1399d129e Migrates from old keyboard option interface 2025-03-14 14:20:08 -04:00
Vitor Pamplona
337a5e3cae Adds a flag to avoid new linting warnings 2025-03-14 14:12:12 -04:00
Vitor Pamplona
32b184a9cd Improves lightning error messages 2025-03-14 13:57:44 -04:00
Vitor Pamplona
fd538ebde1 Improves multiple error dialogs appearing when zapping from the reactions bar in the feed. 2025-03-14 13:57:33 -04:00
Vitor Pamplona
d76de0123a Forces aspect ratio of when loading an image to minimize resizing of the UI after the image is loaded. 2025-03-14 10:27:13 -04:00
Vitor Pamplona
de2b1a6c95 Optimizes caching for hidden chars on emoji 2025-03-14 10:24:03 -04:00
Vitor Pamplona
311964ce33 Caches video and image aspect ratios to recompose with the correct size 2025-03-13 19:30:57 -04:00
Vitor Pamplona
4d4c7d5b72 Moves DVM's last announcement restriction from 90 days to 365 days 2025-03-13 17:38:05 -04:00
Vitor Pamplona
66535e11ce - Removes the TAG_SIZE constant on all tags because each parser might use a different size. There is no minimum size for tags.
- Moves the tag parsing checks to use ensure.
- Explicitly writes matching methods to avoid hiding complexity beyond utility functions.
2025-03-13 17:24:20 -04:00
Vitor Pamplona
682dc62703 Updates zoomable, vico and mockk 2025-03-13 10:47:31 -04:00
Vitor Pamplona
ba7bd0bd8a Migrates to AGP 8.9 2025-03-13 09:21:50 -04:00
Vitor Pamplona
64b42ede7c Updates compose, firebase, navigation 2025-03-13 09:04:54 -04:00
Vitor Pamplona
8afe1ae537 Marks PrivateOutbox contents as encrypted 2025-03-13 09:04:39 -04:00
Vitor Pamplona
258a550e15 updating android studio 2025-03-13 09:00:44 -04:00
Vitor Pamplona
1603a7473a Adds support for Harassment reports. 2025-03-12 15:51:17 -04:00
Vitor Pamplona
ba6786cdcf Updates nip70 Protected Tags to the new structure 2025-03-12 14:51:47 -04:00
Vitor Pamplona
b017e03728 Updates NIP-56 to the newest tag structure for Quartz 2025-03-12 14:51:23 -04:00
Vitor Pamplona
5766bb1887 First idea without changing NIP-44 2025-03-12 09:41:18 -04:00
Vitor Pamplona
40bc651860 Making sure iMetas are unique 2025-03-11 14:27:58 -04:00
Vitor Pamplona
8a779e7f3c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-11 14:26:29 -04:00
Vitor Pamplona
350e2a5a90 Fixes loading iMeta from drafts and reset them after use 2025-03-11 14:17:59 -04:00
Vitor Pamplona
a75e268466 Merge pull request #1291 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-11 14:00:21 -04:00
Crowdin Bot
2fd456e33d New Crowdin translations by GitHub Action 2025-03-11 17:58:06 +00:00
Vitor Pamplona
64f7401b46 Fixes not including iMeta tags on public chats 2025-03-11 13:57:56 -04:00
Vitor Pamplona
5dcb1350ac Losing the test case for unit tests in CI 2025-03-11 13:57:39 -04:00
Vitor Pamplona
3aa370d746 Fixes bug that the room changes after citing a user on NIP-17 2025-03-11 13:46:55 -04:00
Vitor Pamplona
583855de4f Generalizes the chat upload UI and applies it to channels 2025-03-11 13:46:36 -04:00
Vitor Pamplona
05acc19c9a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-10 20:34:42 -04:00
Vitor Pamplona
71f6fe2375 Merge pull request #1290 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-10 20:33:58 -04:00
Vitor Pamplona
286730c50d Adds name to the rendering of people lists in the feed 2025-03-10 20:33:42 -04:00
Crowdin Bot
57d6145677 New Crowdin translations by GitHub Action 2025-03-11 00:06:10 +00:00
Vitor Pamplona
94c78c6327 Minor refactoring of chat screen components. 2025-03-10 20:04:18 -04:00
Vitor Pamplona
7d2b7789d3 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-10 19:33:19 -04:00
Vitor Pamplona
fe55ee1818 Starts the migration of public chats to the new structure
Starts the migration of drafts to the new structure
2025-03-10 19:33:12 -04:00
Vitor Pamplona
58b6be73ee as per https://github.com/nostr-protocol/nips/pull/1836 2025-03-10 18:32:43 -04:00
David Kaspar
cd646951cf Merge pull request #1289 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-07 20:42:39 +00:00
Crowdin Bot
3789ba6c07 New Crowdin translations by GitHub Action 2025-03-07 20:13:11 +00:00
Vitor Pamplona
429f9f81c4 re organizing the private dm composables 2025-03-07 15:11:17 -05:00
Vitor Pamplona
639e2bb645 Updates our base textfield and moves to it's a more general package 2025-03-07 10:52:28 -05:00
David Kaspar
797bea9e72 Merge pull request #1285 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-06 21:40:09 +00:00
Crowdin Bot
1c28aa30f0 New Crowdin translations by GitHub Action 2025-03-06 21:28:31 +00:00
Vitor Pamplona
52e67c9ab6 Uses NIP-22 to reply to Highlights 2025-03-06 16:25:57 -05:00
Vitor Pamplona
fa9ff0c52a Uses NIP-22 to reply to Long form posts. 2025-03-06 16:25:47 -05:00
Vitor Pamplona
9cd73c2663 Adding secret emojis to reactions 2025-03-05 18:24:03 -05:00
Vitor Pamplona
8722f37fa5 Improving the UI of the Custom Emoji page. 2025-03-05 16:56:34 -05:00
Vitor Pamplona
987d8fa5dc Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt
#	quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/StoryOptionTag.kt
#	quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt
#	quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt
#	quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt
2025-03-05 16:14:50 -05:00
Vitor Pamplona
f4d06eee6d Simplifying conditionals for the ATag parser 2025-03-05 16:13:33 -05:00
Vitor Pamplona
e2459e3fbd Simplifying conditionals for the ATag parser 2025-03-05 15:58:55 -05:00
Vitor Pamplona
9adc7a7adf Fixing padding to match the line padding 2025-03-05 15:40:49 -05:00
Vitor Pamplona
d474bb0016 fixes misaligned mute card on notifications 2025-03-05 15:37:57 -05:00
Vitor Pamplona
3942847d16 Refactors chats into public, private and list views for tablets and phones 2025-03-05 15:30:02 -05:00
Vitor Pamplona
2b2b219536 no message 2025-03-05 15:29:16 -05:00
Vitor Pamplona
91c19dc2a1 Adds a wait and display function to make sure the image url doesn't appear while the image is loading from disk 2025-03-05 15:26:52 -05:00
Vitor Pamplona
0536c141b7 Speeding up the loading of emojis 2025-03-05 15:23:37 -05:00
Vitor Pamplona
16331d268d Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-03-05 13:40:59 -05:00
Vitor Pamplona
7e2724ea31 adds support for secret emojis 2025-03-05 13:40:51 -05:00
Vitor Pamplona
8facd02e33 Merge pull request #1284 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-03-04 18:02:13 -05:00
Crowdin Bot
90872b2160 New Crowdin translations by GitHub Action 2025-03-04 23:00:16 +00:00
Vitor Pamplona
0580aef4a2 - Migrates NIP-17 DM new message model to its own model and disable some of the generic behavior from new posts.
- Fixes a bug on edits showing a previous edit.
- Refactors user suggestions and lastword procedures
- Adds a DM inline reply from other parts of the app, like on notification.
- Hides Retweet button from DMs on Notifications
- Fixes colors for drafts in chats.
- Adds a nav function that computes the destination only after the user clicks on it and on a coroutine.
2025-03-04 17:57:47 -05:00
Vitor Pamplona
1a001e7bb3 Moves GeohashPrecision class to Quartz 2025-03-01 18:24:55 -05:00
Vitor Pamplona
4b91cfe809 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-02-28 15:26:58 -05:00
Vitor Pamplona
3d5d7b4987 adds nostr elites as a default global relay 2025-02-27 19:15:55 -05:00
Vitor Pamplona
ca0b6ef2ba Fixing error message on relay notices 2025-02-27 19:15:40 -05:00
Vitor Pamplona
3103b51cf4 Merge pull request #1281 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-02-27 18:45:33 -05:00
Crowdin Bot
8bbc017f91 New Crowdin translations by GitHub Action 2025-02-27 23:37:03 +00:00
Vitor Pamplona
0df07ddfcb - Adds support for https://github.com/nostr-protocol/nips/pull/1770
- Improves signer utils for testing
- Improves test cases for User metadata
2025-02-27 18:34:10 -05:00
Vitor Pamplona
8f0879b8e5 Updates agp, activity compose, accompanist, navigation and zoomable 2025-02-26 15:55:39 -05:00
Vitor Pamplona
f5ac8faa45 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-02-25 19:27:20 -05:00
Vitor Pamplona
40aa469866 Fixes unable to click in the last tab of the profile 2025-02-25 18:26:01 -05:00
Vitor Pamplona
d2590b969b Merge pull request #1280 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-02-25 18:20:05 -05:00
Crowdin Bot
2de3ba82cc New Crowdin translations by GitHub Action 2025-02-25 23:18:41 +00:00
Vitor Pamplona
b3dec691d7 losing up the probability to make tests pass 2025-02-25 18:16:20 -05:00
Vitor Pamplona
b40b37b6ac Adds the logged in user' posts to a user in the user's profile.
Refactors the user profile screen in many packages.
2025-02-25 18:09:04 -05:00
Vitor Pamplona
e6cd3a99c5 Fixes bug on saving Encrypted files on DMs.
Fixes bug when using partial downloads to display videos on DMs.
2025-02-25 16:43:15 -05:00
Vitor Pamplona
d5d289a834 Uses note.toEventUri to make sure the relay hint is correct
Avoids crash when adding an event to gallery without a relay hint.
2025-02-24 20:46:33 -05:00
Vitor Pamplona
a2d9f31a8a Assigning hints to NIPs until NIP-53
Speeding up Bech32 and Hex processing
Updating Boosts and Reposts to the new way of declaring tags
Renaming the SHA256 hash tag from NIP94 to not confuse with # hashtags
2025-02-24 17:09:08 -05:00
Vitor Pamplona
3790325a0f uses const val for faster MurMur 2025-02-22 16:19:01 -05:00
Vitor Pamplona
96f57b2c90 Fixes tag name on ImageTag 2025-02-22 16:18:46 -05:00
Vitor Pamplona
4b10dd58df It's slightly faster to reject params in one line than on many. 2025-02-22 16:17:10 -05:00
Vitor Pamplona
dc4240a8f5 Increasing the amount of addresses 2025-02-20 18:06:17 -05:00
Vitor Pamplona
3220d793b4 Using a linked set for the relayDB is faster to loop through on mightContain 2025-02-20 17:44:36 -05:00
Vitor Pamplona
84a52a1ce0 Speeding MurMur up
Adding more test cases
Mapping out memory used by filter
2025-02-20 17:10:24 -05:00
Vitor Pamplona
aaf86bf53e Adds a Bloom-based hint indexer with MurMur hash 2025-02-20 14:07:10 -05:00
Vitor Pamplona
94e0f4ed02 Performance improvements on the PoW miner 2025-02-18 20:44:24 -05:00
Vitor Pamplona
af29133272 Simplifies the benchmark for PoW 2025-02-18 19:00:05 -05:00
Vitor Pamplona
06240b1f15 Updates libsecp256k 2025-02-18 18:14:52 -05:00
Vitor Pamplona
d47ec06b19 Deleting the experimental serializer test 2025-02-18 17:51:34 -05:00
Vitor Pamplona
9689acb5f3 Deletes experimental serializer on NIP-01
Moves classes to the appropriate packages on NIP-01
Adds a PoW miner to quartz.
Adds a PoW commit evaluator as a bytearray of the ID
2025-02-18 17:47:34 -05:00
Vitor Pamplona
bcb3838e85 Fixes isTagged for events 2025-02-18 12:04:39 -05:00
Vitor Pamplona
4fcf962c71 Fixes r-tag migration 2025-02-18 12:00:22 -05:00
Vitor Pamplona
2de4bb1adc Reverses ATag Generalization (It just made things more complicated) 2025-02-18 11:51:16 -05:00
Vitor Pamplona
9e1a2bb1e4 Removes the notion of generic Address references 2025-02-18 11:29:52 -05:00
Vitor Pamplona
8720b00d4f Separates the random benchmark from the Sign/Verify benchmarks 2025-02-18 10:19:51 -05:00
Vitor Pamplona
9c35008fc4 Moves Sha256 to a pool of digests to double the hashing performance 2025-02-18 09:40:17 -05:00
Vitor Pamplona
fad08830a1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-02-17 19:13:14 -05:00
Vitor Pamplona
6d9964a1cb Decoupling NIP01 methods from CryptoUtils
Decoupling Encryption and Decryptions from CryptoUtils
Decoupling live instances of JNI bindings for Secp and LibSodium from CryptoUtils
Decoupling key cache from CryptoUtils
Reorganizes NIP-04 to match new package structure
Adjusts test structures to match
2025-02-17 19:13:05 -05:00
Vitor Pamplona
70ced84dc3 Merge pull request #1278 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-02-17 18:59:34 -05:00
Crowdin Bot
50da982b36 New Crowdin translations by GitHub Action 2025-02-17 19:14:58 +00:00
Vitor Pamplona
7ad8b2ce46 Migrates LocalCache from being based on ATags to Addresses, removing the need to use memory space to store relay hints 2025-02-17 14:12:54 -05:00
Vitor Pamplona
94ffe783e9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-02-17 11:18:47 -05:00
Vitor Pamplona
41e47fa71d Fixes running new events on the main thread. 2025-02-17 11:17:38 -05:00
Vitor Pamplona
08dd634eae Fixes crash when evaluating the isHex function for an emoji 2025-02-17 11:12:47 -05:00
David Kaspar
cb3aa6c329 Merge pull request #1277 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-02-16 19:42:57 +01:00
Crowdin Bot
4e2e24c7ab New Crowdin translations by GitHub Action 2025-02-16 16:55:21 +00:00
Vitor Pamplona
67106e8a11 Renames PoW processor to parser 2025-02-16 11:53:23 -05:00
Vitor Pamplona
06c7ea0123 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-02-14 16:31:33 -05:00
Vitor Pamplona
8022ac525f Makes sure deletions happen in the background thread. 2025-02-14 16:29:10 -05:00
Vitor Pamplona
534f1ed8cd Adds Tim Bourma's test case 2025-02-14 16:28:56 -05:00
Vitor Pamplona
793b3fcd16 Renames PoW Parser to open space for the PoW creator 2025-02-14 16:28:40 -05:00
Vitor Pamplona
a4f84227b6 Merge pull request #1276 from MoonDusk1996/main
fix wrong word
2025-02-14 14:48:26 -05:00
Dusk
e921f26d5f fix wrong word 2025-02-14 13:34:59 -04:00
David Kaspar
0c3a792a84 Merge pull request #1275 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-02-14 17:15:44 +01:00
Crowdin Bot
6481fcbecd New Crowdin translations by GitHub Action 2025-02-14 16:07:00 +00:00
Vitor Pamplona
0f6c3b9a3a Moves to vico chart 2.0 2025-02-14 11:05:00 -05:00
Vitor Pamplona
dcb0e31cd6 Updating activity compose, coil, combosebom, firebasebom, fragmentktx, kotlinserialization, mockk and navigation compose 2025-02-13 19:02:15 -05:00
Vitor Pamplona
09cff7c930 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-02-13 19:01:37 -05:00
Vitor Pamplona
cb121835f5 Stops using vico's forEach custom function (it's going to be deleted in the next version) 2025-02-13 19:01:30 -05:00
Vitor Pamplona
0610416f61 Updates AGP to 8.8.1 2025-02-13 17:59:23 -05:00
Vitor Pamplona
f0116ad06e Merge pull request #1274 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-02-13 17:55:17 -05:00
Crowdin Bot
89664a2813 New Crowdin translations by GitHub Action 2025-02-13 22:49:05 +00:00
Vitor Pamplona
9f465d3296 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	amethyst/src/main/res/values-uz-rUZ/strings.xml
2025-02-13 17:40:00 -05:00
Vitor Pamplona
9c21a8a7da Massive refactoring of Quartz to:
- Define each tag in their own class.
- Allow extension functions to additional responsibilities to other classes
- Migrate from hardcoded tag filters in events to the Tag's parser and assemble functions.
- Migrate hardcoded event.create to builders that use extension functions
- Restructures threading infrastructure for NIP-10
- Decouple the event signing from the Event building functions via event templates
- Create classes to represent Tags and TagArrays and use extension functions to add domain-related methods to the tag array of each nip.
- Uses external functions on event template builders to better point to which functions and which tags can be used in which event kinds.
- Separates Event kinds in packages inside each nip.
- Improves support for NIP-89
- Correctly establishes which imeta params can be used in each nip (video, picture, files)
- Decouples the iMeta builder from any nip.
- Fixes mute list word and user removal when inserted from a different client.
- Migrates the Account class to avoiding having to build each Event inside of it
2025-02-13 17:39:13 -05:00
Vitor Pamplona
0f3b3c4b2d Adds support for NIP-73 2025-02-13 17:19:09 -05:00
Vitor Pamplona
68d4686424 Adds NostrMedia as a paid blossom relay option 2025-02-13 17:10:51 -05:00
Vitor Pamplona
e2a812a416 Merge pull request #1272 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-02-10 08:48:44 -05:00
Crowdin Bot
2eece49b9b New Crowdin translations by GitHub Action 2025-02-10 13:43:23 +00:00
Vitor Pamplona
c34c107a7c Merge pull request #1270 from believethehype/gallery_options
add option for 2 profile gallery views
2025-02-10 08:41:37 -05:00
dbth
e9861d80f6 add option for 2 profile gallery views 2025-02-01 21:48:26 +01:00
Vitor Pamplona
7b84c81d9c Merge pull request #1267 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-30 14:49:30 -05:00
Crowdin Bot
9caf0d28f5 New Crowdin translations by GitHub Action 2025-01-30 19:48:11 +00:00
Vitor Pamplona
78e5f5e277 Merge pull request #1265 from davotoula/1258-exlude-FileHeaderEvent-from-search-results
Exclude file header events from search results
2025-01-30 14:46:55 -05:00
David Kaspar
1bd52f163f extract logical expression for which notes to exclude from search results 2025-01-28 11:55:17 +01:00
David Kaspar
d825995ef4 Exclude FileHeaderEvent from note search results (direct note id search and free text) 2025-01-28 11:39:46 +01:00
Vitor Pamplona
3e1e3e80fb Merge pull request #1264 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-25 08:55:44 -05:00
Crowdin Bot
951a3a73d0 New Crowdin translations by GitHub Action 2025-01-25 13:54:41 +00:00
Vitor Pamplona
8a9fdd2c34 Merge pull request #1263 from davotoula/ugrade-gradle-and-agp
upgrade gradle to 8.10.2
2025-01-25 08:53:29 -05:00
David Kaspar
5dbba6d06d upgrade gradle to 8.10.2
upgrade agp to 8.8.0
2025-01-25 09:18:57 +00:00
Vitor Pamplona
5ee754c71a Allows the choice of limits on Count 2025-01-15 13:16:44 -05:00
Vitor Pamplona
a739329ab8 Merge pull request #1257 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-15 11:54:44 -05:00
Crowdin Bot
c12febf980 New Crowdin translations by GitHub Action 2025-01-15 16:49:57 +00:00
Vitor Pamplona
c25d64558e - Normalizes the use of ETag
- Creates a BaseReplaceable event to avoid duplicates of empty d tags.
2025-01-15 11:48:01 -05:00
Vitor Pamplona
5a3c821da4 make sure the pow functions are static in java 2025-01-15 10:11:17 -05:00
Vitor Pamplona
b90aac496e Moves PoW tag to the new style of tag builders. 2025-01-15 10:10:10 -05:00
Vitor Pamplona
4b52cdb101 Updates NIP-14 subjects to the new style of extension functions 2025-01-15 10:09:52 -05:00
Vitor Pamplona
4a4f03c12b Refactor proxyTag function names 2025-01-15 09:56:15 -05:00
Vitor Pamplona
1f9aeddb57 Adds support for NIP-48 to Quartz 2025-01-15 09:17:32 -05:00
Vitor Pamplona
f3b049382e Renames Gossip to Rumor to match names on the NIP-59 2025-01-15 08:58:08 -05:00
Vitor Pamplona
d1c64a0d15 Removing additional dependencies in the base Event class 2025-01-15 08:53:13 -05:00
Vitor Pamplona
8e25271bd6 Improves the documentation of the TagArray functions 2025-01-15 08:39:41 -05:00
Vitor Pamplona
0029df4575 Merge pull request #1256 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-14 19:44:32 -05:00
Crowdin Bot
b0e029cd59 New Crowdin translations by GitHub Action 2025-01-15 00:37:17 +00:00
Vitor Pamplona
d19ae56afa - Migrates Zap splits, zapraisers, subject, alts and content warning to their own packages.
- Adds protected event tags
2025-01-14 19:35:16 -05:00
Vitor Pamplona
502d39c893 Generalizes the hashtag and index tag parser for the content as well as the way to pass params to build them on create. 2025-01-14 16:59:53 -05:00
Vitor Pamplona
4f774ae3c5 Simple refactoring to avoid empty challenges. 2025-01-14 16:58:16 -05:00
Vitor Pamplona
86a9fb2af5 Moves the relay structure to quartz 2025-01-14 12:09:25 -05:00
Vitor Pamplona
e36e49cc88 Breakes the Command and Message parsers out of the Relay class 2025-01-14 11:47:52 -05:00
Vitor Pamplona
3ac303c724 Starts the work to filter events that return from the relay to make sure they match the subscription submitted 2025-01-13 19:42:43 -05:00
Vitor Pamplona
e3e90229ce Separates Typed Relay class from a SimpleRelay class to prepare to move to Quartz 2025-01-13 19:15:20 -05:00
Vitor Pamplona
9b0f24ba94 Removing duplicated permission for the camera 2025-01-13 13:03:40 -05:00
Vitor Pamplona
7f6b0ef7c2 Fixes package names for filters that moved to quartz 2025-01-13 13:03:22 -05:00
Vitor Pamplona
5c805d7551 Fixes greek name for Backup keys 2025-01-13 12:46:23 -05:00
Vitor Pamplona
57fe3740fc Moves limits and Filters to quartz 2025-01-13 12:46:15 -05:00
Vitor Pamplona
b871954293 Moves NIP96 parsing test to Quartz 2025-01-13 12:23:57 -05:00
Vitor Pamplona
d66e8d4cde refactoring android tests to match refactoring of package names
moving some nip96 stuff to quartz
2025-01-13 12:20:02 -05:00
Vitor Pamplona
103210ade4 Moves pow test to pow package 2025-01-13 10:44:15 -05:00
Vitor Pamplona
0d4936cf4b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2025-01-13 10:38:53 -05:00
Vitor Pamplona
d2b731e372 Massive refactoring of quartz to prepare for nip-based packages. 2025-01-13 10:38:45 -05:00
Vitor Pamplona
ceba31ec61 Merge pull request #1255 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-13 09:51:50 -05:00
Crowdin Bot
9684940265 New Crowdin translations by GitHub Action 2025-01-13 14:49:44 +00:00
Vitor Pamplona
dd7387a3f5 Merge pull request #1254 from greenart7c3/main
Move profile edit dialog to a screen
2025-01-13 09:48:13 -05:00
greenart7c3
020ead3c3f Move profile edit dialog to a screen 2025-01-08 10:47:32 -03:00
Vitor Pamplona
1430ba4745 Moves Event ID hashing to a new class 2025-01-06 15:13:14 -05:00
Vitor Pamplona
5f577df819 Refactors SHA-256 methods to its own class 2025-01-06 12:56:57 -05:00
Vitor Pamplona
0971b716e1 Refactors secure random to extension class 2025-01-06 12:56:29 -05:00
Vitor Pamplona
53458ab484 Avoids showing youtube videos on the feed since we can't play them 2025-01-06 12:54:08 -05:00
Vitor Pamplona
879f70fcbb Merge pull request #1252 from greenart7c3/main
Add some missing functions to NostrClient
2025-01-05 14:23:40 -05:00
greenart7c3
eb93664968 Add getRelay 2025-01-05 14:43:44 -03:00
Vitor Pamplona
9816577f00 v0.94.3 2025-01-05 12:16:06 -05:00
Vitor Pamplona
306ce9a36c Adds iMeta tags to GIF urls 2025-01-05 12:14:28 -05:00
greenart7c3
10459b0a7a Add getAll 2025-01-05 13:58:48 -03:00
Vitor Pamplona
f1d61daa04 Merge pull request #1250 from geovnn/main
Keep note state across LazyColumns recompositions
2025-01-05 11:23:43 -05:00
Vitor Pamplona
1ca0abf30d Minor refactoring: uploaders can do image and videos. 2025-01-05 11:22:40 -05:00
Vitor Pamplona
d0a9c1ccd0 Fixes: Only close the upload screen if the video upload is a success on the Media tab 2025-01-05 11:22:15 -05:00
Vitor Pamplona
617f1ba2a6 Fixes the extra empty kind 20 post when uploading videos. 2025-01-05 11:21:32 -05:00
Giovanni Gatti
f1141e698e Use rememberSaveable to keep note state across LazyColumns recompositions 2025-01-05 00:52:53 +01:00
Vitor Pamplona
f94feebbd7 v0.94.2 2025-01-04 17:47:13 -05:00
Vitor Pamplona
82d006f65e letter change to avoid mixing with other log messages 2025-01-04 17:44:43 -05:00
Vitor Pamplona
3a553fa550 Updates JNA and URI Normalizer 2025-01-04 17:41:47 -05:00
Vitor Pamplona
1e9c3a47a4 Merge pull request #1249 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-04 17:40:46 -05:00
Crowdin Bot
3d9b4431ad New Crowdin translations by GitHub Action 2025-01-04 22:37:25 +00:00
Vitor Pamplona
6522d74253 Fixes the Tor leaks for relays and some image content. 2025-01-04 17:35:34 -05:00
Vitor Pamplona
8a8ae4fb8b Merge pull request #1248 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-03 20:39:08 -05:00
Crowdin Bot
6cac5fa5e4 New Crowdin translations by GitHub Action 2025-01-04 01:38:36 +00:00
Vitor Pamplona
aa3063acae v0.94.1 2025-01-03 20:36:26 -05:00
Vitor Pamplona
46de90ea65 Removing the unecessary loading of the emoji suggestions when creating the new post view model 2025-01-03 20:36:18 -05:00
Vitor Pamplona
aa44560714 Adds support for Custom Reaction selection on new posts and direct GIF url usage. 2025-01-03 20:29:31 -05:00
Vitor Pamplona
c9a6b63f67 v0.94.0 2025-01-03 11:02:15 -05:00
David Kaspar
47211e9136 Merge pull request #1247 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-03 15:29:44 +00:00
Crowdin Bot
cfbcc49940 New Crowdin translations by GitHub Action 2025-01-03 15:28:37 +00:00
Vitor Pamplona
b4a497fd11 Merge pull request #1246 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-03 10:27:22 -05:00
Crowdin Bot
9aa287f96a New Crowdin translations by GitHub Action 2025-01-03 15:16:06 +00:00
David Kaspar
a6880f339c Merge pull request #1245 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-03 15:14:36 +00:00
Crowdin Bot
43996499ba New Crowdin translations by GitHub Action 2025-01-03 15:13:40 +00:00
Vitor Pamplona
5c0c1acc4a Allowing multiple images on a single event to be displayed in the Profile gallery 2025-01-03 10:11:10 -05:00
Vitor Pamplona
4bfacbc11e Improves the spacing in the layout of the gallery 2025-01-02 19:42:17 -05:00
Vitor Pamplona
633c9e8dcf Merge pull request #1244 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-02 19:04:53 -05:00
Crowdin Bot
0f2a4d104c New Crowdin translations by GitHub Action 2025-01-02 23:55:24 +00:00
Vitor Pamplona
fb92d2da2f Merge pull request #1243 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-02 18:54:30 -05:00
Vitor Pamplona
d29e90fe8f Adjusts the top bar when uploading media to DMs to match the chat top bar 2025-01-02 18:53:44 -05:00
Crowdin Bot
27329b7a0f New Crowdin translations by GitHub Action 2025-01-02 23:11:04 +00:00
Vitor Pamplona
577ef6437f removes logs 2025-01-02 18:09:27 -05:00
Vitor Pamplona
dbe593db98 Adds a server and compression selection screen when uploading images on DMs 2025-01-02 18:06:41 -05:00
Vitor Pamplona
ff9509b649 Refactors user suggestion views 2025-01-02 17:58:06 -05:00
Vitor Pamplona
2772bdff8b Increases the number of active playbacks on the screen 2025-01-02 10:52:17 -05:00
Vitor Pamplona
c6869eaff6 Merge pull request #1242 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2025-01-02 09:04:49 -05:00
Crowdin Bot
663f5f282e New Crowdin translations by GitHub Action 2025-01-02 13:57:39 +00:00
Vitor Pamplona
9f147f03c7 Fixes https://github.com/vitorpamplona/amethyst/issues/1241 2025-01-02 08:55:42 -05:00
Vitor Pamplona
0ceccb23c6 Avoids cutting off some of the payables in the Pay intent screen 2024-12-31 16:21:35 -05:00
Vitor Pamplona
ee65169896 Final touches of the new error message dashboard 2024-12-31 16:21:14 -05:00
Vitor Pamplona
6ac930c241 Don't show the "message the user" button when the problem is local 2024-12-31 16:20:53 -05:00
Vitor Pamplona
a09b8c5862 Moves to using cancellable coroutines to avoid cancelling the parent group all together. 2024-12-31 16:20:10 -05:00
Vitor Pamplona
160d4722c0 Refactoring Zap Error message screen to allow sending messages directly to each receiver with an error 2024-12-31 15:51:31 -05:00
Vitor Pamplona
497ae937fd Fixes the navigation padding on Samsung devices for the Shorts tab. 2024-12-30 20:32:53 -05:00
Vitor Pamplona
b366aee1a9 Makes sure to keep the chat's input on top of the system nav 2024-12-30 20:13:38 -05:00
Vitor Pamplona
09539ac22a adds new rules for proguard 2024-12-30 19:57:31 -05:00
Vitor Pamplona
e1e3a0e6a0 Expands the Around me filter to 50km 2024-12-30 19:57:20 -05:00
Vitor Pamplona
54e6be8ef3 Fixes notes appearing of replies for some recipes 2024-12-30 16:03:00 -05:00
Vitor Pamplona
e8462fe67f Fully removing the swipe to delete from right to left. 2024-12-30 15:56:24 -05:00
Vitor Pamplona
626daa9967 Refactoring 2024-12-30 15:43:09 -05:00
Vitor Pamplona
266d4cb34f Solves crashing when a p-tag contains only "ffff" 2024-12-30 15:42:54 -05:00
Vitor Pamplona
fc72e20da7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-12-30 08:51:24 -05:00
Vitor Pamplona
594abd1d7a Updates zoomable 2024-12-30 08:51:14 -05:00
Vitor Pamplona
a0d8a770b7 Merge pull request #1239 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-30 08:50:31 -05:00
Crowdin Bot
fc7b44b9d0 New Crowdin translations by GitHub Action 2024-12-30 13:39:56 +00:00
Vitor Pamplona
00eb4e364a Merge pull request #1238 from davotoula/display-toast-on-media-download-start
Display toast on media download start
2024-12-30 08:38:31 -05:00
David Kaspar
e88a36187d added translations 2024-12-30 14:16:30 +01:00
David Kaspar
21d80518c9 strings refactor 2024-12-30 14:07:06 +01:00
David Kaspar
7999608f28 added translatable strings 2024-12-30 14:03:28 +01:00
David Kaspar
ee20508e4b added video download started toast 2024-12-30 13:56:39 +01:00
David Kaspar
1231a1483f Corrected "added to profile gallery" toast text 2024-12-30 13:45:02 +01:00
David Kaspar
f8dd82f592 added a first toast for start of image download 2024-12-30 13:21:11 +01:00
Vitor Pamplona
ca00e6a302 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-12-28 16:43:34 -05:00
Vitor Pamplona
aeba5d1b51 delete unnecessary comment 2024-12-28 16:43:05 -05:00
Vitor Pamplona
3e014e7b0f Merge pull request #1237 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-28 15:32:06 -05:00
Crowdin Bot
95cda8ec08 New Crowdin translations by GitHub Action 2024-12-28 20:20:54 +00:00
Vitor Pamplona
1f3a818922 Remove crossfades and double composition on image success 2024-12-28 15:15:52 -05:00
Vitor Pamplona
7fe02fcf2c Disables delete draft from swipe from right to left. Only left to right remains available. 2024-12-27 18:25:24 -05:00
Vitor Pamplona
1c52887e3a Merge pull request #1236 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-27 17:02:07 -05:00
Crowdin Bot
62d95e2128 New Crowdin translations by GitHub Action 2024-12-27 21:52:27 +00:00
Vitor Pamplona
d0961d08da Fixes edge to edge issues when the keyboard is shown. 2024-12-27 16:49:43 -05:00
Vitor Pamplona
f38383d4e4 Removes debugging logs 2024-12-25 11:09:20 -05:00
Vitor Pamplona
739a073d4e Updates Vico, mockk, kotlin and adaptive acompanist 2024-12-24 18:22:53 -05:00
Vitor Pamplona
a195167a56 Fixes keyboard overriding the relay settings screen 2024-12-24 17:55:26 -05:00
Vitor Pamplona
72c089858d Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-12-24 16:05:43 -05:00
Vitor Pamplona
92fcf05557 Fixes double quotes on NIP-28 Channel messages 2024-12-24 15:53:45 -05:00
Vitor Pamplona
73d38f6257 Merge pull request #1233 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-24 11:31:53 -05:00
Crowdin Bot
48d8e418e8 New Crowdin translations by GitHub Action 2024-12-24 16:30:04 +00:00
Vitor Pamplona
ff427d0fde Fixes image upload testing. 2024-12-24 11:27:44 -05:00
Vitor Pamplona
6807554d1d Merge pull request #1232 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-23 20:40:49 -05:00
Crowdin Bot
433a15cf0a New Crowdin translations by GitHub Action 2024-12-24 01:33:04 +00:00
Vitor Pamplona
94c74a1e0c Adds support for encrypted media uploads on NIP-17 DMs 2024-12-23 20:30:22 -05:00
Vitor Pamplona
f839565152 Refactors Ammolite to remove the dependency on OkHttp to prepare for KTor and multiplatform settings.
- This also reduces the Singleton coupling between Client and RelayPool.

To migrate, create a NostrClient instance on your Application class and update your code to access that `client` instance.
2024-12-20 15:36:50 -05:00
Vitor Pamplona
d9c14a78a7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-12-18 20:55:55 -05:00
Vitor Pamplona
a9d87883d2 Integrates with Pokey's Broadcast receiver. 2024-12-18 20:55:45 -05:00
Vitor Pamplona
66e7559ddb Merge pull request #1231 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-18 20:32:34 -05:00
Crowdin Bot
2cd9a7dca6 New Crowdin translations by GitHub Action 2024-12-19 01:10:26 +00:00
Vitor Pamplona
123239443d organize imports 2024-12-18 20:08:27 -05:00
Vitor Pamplona
76ce8f2974 Fixes bug that resets localhost relay settings. 2024-12-18 18:38:26 -05:00
Vitor Pamplona
e002e5c10f Merge pull request #1229 from davotoula/account-backup-qr-code
Account backup qr codes
2024-12-18 18:28:13 -05:00
Vitor Pamplona
d101397166 Merge pull request #1230 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-18 17:58:02 -05:00
Crowdin Bot
2fd96da7c7 New Crowdin translations by GitHub Action 2024-12-18 22:48:35 +00:00
Vitor Pamplona
06bc1922cb Fixes tests 2024-12-18 17:27:20 -05:00
David Kaspar
9f1b5a3c1a added content description to QR code icons 2024-12-18 23:23:57 +01:00
David Kaspar
4f4da12aa7 refactor QrCodeButton methods to avoid duplications 2024-12-18 23:06:37 +01:00
Vitor Pamplona
17495631f7 Simplifies a bit of the game controller icon 2024-12-18 17:01:29 -05:00
Vitor Pamplona
0041d87645 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-12-18 16:53:11 -05:00
Vitor Pamplona
579f63935a - Adds support for multiple media uploads at the same time.
- Adds support to display PictureEvents with multiple images at the same time
- Improves Uploading feedback for the NewPost screen
- 10x better performance on Blurhash generation
- Fixes cosine caching on Blurhash
- Removes troublesome dependency on blurhash encoder liberary
- Restructures contentScale for Images and Video dialogs
- Refactors Media Uploaders to improve code reuse
- Refactors iMeta usage on Quartz to move away from NIP-94
2024-12-18 16:52:50 -05:00
David Kaspar
b273e6dd4f rearrange code for easier diff 2024-12-18 22:52:01 +01:00
David Kaspar
eec8212070 added keyguard to qr code encrypted key export 2024-12-18 20:54:14 +01:00
David Kaspar
af5f136e6c added keyguard to qr code key export 2024-12-18 20:18:23 +01:00
David Kaspar
12d646e8a9 Move QR code next to button 2024-12-18 15:08:06 +01:00
David Kaspar
bd26fcfafd Clean up layout 2024-12-18 14:54:09 +01:00
David Kaspar
ca05b768df Added QR code private key export dialog 2024-12-18 14:43:23 +01:00
Vitor Pamplona
b75024e1fb Merge pull request #1227 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-16 10:10:44 -05:00
Crowdin Bot
816c598971 New Crowdin translations by GitHub Action 2024-12-16 15:08:47 +00:00
Vitor Pamplona
5636748cf8 Merge pull request #1223 from davotoula/add-custom-hashtag-icon-gamestr
Added Gamestr custom hashtag icon
2024-12-16 10:07:25 -05:00
Vitor Pamplona
3defc4d45b Merge pull request #1226 from greenart7c3/main
Fix download and loading icon not showing when text overflow
2024-12-16 10:06:17 -05:00
greenart7c3
ee16324673 Fix download and loading icon not showing when text overflow 2024-12-16 10:40:18 -03:00
David Kaspar
dfabd7afdd Added Gamestr custom hashtag icon 2024-12-15 21:52:30 +01:00
Vitor Pamplona
728768632c Update Compose Bom, firebase, navigation compose and android camera libs 2024-12-12 14:16:58 -05:00
Vitor Pamplona
c2243c53cf Optimizes user search to account for names that start with the typed prefix 2024-12-10 19:07:37 -05:00
Vitor Pamplona
5aed1cee2c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-12-10 18:46:08 -05:00
Vitor Pamplona
8809593bbc - adds an account switcher to the notification click
- removes the use of nostr: for notifications
- enables a new screen to be routed when new logins happen
2024-12-10 18:45:59 -05:00
Vitor Pamplona
83e942a7be Fixes lack of notification when a new account is logged into the app and before the app restarts. 2024-12-10 18:39:45 -05:00
Vitor Pamplona
1ee6316e92 Merge pull request #1221 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-10 15:28:07 -05:00
Crowdin Bot
357ca9b6eb New Crowdin translations by GitHub Action 2024-12-10 20:16:01 +00:00
Vitor Pamplona
b0f5e77077 Merge pull request #1220 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-10 15:14:39 -05:00
Crowdin Bot
1b7b8fa268 New Crowdin translations by GitHub Action 2024-12-10 19:22:17 +00:00
Vitor Pamplona
82d8411ac6 Merge pull request #1219 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-10 14:21:00 -05:00
Crowdin Bot
684b78d2df New Crowdin translations by GitHub Action 2024-12-10 19:19:46 +00:00
Vitor Pamplona
8b80ac13db Fixes displaying an old result when coming back to a DVM screen 2024-12-10 14:17:50 -05:00
Vitor Pamplona
83235dab69 Removes contract of the old image picker 2024-12-10 14:17:15 -05:00
Vitor Pamplona
b01f46b2ee Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-12-09 14:59:44 -05:00
Vitor Pamplona
6a787f2774 Moves relayToAdd from the Object to a param in the relay screen 2024-12-09 14:59:35 -05:00
Vitor Pamplona
53206cc0b2 Merge pull request #1218 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-09 14:43:09 -05:00
Crowdin Bot
7cb6854489 New Crowdin translations by GitHub Action 2024-12-09 19:32:52 +00:00
Vitor Pamplona
95966e5874 Merge pull request #1214 from greenart7c3/main
Move relay dialog to a route
2024-12-09 14:31:24 -05:00
Vitor Pamplona
723979f185 Merge pull request #1216 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-07 18:10:42 -05:00
Crowdin Bot
b989bc0236 New Crowdin translations by GitHub Action 2024-12-07 23:01:56 +00:00
Vitor Pamplona
54ab8f1448 Merge pull request #1215 from davotoula/improve-accessibility-add-content-descriptions
Improve accessibility add content descriptions
2024-12-07 18:00:18 -05:00
David Kaspar
1b0ff52c61 translations 2024-12-07 21:29:34 +01:00
David Kaspar
46b70b719a re-add key to LaunchedEffect 2024-12-07 21:21:47 +01:00
David Kaspar
1972cdeafb Add content description to custom emojis 2024-12-07 21:06:21 +01:00
David Kaspar
39a7d98651 Add content description to icon in app definition 2024-12-07 16:56:24 +01:00
greenart7c3
a93910f8cb Move relay dialog to a route 2024-12-07 11:17:38 -03:00
David Kaspar
314cbe1a32 Add content description to recommended apps icons - refactored 2024-12-07 15:14:46 +01:00
David Kaspar
54d0ec806d Add content description to recommended apps icons 2024-12-07 15:12:27 +01:00
David Kaspar
6faab55a55 Add content description to pushpin icon (just in case) 2024-12-07 14:38:58 +01:00
David Kaspar
8b5366f9e2 Add description to relay list selector in NewMediaView 2024-12-07 14:11:09 +01:00
Vitor Pamplona
15b527c58d Fixes blurhash rendering 2024-12-06 18:13:40 -05:00
Vitor Pamplona
e501101062 Merge pull request #1213 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-06 15:57:41 -05:00
Crowdin Bot
15411edfa6 New Crowdin translations by GitHub Action 2024-12-06 20:07:57 +00:00
Vitor Pamplona
e65346d268 Move gallery previews to it's own package. Avoid reusing VideoView 2024-12-06 15:05:59 -05:00
Vitor Pamplona
01b880c4a5 Simplifies the Gallery stack 2024-12-06 13:41:03 -05:00
Vitor Pamplona
0d6760360f Fixes the bugs from migrating video events to imeta tags 2024-12-06 10:19:55 -05:00
Vitor Pamplona
4610519bb3 - Removes pull to refresh from gallery 2024-12-06 10:17:37 -05:00
Vitor Pamplona
905655461d Adds kind picture and video events to the user profile gallery subscription. 2024-12-06 10:16:54 -05:00
Vitor Pamplona
f759acf66e Merge pull request #1210 from believethehype/olas-profile-with-legacy
Extend NIP93 Profile Gallery with Nip68 Events
2024-12-06 08:22:22 -05:00
Vitor Pamplona
8979706a81 Merge pull request #1212 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-06 08:21:02 -05:00
Crowdin Bot
0db664fabc New Crowdin translations by GitHub Action 2024-12-06 13:19:06 +00:00
Vitor Pamplona
c33fee59a2 Delete unused constant 2024-12-05 18:03:40 -05:00
Vitor Pamplona
eb36489094 Adds basic support for RelationshipStatus to Quartz 2024-12-05 17:55:59 -05:00
Vitor Pamplona
51ce509928 Simple refactoring to newer versions of the clickable text 2024-12-05 17:55:36 -05:00
Vitor Pamplona
0dd306b7fa Separates event class that manages general lists and abstracts another intermediary class that manages private tag arrays in its content. 2024-12-05 17:55:10 -05:00
Vitor Pamplona
8a6f830e29 Removes invalid import 2024-12-05 17:26:47 -05:00
Vitor Pamplona
bcb9ba26ab Improves search by npubs to use all relays. 2024-12-05 17:14:16 -05:00
Vitor Pamplona
3aa15889c1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-12-05 17:12:50 -05:00
Vitor Pamplona
b47d9ad4d9 - Migrates Video events to imeta tags
- Removes youtu.be links from the video feed.
- Checks for video file types in uppercase as well as lowercase
2024-12-05 16:41:43 -05:00
David Kaspar
b504a2c0d4 Merge pull request #1211 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-05 17:33:30 +01:00
Crowdin Bot
69c9e84dc2 New Crowdin translations by GitHub Action 2024-12-05 15:54:05 +00:00
Vitor Pamplona
81591d6f76 v0.93.1 2024-12-05 10:48:52 -05:00
Believethehype
d2187995b8 Update strings.xml 2024-12-04 18:06:33 +01:00
Believethehype
a3257a365b hybrid solution nip68 / nip93
it keeps the nip93 gallery as before, but also adds all nip68 media to the gallery
2024-12-04 17:56:06 +01:00
Vitor Pamplona
9180eeb59f Merge pull request #1209 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-03 16:10:21 -05:00
Crowdin Bot
b24983b232 New Crowdin translations by GitHub Action 2024-12-03 20:56:03 +00:00
Vitor Pamplona
fe3246c94e Fixes video rotation screen in full size while playing 2024-12-03 15:53:17 -05:00
Vitor Pamplona
b9883093ac Improving performance of the Hex encoder. 2024-12-03 14:07:13 -05:00
Vitor Pamplona
869debaf9d Fixes Satellite's blossom upload 2024-12-02 20:05:13 -05:00
Vitor Pamplona
6a522f5c92 updates AGP 2024-12-02 18:03:47 -05:00
Vitor Pamplona
ae13725db5 Improves the Discovery block UI 2024-12-02 17:45:44 -05:00
Vitor Pamplona
95774ddca5 Merge pull request #1207 from believethehype/main
Content DVMS: show amount and personalized status in feed
2024-12-02 16:04:39 -05:00
Believethehype
56450cf6c5 Merge branch 'main' of https://github.com/believethehype/amethyst 2024-12-02 22:02:49 +01:00
Believethehype
2da1dda019 Merge remote-tracking branch 'upstream/main' 2024-12-02 22:00:24 +01:00
dbth
fb73f6153f Merge branch 'main' into main 2024-12-02 22:00:16 +01:00
Believethehype
62807336fd DVM: show amount and personalized in feed 2024-12-02 21:56:13 +01:00
Vitor Pamplona
fddb9b16ab updates jackson and secp256k1 2024-12-02 15:32:19 -05:00
Vitor Pamplona
0dcce5a987 Fixes lack of reactions on video feeds 2024-12-02 15:31:59 -05:00
Vitor Pamplona
16cf3c850a Merge pull request #1205 from believethehype/main
DVM: adapt nip89 naming, show picture then image (legacy)
2024-12-02 14:09:23 -05:00
Vitor Pamplona
2b17e9303a Merge pull request #1206 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-12-02 14:05:51 -05:00
Crowdin Bot
61d9145310 New Crowdin translations by GitHub Action 2024-12-02 18:59:15 +00:00
Vitor Pamplona
40921d4b03 fixes badge crash 2024-12-02 13:56:38 -05:00
Believethehype
e0e898d4b1 DVM: adapt nip89 naming, show picture then image (legacy) 2024-12-02 19:55:02 +01:00
Vitor Pamplona
bd2005ec6e Fixes screen mispositioning when rotating full screen video/image dialogs. 2024-11-29 16:40:07 -05:00
Vitor Pamplona
2c825d5380 Remove unecessary log 2024-11-29 16:32:04 -05:00
Vitor Pamplona
7fe522307a Fixes the bug on not having the video feed at the top when loading the app from scratch. 2024-11-29 15:12:54 -05:00
Vitor Pamplona
9660c739f3 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-11-29 14:50:39 -05:00
Vitor Pamplona
9025741c13 deletes unecessary logs 2024-11-29 14:50:28 -05:00
Vitor Pamplona
0ef851e4bb Merge pull request #1201 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-29 14:41:13 -05:00
Crowdin Bot
7204d337e8 New Crowdin translations by GitHub Action 2024-11-29 19:39:34 +00:00
Vitor Pamplona
cb538bc6a3 Fixes missing reply order when the direct reply is also included as a quote. 2024-11-29 14:31:31 -05:00
Vitor Pamplona
d0b0f441b1 Fixes image testing. 2024-11-27 15:42:31 -05:00
Vitor Pamplona
ddd7f19f38 Merge pull request #1198 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-27 12:09:28 -05:00
Crowdin Bot
e46c8f4c15 New Crowdin translations by GitHub Action 2024-11-27 17:02:57 +00:00
Vitor Pamplona
6b390dad30 Merge pull request #1197 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-27 12:01:24 -05:00
Crowdin Bot
50e067fbf0 New Crowdin translations by GitHub Action 2024-11-27 16:43:19 +00:00
Vitor Pamplona
04da076d95 Adds NIP-22 as replies to Interactive Stories. 2024-11-27 11:41:13 -05:00
David Kaspar
fd88dfed7d Merge pull request #1196 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-27 17:04:41 +01:00
Crowdin Bot
22905bc04e New Crowdin translations by GitHub Action 2024-11-27 15:40:29 +00:00
Vitor Pamplona
c02ced7855 Merge pull request #1195 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-27 10:39:14 -05:00
Crowdin Bot
ea1145c890 New Crowdin translations by GitHub Action 2024-11-27 15:36:35 +00:00
Vitor Pamplona
043c8eff57 Merge pull request #1194 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-27 10:35:14 -05:00
Vitor Pamplona
fbb036da4a v0.93.0 2024-11-27 10:33:59 -05:00
Crowdin Bot
71d30f83ea New Crowdin translations by GitHub Action 2024-11-27 15:26:37 +00:00
Vitor Pamplona
9a0c998654 Fixes "null" strings on profile fields. 2024-11-27 10:24:09 -05:00
Vitor Pamplona
88082b1ba3 Updates media and coil 2024-11-27 09:19:17 -05:00
Vitor Pamplona
ad2b2e6d02 Merge pull request #1193 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-27 09:16:25 -05:00
Crowdin Bot
5044a687fa New Crowdin translations by GitHub Action 2024-11-27 14:02:42 +00:00
Vitor Pamplona
298d4bb237 removes unnecessary metadata 2024-11-27 08:59:23 -05:00
Vitor Pamplona
9c180cb87a Merge pull request #1192 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-26 19:10:54 -05:00
Crowdin Bot
2e3f2044d5 New Crowdin translations by GitHub Action 2024-11-27 00:09:32 +00:00
Vitor Pamplona
bdf012f641 Adds support for Interactive Stories 2024-11-26 19:07:12 -05:00
Vitor Pamplona
034ee46543 Refactors additional factories 2024-11-26 09:28:42 -05:00
Vitor Pamplona
6a17f94cb1 Merge pull request #1190 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-25 14:31:56 -05:00
Crowdin Bot
233059d544 New Crowdin translations by GitHub Action 2024-11-25 19:31:18 +00:00
Vitor Pamplona
7bd9b6a1ec Moves to the new Android Image/Video Picker 2024-11-25 14:24:17 -05:00
Vitor Pamplona
04e77daaab Light chat bubbles 2024-11-22 19:17:50 -05:00
Vitor Pamplona
308048c352 Adds date separators on chats 2024-11-22 18:08:30 -05:00
Vitor Pamplona
3f1173a42a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-11-22 17:01:57 -05:00
Vitor Pamplona
39726142ae Fixes account creation that follows itself. 2024-11-22 17:01:50 -05:00
Vitor Pamplona
025cbeed07 Merge pull request #1189 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-22 16:33:33 -05:00
Crowdin Bot
8a1550cbd9 New Crowdin translations by GitHub Action 2024-11-22 21:20:34 +00:00
Vitor Pamplona
49148ddc76 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-11-22 16:18:01 -05:00
Vitor Pamplona
d0e870e1b8 Adds Custom Empty Feed Screen for notifications 2024-11-22 16:17:55 -05:00
Vitor Pamplona
dbb2bfa512 Merge pull request #1188 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-22 12:26:35 -05:00
Crowdin Bot
d6b66c65fa New Crowdin translations by GitHub Action 2024-11-22 16:36:21 +00:00
Vitor Pamplona
9a3d365243 Fixes: https://github.com/vitorpamplona/amethyst/issues/1186 2024-11-22 11:33:51 -05:00
Vitor Pamplona
3b30d5493d Merge pull request #1184 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-21 18:58:17 -05:00
Crowdin Bot
7bfd1a9107 New Crowdin translations by GitHub Action 2024-11-21 23:53:47 +00:00
Vitor Pamplona
2c9e2de524 Adds support for Blossom media servers. 2024-11-21 18:51:43 -05:00
Vitor Pamplona
c89c5eb4b0 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-11-21 15:46:16 -05:00
Vitor Pamplona
5c5eb53395 Fixes translations of http urls 2024-11-21 15:46:07 -05:00
Vitor Pamplona
55f0092e03 Merge pull request #1185 from davotoula/enable-unfollow-in-note-dropdown
Enable unfollow in note dropdown
2024-11-21 15:32:44 -05:00
Vitor Pamplona
54d9ac5730 no need for identifier anymore 2024-11-21 15:15:03 -05:00
David Kaspar
d9a9137d89 cleaner code 2024-11-21 21:02:31 +01:00
David Kaspar
7beff8c835 Add unfollow to NoteDropDownMenu 2024-11-21 20:58:40 +01:00
Vitor Pamplona
4a80a16473 Adds support for pronouns when the user makes them available. 2024-11-21 11:19:13 -05:00
Vitor Pamplona
b3e3b70dd0 Marks which events can receive Comment replies as root scopes. 2024-11-19 18:01:56 -05:00
Vitor Pamplona
03662b9d1b Migrates Video feed new posts from NIP-94 to Video and Picture events. 2024-11-19 17:54:51 -05:00
Vitor Pamplona
d33421fce4 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-11-19 15:37:11 -05:00
Vitor Pamplona
4db6f5531c Adds support for Instagram feeds. https://github.com/nostr-protocol/nips/pull/1551 2024-11-19 15:36:30 -05:00
Vitor Pamplona
72ea4a44a2 Merge pull request #1183 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-19 09:31:20 -05:00
Crowdin Bot
022a9f6f6c New Crowdin translations by GitHub Action 2024-11-19 14:25:24 +00:00
Vitor Pamplona
248848799c Fully Deprecates note1, removing the last usages of the standard 2024-11-19 09:22:37 -05:00
Vitor Pamplona
72f317717b Remove the unused encryption field from NIP-94 2024-11-18 17:29:59 -05:00
Vitor Pamplona
4651074774 Fixes bug on searching for geohashes on addresses 2024-11-18 16:48:21 -05:00
Vitor Pamplona
fb058f93b6 Makes NIP-22 posts only visible to the main feed if inside the scope they were placed. 2024-11-18 16:47:47 -05:00
Vitor Pamplona
9e34e9d720 Fixes issue with the order of same-author events in a thread 2024-11-18 16:00:48 -05:00
Vitor Pamplona
04d6da28af Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-11-18 15:39:19 -05:00
Vitor Pamplona
192a43dc21 Updates compose, coil, firebase and navigation 2024-11-18 15:39:12 -05:00
Vitor Pamplona
61510ae7e3 Adds support for NIP-22 exclusive geo posts 2024-11-18 15:38:57 -05:00
Vitor Pamplona
488286ce20 Merge pull request #1181 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-18 08:25:31 -05:00
Crowdin Bot
fab6c65e98 New Crowdin translations by GitHub Action 2024-11-18 13:22:15 +00:00
Vitor Pamplona
accde8622c Merge pull request #1179 from davotoula/1172-hide-drafts-from-other-accounts-on-device
Hide drafts from other accounts on device
2024-11-18 08:20:55 -05:00
David Kaspar
4a590d67a6 move filtering of "not your drafts" to ThreadFeedFilter 2024-11-17 10:48:03 +01:00
David Kaspar
6c3200a9cc cleaner code, change cascading ifs to when 2024-11-16 10:34:01 +01:00
David Kaspar
f16143bec3 cleaner code, fewer warnings 2024-11-16 09:29:45 +01:00
David Kaspar
8216476916 cleaner code, fewer warnings 2024-11-16 08:48:30 +01:00
David Kaspar
ffd43e448b filter out drafts that are not same author as current account 2024-11-15 12:54:59 +01:00
Vitor Pamplona
3a566dad6b Improves (Show More) presence to only when it actually makes a difference. 2024-11-12 13:36:59 -05:00
Vitor Pamplona
bf861d6bc5 - Improves thread preloading
- Fixes jumping of scroll when the thread updates
2024-11-12 12:54:41 -05:00
Vitor Pamplona
b31ace79c3 App hangs when switching to an account that was logged into with the npub just because it waits to decrypt the hidden user list. 2024-11-11 18:29:38 -05:00
Vitor Pamplona
37a92c25f0 - Unifies Location Flows and GeoHash Flows into one
- Make them react to changing location permissions
- Adds UI for when the location permission is rejected.
2024-11-11 17:51:42 -05:00
Vitor Pamplona
062e4af118 Updates JNA, Coil, Jackson, and Kotlin Collections 2024-11-11 14:54:38 -05:00
Vitor Pamplona
c083295f6a Fixes error message for the signup page when the display name is blank 2024-11-11 14:53:39 -05:00
Vitor Pamplona
924baf0de3 Merge pull request #1177 from greenart7c3/main
Fix initial decryption of mute lists when using amber
2024-11-11 09:48:46 -05:00
greenart7c3
6015c968f5 Fix initial decryption of mutelists when using amber 2024-11-11 11:33:14 -03:00
Vitor Pamplona
3e85991f19 Merge pull request #1176 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-11 09:04:33 -05:00
Crowdin Bot
893b23e4fb New Crowdin translations by GitHub Action 2024-11-11 13:54:10 +00:00
Vitor Pamplona
cb5d69e358 Merge pull request #1171 from davotoula/crashfix-test-for-empty-string-in-mentions
Crashfix: test for empty string in mentions
2024-11-11 08:52:39 -05:00
Vitor Pamplona
43688c4117 Merge pull request #1174 from davotoula/1173-add-nip05-to-hidden-words-filtering
Added nip05 field from user profile to be used as part of hidden words filtering
2024-11-11 08:52:28 -05:00
David Kaspar
4e4521ecef Added nip05 field from user profile to be used as part of hidden words filtering 2024-11-10 06:51:34 +00:00
David Kaspar
7bc9205758 Added a test around NewPostViewModel.load with no mentions, valid mentions and invalid empty mentions 2024-11-08 19:15:04 +00:00
David Kaspar
abf6cc00ae test for empty string in mentions 2024-11-07 09:24:30 +00:00
Vitor Pamplona
7146838f63 Better names for github jobs 2024-11-04 11:56:25 -05:00
Vitor Pamplona
1037ab33e3 fixes names of uploaded files 2024-11-04 11:31:04 -05:00
Vitor Pamplona
230fd553ae fixes paths on github actions 2024-11-04 11:12:40 -05:00
Vitor Pamplona
eac078c42d updates build github action to test and upload the right assets 2024-11-04 10:54:14 -05:00
Vitor Pamplona
39c9b75bf4 Moves zapstore to local image and first in the list 2024-11-04 10:15:31 -05:00
Vitor Pamplona
e076a2dbe3 Merge pull request #1167 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-04 10:12:31 -05:00
Crowdin Bot
5705a26836 New Crowdin translations by GitHub Action 2024-11-04 15:12:18 +00:00
Vitor Pamplona
740c279825 Merge pull request #1165 from davotoula/1164-launch-in-scope-to-persist-translation-prefs
Crash fix: Launch in Dispatchers.IO when changing translations prefs in account
2024-11-04 10:10:48 -05:00
Vitor Pamplona
718605e44b Merge pull request #1163 from ilcompratoreconsapevole/banner
Added ZapStore's SVG banner that says "Get it on" linking to its repo
2024-11-04 10:09:49 -05:00
Vitor Pamplona
8df2ff839c Merge pull request #1162 from davotoula/bugfix-images-reported-saved-as-video
Bugfix: images reported saved as video
2024-11-04 10:09:36 -05:00
ilcompratoreconsapevole
00be9fc595 Rewritten connection link to ZapStore's banner image to ensure reliable connection to specific repo 2024-11-03 21:38:01 +01:00
David Kaspar
7d25ab13a9 Launch in Dispatchers.IO when changing translations prefs in account 2024-11-03 16:15:54 +00:00
ilcompratoreconsapevole
d7888096e9 Added ZapStore's SVG banner that says "Get it on" linking to its repository. 2024-11-03 12:50:46 +01:00
David Kaspar
aad5aaa930 rename inner it to avoid shadowed it 2024-11-03 11:48:12 +00:00
David Kaspar
b877e4105a Replace nested ifs with a when 2024-11-03 10:30:07 +00:00
David Kaspar
d1cd15c389 Remove unused val 2024-11-03 10:24:20 +00:00
David Kaspar
69e139e382 Remove unused @OptIn(ExperimentalFoundationApi::class) 2024-11-03 10:23:32 +00:00
David Kaspar
b2628764c9 Remove unused shadowed orientation val 2024-11-03 10:20:30 +00:00
David Kaspar
f7f0d74be1 Correct boolean operation from AND to OR 2024-11-03 10:19:29 +00:00
Vitor Pamplona
b47122d79a Updates accompanist, kotlin serialization and mockk 2024-11-01 18:41:00 -04:00
Vitor Pamplona
635ad30de9 Let it throw in the giftwrap parser to make sure Notifications work 2024-11-01 18:40:34 -04:00
Vitor Pamplona
0884318a55 Merge pull request #1161 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-01 18:27:38 -04:00
Crowdin Bot
d8c89317d2 New Crowdin translations by GitHub Action 2024-11-01 22:20:09 +00:00
Vitor Pamplona
772987dffd coroutines test to 1.9 2024-11-01 18:17:32 -04:00
Vitor Pamplona
61c51ee9a8 Updates AGP 2024-11-01 18:17:07 -04:00
Vitor Pamplona
f157f8eb9b - Fixes lack of zap updates when there is a private zap the user cannot decrypt or when a nwc payment fails.
- Moves the parallel processing to a utils class
2024-11-01 18:16:57 -04:00
Vitor Pamplona
d96f5953d5 Moving animation size to a constant. 2024-11-01 17:27:19 -04:00
Vitor Pamplona
ffb5cce509 Merge pull request #1160 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-11-01 10:35:40 -04:00
Crowdin Bot
b441292413 New Crowdin translations by GitHub Action 2024-11-01 14:16:09 +00:00
Vitor Pamplona
91748d5c39 - Adds a cache for reply levels when viewing threads.
- Moves the lazy list state to the view model to saving the scrolling position when navigating between multiple threads.
2024-11-01 10:10:05 -04:00
Vitor Pamplona
a825f575de Fixes rendering of notes when the modifier changes due to new posts found and the reply level gets recalculated. 2024-11-01 10:06:09 -04:00
Vitor Pamplona
0581a3e836 Fixes reply level calculation caching. 2024-11-01 10:05:25 -04:00
Vitor Pamplona
82f0ed8a0d Upgrades to Coil 3 2024-10-31 16:34:22 -04:00
Vitor Pamplona
116bc108c3 Merge pull request #1158 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-31 14:07:13 -04:00
Crowdin Bot
44868ac4e6 New Crowdin translations by GitHub Action 2024-10-31 18:05:03 +00:00
Vitor Pamplona
2a24ca0787 Adds support for q tags on addresses 2024-10-31 14:02:39 -04:00
Vitor Pamplona
03225fc696 Merge pull request #1157 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-30 18:00:57 -04:00
Crowdin Bot
a0ab4a824b New Crowdin translations by GitHub Action 2024-10-30 21:59:32 +00:00
Vitor Pamplona
0ba4ca13bf Updates compose, corektx, fragment, kotlin, runtime, and android camera libs 2024-10-30 17:56:28 -04:00
Vitor Pamplona
060a242a0f Removes logs 2024-10-30 17:55:35 -04:00
Vitor Pamplona
ae530ed813 adds extra padding for the zap setup screen 2024-10-30 16:02:00 -04:00
Vitor Pamplona
a5c4a53afe - Adds an Around ME feed
- Refactors location to operate as a flow
- Refactors FeedStructures to prepare for custom feeds
- Moves Account to operate feeds with location
2024-10-30 16:01:39 -04:00
Vitor Pamplona
fb6137f99a Missing string 2024-10-29 12:31:07 -04:00
Vitor Pamplona
de99c50639 Adds search.nos.today to bootstrapping relays for search 2024-10-29 12:23:09 -04:00
Vitor Pamplona
d5f8994128 Merge pull request #1156 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-29 12:16:33 -04:00
Crowdin Bot
cf02d808d6 New Crowdin translations by GitHub Action 2024-10-29 16:10:36 +00:00
Vitor Pamplona
f6aa732558 Adds DM and Search default buttons to the dialogs. 2024-10-29 12:07:15 -04:00
Vitor Pamplona
d321df79e8 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-10-28 16:11:09 -04:00
Vitor Pamplona
d1b4f81083 Fixes jumpy thread view 2024-10-28 16:10:47 -04:00
Vitor Pamplona
0205f1fd12 Merge pull request #1155 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-28 10:14:42 -04:00
Crowdin Bot
d739fdac2d New Crowdin translations by GitHub Action 2024-10-28 14:11:31 +00:00
Vitor Pamplona
4a43a87a79 Moves templates to explain that bounties matter. 2024-10-28 10:08:53 -04:00
Vitor Pamplona
6f31898cdc Updates Firebase 2024-10-24 17:34:13 -04:00
Vitor Pamplona
6ebc878312 Fixes poll's background rendering with the right percentages. 2024-10-24 17:34:00 -04:00
Vitor Pamplona
518e1c88ce Adds hidden words filter to search, hashtag and geotag feeds
Applies hidden words even to hashtags that were not included in the content of the event.
Pre-process search to avoid showing and hiding posts after hidden words where processed by the UI.
2024-10-24 11:00:07 -04:00
Vitor Pamplona
f88985b2bf Adds an icon to the repo 2024-10-23 18:00:23 -04:00
Vitor Pamplona
89e1e20b40 Improves algorithm for placing the clicked note in a visible position when opening a new threads that need to load dynamically 2024-10-22 17:26:53 -04:00
Vitor Pamplona
c2e725ef5f Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-10-22 17:14:36 -04:00
Vitor Pamplona
6e8c348cbc Moves NIP-44 test model classes to a new file 2024-10-22 17:14:29 -04:00
Vitor Pamplona
84316d320d Merge pull request #1150 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-22 15:26:36 -04:00
Crowdin Bot
87fb63af06 New Crowdin translations by GitHub Action 2024-10-22 19:00:17 +00:00
Vitor Pamplona
a3b0436d91 Uses NIP-78 to start syncing some data between instances of Amethyst 2024-10-22 14:52:11 -04:00
Vitor Pamplona
bb430605bc Merge pull request #1148 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-19 18:50:37 -04:00
Crowdin Bot
4c0d92f89f New Crowdin translations by GitHub Action 2024-10-19 18:19:59 +00:00
Vitor Pamplona
f0b14edef6 v0.92.7 2024-10-19 14:17:42 -04:00
Vitor Pamplona
aae2ffebd7 Fixes the bug of unable to decrypt mute lists. 2024-10-19 14:17:17 -04:00
Vitor Pamplona
7b11d0ce81 v0.92.6 2024-10-17 17:21:20 -04:00
Vitor Pamplona
26ba75c7c9 Fixes array too big because strfry blocks more than 20 filters 2024-10-17 17:18:35 -04:00
Vitor Pamplona
67fb3bf488 Fixes margin of poll options with translation 2024-10-17 15:22:34 -04:00
Vitor Pamplona
24b29396cb Reduces the font size for the translation label 2024-10-17 15:22:19 -04:00
Vitor Pamplona
25c3532637 Fixes poll rendering 2024-10-17 14:55:07 -04:00
Vitor Pamplona
cff6ee457d Fixes zapraiser progress bar 2024-10-17 14:18:40 -04:00
Vitor Pamplona
c0ba6f5b00 If the Share target intent opens with the new post screen already active, just updates the post instead of creating a new one. 2024-10-17 13:59:52 -04:00
Vitor Pamplona
44d8b8ff33 v0.92.5 2024-10-16 17:34:10 -04:00
Vitor Pamplona
f495acb570 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-10-16 17:33:58 -04:00
Vitor Pamplona
ecfc8bcd87 Merge pull request #1144 from geovnn/main
Prevent clicks outside boost popup
2024-10-16 17:33:39 -04:00
Vitor Pamplona
724bea6bbb Updates compose, benchmark, navigation, and activity 2024-10-16 17:32:33 -04:00
Giovanni Gatti
6826db13a4 Prevent clicks outside boost popup 2024-10-16 23:11:51 +02:00
Vitor Pamplona
c5dd2c5651 Fixes recreating the new post screen from intent when switching accounts. 2024-10-16 13:33:46 -04:00
Vitor Pamplona
236ff2d680 v0.92.4 2024-10-16 11:13:56 -04:00
Vitor Pamplona
13e2546d36 Makes Amethyst a share target for texts, images and videos. 2024-10-16 10:54:57 -04:00
Vitor Pamplona
549b9f53e5 Fixes new media post dialog for the edge to edge borders 2024-10-16 09:41:02 -04:00
Vitor Pamplona
974c022aed Fixes padding of the new new post screen 2024-10-15 17:20:16 -04:00
Vitor Pamplona
a57566dd84 Changes the new post screen to use the non-disappearing version of the scaffold. 2024-10-15 16:55:09 -04:00
Vitor Pamplona
72e86cf391 Merge pull request #1143 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 16:25:21 -04:00
Crowdin Bot
3b23cded56 New Crowdin translations by GitHub Action 2024-10-15 20:04:18 +00:00
Vitor Pamplona
121337eb83 Makes new post screen come in and out from the bottom 2024-10-15 16:02:20 -04:00
Vitor Pamplona
fe87d642c9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NewPostScreen.kt
2024-10-15 15:54:26 -04:00
Vitor Pamplona
b8112ff1aa Correctly maps the write status of the outbox relays. 2024-10-15 15:53:00 -04:00
Vitor Pamplona
1eee0bd1ae Merge pull request #1142 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 13:55:09 -04:00
Crowdin Bot
f48335e3be New Crowdin translations by GitHub Action 2024-10-15 17:46:27 +00:00
Vitor Pamplona
fcc4befb22 Merge pull request #1141 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 13:44:54 -04:00
Crowdin Bot
e28a1896ec New Crowdin translations by GitHub Action 2024-10-15 17:38:52 +00:00
Vitor Pamplona
f10e66e6a5 Merge pull request #1137 from geovnn/main
Change NewPostView to NewPostScreen to make it route based
2024-10-15 13:37:33 -04:00
Vitor Pamplona
825a227e2d Merge pull request #1140 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 12:09:51 -04:00
Crowdin Bot
3446fdaf7f New Crowdin translations by GitHub Action 2024-10-15 16:08:45 +00:00
Vitor Pamplona
9c29bf3f25 v0.92.3 2024-10-15 12:06:36 -04:00
Vitor Pamplona
9642e45775 Merge pull request #1139 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 12:06:23 -04:00
Crowdin Bot
58788d2cd5 New Crowdin translations by GitHub Action 2024-10-15 16:00:54 +00:00
Vitor Pamplona
d7c18341cd Uses the same Reaction animation to also do Boosts 2024-10-15 11:58:40 -04:00
Vitor Pamplona
ee16f6c143 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-10-15 11:28:41 -04:00
Vitor Pamplona
d78b55d134 Reverts hack for fling behavior on the Vertical Pager 2024-10-15 11:28:33 -04:00
Vitor Pamplona
ff5e7359e2 Merge pull request #1138 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-15 11:05:39 -04:00
Crowdin Bot
4fd0f38402 New Crowdin translations by GitHub Action 2024-10-15 14:56:33 +00:00
Vitor Pamplona
5b516f4101 Updates to AGP 8.7.1 and SDK 35 2024-10-15 10:53:21 -04:00
Vitor Pamplona
1fc390c215 Do not bring the keyboard when the search field reappears. 2024-10-15 10:52:54 -04:00
Vitor Pamplona
2bb09f583b Fixes Thread view when loading a reply of a thread that is not cached yet. 2024-10-15 10:45:03 -04:00
Giovanni Gatti
89de272ea1 Change NewPostView to NewPostScreen to make it route based 2024-10-14 23:19:43 +02:00
Vitor Pamplona
b4eba2da02 0.92.2 2024-10-11 19:35:52 -04:00
Vitor Pamplona
786a173ba5 Merge pull request #1133 from greenart7c3/external_signer_result
Check for the result field when using external signer
2024-10-11 08:08:30 -04:00
greenart7c3
5ef3c8f00e Check for the result field when using external signer 2024-10-11 07:22:34 -03:00
Vitor Pamplona
d832351bcd Fixes the ability to see muted lists in Shorts 2024-10-10 17:52:24 -04:00
Vitor Pamplona
9ae611831f Enables new fields for vision prescriptions 2024-10-10 16:48:33 -04:00
Vitor Pamplona
6db287a008 Merge pull request #1131 from davotoula/prioritise-name-when-searching
Prioritise search results with user name starting with search term
2024-10-10 09:47:28 -04:00
David Kaspar
99641e1a12 Added comparison between search term and display name to comparator to prioritise results with search term in name 2024-10-10 14:11:13 +02:00
Vitor Pamplona
abb2f0557d Merge pull request #1118 from davotoula/refactor-media-compressor
Refactor media compressor as prep for replacing 3rd party compression library
2024-10-09 16:38:29 -04:00
David Kaspar
76a66e7bd3 added test for when exception happens during compression 2024-10-09 21:46:15 +02:00
David Kaspar
6cd50a115b Merge branch 'vitorpamplona:main' into refactor-media-compressor 2024-10-09 14:16:14 +01:00
Vitor Pamplona
0ed34cec09 Fixes https://github.com/vitorpamplona/amethyst/issues/1130 2024-10-09 08:42:33 -04:00
David Kaspar
c5e3657bfe added missing dot for file extension 2024-10-09 01:45:50 +02:00
David Kaspar
da47965d76 Merge branch 'main' into refactor-media-compressor 2024-10-09 00:16:35 +01:00
David Kaspar
a66417ec82 Added Unknown media type test 2024-10-09 01:07:28 +02:00
David Kaspar
62c3c93d2e Removed splitFileName function as both file name and extension are known separately 2024-10-09 01:02:41 +02:00
David Kaspar
fbecc8b6d1 Use "use" blocks to close resources automatically 2024-10-09 00:52:35 +02:00
Vitor Pamplona
e95afa89ff Merge pull request #1129 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-08 18:38:36 -04:00
Crowdin Bot
d6f2560c1c New Crowdin translations by GitHub Action 2024-10-08 22:33:54 +00:00
Vitor Pamplona
d320b951ae Fixes long term issue with the video in the Shorts feed not aligning with the padding of the screen 2024-10-08 18:31:54 -04:00
David Kaspar
af28e6d466 Use copyTo to copy streams 2024-10-09 00:15:19 +02:00
Vitor Pamplona
86e6d56dcd Removes old logs used for debuging the previous issue 2024-10-08 17:19:35 -04:00
Vitor Pamplona
c66fa1073b Fixes intent reopening the Zap Setup screen because it was saved in the backstack of the Home screen. 2024-10-08 16:09:09 -04:00
Vitor Pamplona
6d9bffe9e7 Disabling #1122 until we solve those little issues. 2024-10-08 15:00:25 -04:00
Vitor Pamplona
04158cec6e Merge pull request #1128 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-08 14:43:07 -04:00
Crowdin Bot
76376a7c40 New Crowdin translations by GitHub Action 2024-10-08 18:32:53 +00:00
Vitor Pamplona
67ea0dcf7f Faster logout processing without closing the account switcher dialog. 2024-10-08 13:59:51 -04:00
David Kaspar
cdc8850894 Refactor out file operations into seperate helper
Additional tests for MediaCompressor
2024-10-08 17:47:27 +02:00
David Kaspar
ce0c7c842d Merge branch 'main' into refactor-media-compressor 2024-10-08 14:51:53 +01:00
Vitor Pamplona
f51f929079 Merging 2024-10-08 09:24:10 -04:00
Vitor Pamplona
d87d2e0ef8 Merge pull request #1127 from geovnn/main
Added animations
2024-10-08 09:22:40 -04:00
Giovanni Gatti
a3166f4ff3 Add animation to notification chart 2024-10-08 12:13:47 +02:00
Giovanni Gatti
9340c440f5 Add animation to FABs 2024-10-08 11:54:42 +02:00
Giovanni Gatti
a7343d5b54 Add animation to zap and reaction popups 2024-10-08 11:06:29 +02:00
Vitor Pamplona
0551b82bd9 Updates AGP and compose, fragment, navigation, benchmarking and firebase libraries. 2024-10-07 17:02:09 -04:00
Vitor Pamplona
18fca8adac Merge pull request #1122 from KotlinGeekDev/conditional-rotation
Re-enable tap to fullscreen behaviour with stricter orientation checks.
2024-10-07 12:57:28 -04:00
Vitor Pamplona
5d788ed669 Merge pull request #1125 from geovnn/main
Prevent clicks outside reaction and zap popups
2024-10-07 12:56:57 -04:00
Giovanni Gatti
54d52e0b77 Prevent clicks outside reaction and zap popups 2024-10-07 17:38:05 +02:00
Vitor Pamplona
28c92675b2 Merge pull request #1124 from greenart7c3/main
Support for login with hex key when using amber
2024-10-07 08:32:59 -04:00
greenart7c3
baa9ee0562 Support for login with hex key when using amber 2024-10-07 08:50:06 -03:00
KotlinGeekDev
7326d6be93 Rotation:actually rotate if the device orientation is not locked. 2024-10-05 17:06:36 +01:00
David Kaspar
e187e5392f refactor "from" method 2024-10-03 14:56:03 +02:00
David Kaspar
7c8f6eb239 formatting 2024-10-03 14:56:03 +02:00
David Kaspar
6294c65ad6 Create private functions for image vs video compression and when statement to select on content type 2024-10-03 14:56:03 +02:00
David Kaspar
51d9657371 Use Pair instead of Array to split up file name 2024-10-03 14:56:03 +02:00
David Kaspar
32783794ee Exit early if UNCOMPRESSED is selected 2024-10-03 14:56:03 +02:00
David Kaspar
e3858dc830 added kotlinx.coroutines.test library
added MediaCompressor first tests
2024-10-03 14:56:03 +02:00
Vitor Pamplona
38a9030963 Merge pull request #1117 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-10-03 08:28:43 -04:00
Crowdin Bot
29c0c4cf5f New Crowdin translations by GitHub Action 2024-10-03 12:23:07 +00:00
Vitor Pamplona
dcbde81a23 Merge pull request #1116 from davotoula/update-tests-for-new-forceProxy-parameter
Update tests for new forceProxy param
2024-10-03 08:21:47 -04:00
David Kaspar
56e6f06fe7 add "forceProxy = false" to tests 2024-10-03 10:02:31 +02:00
Vitor Pamplona
342b64f0eb Merge pull request #1114 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-30 19:18:57 -04:00
Crowdin Bot
a64a640165 New Crowdin translations by GitHub Action 2024-09-30 23:05:25 +00:00
Vitor Pamplona
6433830c32 Fixes language setting toggles 2024-09-30 18:47:45 -04:00
Vitor Pamplona
3a58819b52 Merge pull request #1112 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-28 11:54:58 -04:00
Crowdin Bot
5c7be44369 New Crowdin translations by GitHub Action 2024-09-28 15:43:33 +00:00
Vitor Pamplona
6e66d008dc Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-28 11:41:30 -04:00
Vitor Pamplona
c61c4ce16c Improves names and icons of the left drawer. 2024-09-28 11:01:56 -04:00
Vitor Pamplona
246ce55253 Merge pull request #1111 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-28 09:49:21 -04:00
Crowdin Bot
a704c81d24 New Crowdin translations by GitHub Action 2024-09-28 13:43:32 +00:00
Vitor Pamplona
9963245ec9 Improves zap store descriptions 2024-09-28 09:40:02 -04:00
Vitor Pamplona
64016aa940 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-27 18:02:33 -04:00
Vitor Pamplona
c6462f102a Merge pull request #1110 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-27 17:23:41 -04:00
Vitor Pamplona
b05083863d removes the aab files 2024-09-27 17:13:44 -04:00
Crowdin Bot
86fffe6539 New Crowdin translations by GitHub Action 2024-09-27 21:13:01 +00:00
Vitor Pamplona
f94a4cfa65 Merge pull request #1109 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-27 17:11:24 -04:00
Crowdin Bot
3825642ae7 New Crowdin translations by GitHub Action 2024-09-27 19:40:05 +00:00
Vitor Pamplona
f90f81b2a5 Merge pull request #1108 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-27 15:38:46 -04:00
Crowdin Bot
bc042e52b0 New Crowdin translations by GitHub Action 2024-09-27 19:28:52 +00:00
Vitor Pamplona
d80b22fa29 Merge pull request #1107 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-27 15:27:38 -04:00
Crowdin Bot
b63ed4fdbd New Crowdin translations by GitHub Action 2024-09-27 19:24:11 +00:00
Vitor Pamplona
397acce85b Merge pull request #1106 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-27 15:23:01 -04:00
Crowdin Bot
5305929df3 New Crowdin translations by GitHub Action 2024-09-27 19:22:04 +00:00
Vitor Pamplona
6d4ba6d507 v0.92.1 2024-09-27 15:20:06 -04:00
Vitor Pamplona
a028c2c3fc Fixes missing zaps on the first connection to a NWC wallet with a split payment. 2024-09-27 15:19:59 -04:00
Vitor Pamplona
86e5dfa799 Fixes access to Citrine 2024-09-27 14:38:14 -04:00
Vitor Pamplona
3aa316d262 v0.92.0 2024-09-27 11:14:45 -04:00
Vitor Pamplona
3f4efdb6d9 Merge pull request #1105 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-27 08:26:41 -04:00
Crowdin Bot
a178e5f589 New Crowdin translations by GitHub Action 2024-09-27 12:23:29 +00:00
Vitor Pamplona
cc5a6cdaf2 Merge pull request #1104 from davotoula/update_translations
Update de, cs, se, pt translations
2024-09-27 08:21:49 -04:00
David Kaspar
d9596335de Update de, cs, se, pt translations 2024-09-27 09:51:41 +02:00
Vitor Pamplona
44c600dfa8 Fixes icon position on chat user pictures 2024-09-26 18:36:42 -04:00
Vitor Pamplona
a92fe0bca2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-26 17:20:36 -04:00
Vitor Pamplona
dea9d698df Adds privacy presets for Tor Settings 2024-09-26 17:20:29 -04:00
Vitor Pamplona
94ae7b3da1 Merge pull request #1103 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-26 15:53:03 -04:00
Crowdin Bot
da1c94f77b New Crowdin translations by GitHub Action 2024-09-26 19:26:05 +00:00
Vitor Pamplona
64ff33e745 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-26 15:23:47 -04:00
Vitor Pamplona
8810cfde37 Adjusts Login and Sign up screens for the new Tor Settings 2024-09-26 15:23:39 -04:00
Vitor Pamplona
c7b7ad78b7 Adds mempool to verify OTS via Tor. 2024-09-26 15:23:13 -04:00
Vitor Pamplona
ff9f25ccdb Removes unnecessary log 2024-09-26 14:42:40 -04:00
Vitor Pamplona
498595b892 Merge pull request #1102 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-26 13:48:48 -04:00
Vitor Pamplona
56878900b0 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-26 13:46:52 -04:00
Vitor Pamplona
d8d6736d55 Caches OTS web calls to avoid pinging the server repeatedly for the same event. 2024-09-26 13:45:01 -04:00
Crowdin Bot
ddc68e61d5 New Crowdin translations by GitHub Action 2024-09-26 17:43:05 +00:00
Vitor Pamplona
d4e8fc5737 Merge pull request #1101 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-26 13:41:24 -04:00
Crowdin Bot
3a7dfe7d03 New Crowdin translations by GitHub Action 2024-09-26 15:57:17 +00:00
Vitor Pamplona
d23a859f34 Improves OTS Verification error message 2024-09-26 11:55:29 -04:00
Vitor Pamplona
824cb4e5f1 @Suppress("ProduceStateDoesNotAssignValue") until we figure out what's wrong with this lint. 2024-09-26 11:36:52 -04:00
Vitor Pamplona
35aeeb571c Fixes setting the value in the main thread 2024-09-26 11:20:51 -04:00
Vitor Pamplona
70d68c89c8 Updates compose, benchmark, firebase, navigation and lifecycle dependencies. 2024-09-26 11:20:33 -04:00
Vitor Pamplona
35a6331da2 Fixes test case 2024-09-26 10:59:50 -04:00
Vitor Pamplona
6229a0ad63 making sure to only use .onion urls if the option is active 2024-09-26 10:41:36 -04:00
Vitor Pamplona
620285596d Show buffering when loading videos 2024-09-26 10:39:12 -04:00
Vitor Pamplona
fd2637ff42 Removes unused tor setting 2024-09-26 10:38:36 -04:00
Vitor Pamplona
afe8a06486 - Adds a new Settings page for Tor
- Adjusts all web calls to use Tor if that setting is enabled.
- Allows .onion urls to use Tor
- Blocks localhost from using Tor
- Moves OTS web calls to use the Tor Proxy as well.
- Starts to build all OkHttp clients from a main root node to keep the same thread pool
- Refactors the URL Preview code
- Changes ammolite to force proxy.
- Changes NIP-47 implementation to force relay for the NWC connection.
2024-09-25 17:58:37 -04:00
Vitor Pamplona
b682e90f81 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-23 21:51:23 -04:00
Vitor Pamplona
ec700ca61f Refactoring 2024-09-23 21:51:15 -04:00
Vitor Pamplona
9b26ffd92a Updates to AGP 8.6.1 2024-09-23 21:48:13 -04:00
Vitor Pamplona
a8ca9ab727 Merge pull request #1091 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-20 09:00:43 -04:00
Crowdin Bot
e5832f6a45 New Crowdin translations by GitHub Action 2024-09-20 12:57:12 +00:00
Vitor Pamplona
8b3035f635 Merge pull request #1090 from greenart7c3/main
Fix for notifications when using amber
2024-09-20 08:56:00 -04:00
greenart7c3
9f17cc50f8 Fix for notifications when using amber 2024-09-20 06:48:27 -03:00
Vitor Pamplona
d0633d07fa Fixes inability to go back after opening a nostr link from the web into Amethyst 2024-09-18 19:47:09 -04:00
Vitor Pamplona
97b88c7ceb Merge pull request #1088 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-18 17:38:59 -04:00
Crowdin Bot
4961a22753 New Crowdin translations by GitHub Action 2024-09-18 21:23:22 +00:00
Vitor Pamplona
45b568f6a7 updates amy bg 2024-09-18 17:21:44 -04:00
Vitor Pamplona
446859e8bd adds amy 2024-09-18 17:16:52 -04:00
Vitor Pamplona
e1b24c22b9 adds phone mockups 2024-09-18 17:16:38 -04:00
Vitor Pamplona
e0b1c4dc3f Sending jpg and webp version of the image 2024-09-18 16:28:07 -04:00
Vitor Pamplona
5722507b3d Adds two designed images for the homepage 2024-09-18 16:23:13 -04:00
Vitor Pamplona
74df309527 Improving bug report template 2024-09-17 18:07:41 -04:00
Vitor Pamplona
740c6a8c33 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-17 18:07:29 -04:00
Vitor Pamplona
929a263665 Adds zapstore yaml 2024-09-17 18:07:12 -04:00
Vitor Pamplona
6f44c02b5c Makes sure if the d component of an a-tag has :, it stays in the d-tag after parse. 2024-09-16 18:10:54 -04:00
Vitor Pamplona
bce01dc264 Merge pull request #1083 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-16 11:26:36 -04:00
Crowdin Bot
7919637e12 New Crowdin translations by GitHub Action 2024-09-16 15:25:34 +00:00
Vitor Pamplona
28da06ef01 Fixes #1084. Opengraph parser fails when value-less properties are present. 2024-09-16 11:23:41 -04:00
Vitor Pamplona
428ed464af Adds salt to the bloom filter 2024-09-15 14:28:49 -04:00
Vitor Pamplona
5921b712b0 Merge pull request #1082 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-13 14:17:46 -04:00
Crowdin Bot
5c9a37f01c New Crowdin translations by GitHub Action 2024-09-13 18:12:12 +00:00
Vitor Pamplona
514a69deec Bloom filter test case for NIP76 2024-09-13 14:06:45 -04:00
Vitor Pamplona
356cbc24d4 Merge pull request #1081 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-12 16:09:38 -04:00
Crowdin Bot
f4a2f247f7 New Crowdin translations by GitHub Action 2024-09-12 20:05:59 +00:00
Vitor Pamplona
9ee28e28b8 If already logged in and an NFC tag shows up with ncryptsec, logs in as a transient account 2024-09-12 15:57:35 -04:00
Vitor Pamplona
2457429470 Merge pull request #1078 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-12 08:15:27 -04:00
Crowdin Bot
b998381bb6 New Crowdin translations by GitHub Action 2024-09-12 12:04:40 +00:00
Vitor Pamplona
74725235b7 Merge pull request #1074 from davotoula/add-media-compression-level-uncompressed
Add media compression level uncompressed
2024-09-12 08:02:51 -04:00
David Kaspar
ef4d975c7c Merge branch 'vitorpamplona:main' into add-media-compression-level-uncompressed 2024-09-12 12:40:46 +01:00
Vitor Pamplona
3fdd3f9b69 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-11 17:15:33 -04:00
Vitor Pamplona
53aa9b0017 Trying to fix upload-artifact. 2024-09-11 17:15:23 -04:00
Vitor Pamplona
a52d87a555 Merge pull request #1076 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-11 17:15:10 -04:00
Crowdin Bot
39f062fe32 New Crowdin translations by GitHub Action 2024-09-11 21:03:20 +00:00
Vitor Pamplona
b2c13089ce adds NFC-Based transient accounts.
Refactors the login screen
2024-09-11 16:49:35 -04:00
David Kaspar
4ddf307b9f added translations 2024-09-11 17:27:08 +02:00
David Kaspar
3db36d8c67 added skipping of media compressor when quality is UNCOMPRESSED
added media compressor logging
2024-09-11 17:27:08 +02:00
David Kaspar
bba7d3455c enhanced nip05 logging 2024-09-11 17:27:08 +02:00
Vitor Pamplona
f086c0fe10 Merge pull request #1043 from greenart7c3/camera
Take pictures with camera
2024-09-10 15:32:01 -04:00
Vitor Pamplona
abc803953e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-09 19:44:33 -04:00
Vitor Pamplona
8841aa9a10 Adds Tor 2024-09-09 19:42:43 -04:00
greenart7c3
7e417f09f7 Merge remote-tracking branch 'origin/camera' into camera 2024-09-09 16:10:47 -03:00
greenart7c3
0fde6ffd30 Move camera dependencies to version catalogs 2024-09-09 16:03:35 -03:00
greenart7c3
6f291139d8 Merge branch 'main' into camera 2024-09-09 15:40:56 -03:00
greenart7c3
1e3104d489 Add camera button in the stories screen 2024-09-09 15:37:30 -03:00
greenart7c3
2e110422c8 after enabling permissions open the camera 2024-09-09 14:59:05 -03:00
Vitor Pamplona
7b7aa1932f Merge pull request #1072 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-09 13:37:59 -04:00
Crowdin Bot
773c96997c New Crowdin translations by GitHub Action 2024-09-09 17:37:07 +00:00
Vitor Pamplona
8f8199f489 Merge pull request #1071 from greenart7c3/main
Fix edit draft not working when using the quick action menu
2024-09-09 13:33:54 -04:00
greenart7c3
a63d50c24f Merge remote-tracking branch 'origin/camera' into camera 2024-09-09 14:32:55 -03:00
greenart7c3
6af2b48d23 Add a button to take a picture from camera 2024-09-09 14:32:37 -03:00
greenart7c3
37197e1f8a Fix edit draft not working when using the quick action menu 2024-09-09 14:25:11 -03:00
Vitor Pamplona
8ede4eb3d6 Updates navigation compose, lifecycle, fragment, activity and composeBoms 2024-09-06 17:29:06 -04:00
Vitor Pamplona
c7fcc47b11 Adds an isRefreshing status for all feeds. 2024-09-06 17:19:10 -04:00
Vitor Pamplona
f12cf93e9f Merge pull request #1065 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-06 14:20:11 -04:00
Crowdin Bot
00e354394f New Crowdin translations by GitHub Action 2024-09-06 18:12:34 +00:00
Vitor Pamplona
bbb548fe21 Fixes the zap options available for the Zap the Devs button. 2024-09-06 11:36:01 -04:00
Vitor Pamplona
c19e1fc56d Fixes #1064 2024-09-06 11:19:01 -04:00
Vitor Pamplona
065cf83b68 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-06 11:00:04 -04:00
Vitor Pamplona
ca0da1dcc8 v0.91.0 2024-09-06 10:59:54 -04:00
Vitor Pamplona
58a5cbc624 Merge pull request #1063 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-06 10:12:13 -04:00
Crowdin Bot
554fef9eae New Crowdin translations by GitHub Action 2024-09-06 14:10:20 +00:00
Vitor Pamplona
7dd76f866a Enables the use of hidden words for all visible properties of the user 2024-09-06 10:07:41 -04:00
Vitor Pamplona
84bcc33e87 minor refactoring on variables inside the chat screen rendering. 2024-09-06 10:03:49 -04:00
Vitor Pamplona
6309aa1711 Fixes size of default banner when the profile is not loaded yet. 2024-09-06 10:02:04 -04:00
Vitor Pamplona
a8a3bc8a6b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-05 18:00:25 -04:00
Vitor Pamplona
d2b4a859bf Fixes Community Load up with Global filter 2024-09-05 18:00:17 -04:00
Vitor Pamplona
92d11d84a7 Merge pull request #1061 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-05 17:49:28 -04:00
Crowdin Bot
1cba6e6cdb New Crowdin translations by GitHub Action 2024-09-05 21:41:52 +00:00
Vitor Pamplona
20115c7080 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-05 17:39:51 -04:00
Vitor Pamplona
4b3d986c5b Moves has-new-item bottom navigation computations to use the new feed states and last read database in flows. 2024-09-05 17:39:36 -04:00
Vitor Pamplona
21112d3d46 Merge pull request #1060 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-05 15:23:45 -04:00
Crowdin Bot
ec3f307e0b New Crowdin translations by GitHub Action 2024-09-05 19:17:01 +00:00
Vitor Pamplona
e17cfb4fdb Simplifies the "and 2 more" translations 2024-09-05 15:11:00 -04:00
Vitor Pamplona
010305fbeb - Disables saving m3u8 files (streaming can't be saved)
- Changes Error messages to not include stack traces and move them to a copy to clipboard action instead.
2024-09-05 14:36:23 -04:00
Vitor Pamplona
5af473870e Fixes Search layout after the transition to nav 2024-09-05 14:00:39 -04:00
Vitor Pamplona
80f805617a Adds sliding animations in all inner screens 2024-09-05 13:53:52 -04:00
Vitor Pamplona
3e1184562f Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-05 13:47:33 -04:00
Vitor Pamplona
39edbd22cc fixes #1058 language update issue 2024-09-05 13:47:23 -04:00
Vitor Pamplona
c90e615d5b Merge pull request #1059 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-05 13:00:43 -04:00
Crowdin Bot
5e6e7a3b5f New Crowdin translations by GitHub Action 2024-09-05 16:08:57 +00:00
Vitor Pamplona
3e23d6e526 Merge pull request #1051 from davotoula/configurable-media-compression
Configurable media compression
2024-09-05 12:07:33 -04:00
David Kaspar
08054e226e Merge branch 'vitorpamplona:main' into configurable-media-compression 2024-09-05 16:02:52 +01:00
David Kaspar
cfe668ab0d move slider value label to above slider 2024-09-05 16:58:39 +02:00
Vitor Pamplona
8f19276fac Fixes blinking on crossfades when the system's light/dark theme is different than the app's theme. 2024-09-05 10:50:16 -04:00
Vitor Pamplona
3a3c0d74d2 Fixes the Messages layout on foldables. 2024-09-05 10:30:58 -04:00
David Kaspar
3ad21e72a9 Clean up by using Int instead of Float for compression level 2024-09-05 16:10:26 +02:00
David Kaspar
790b9359bd Merge branch 'vitorpamplona:main' into configurable-media-compression 2024-09-05 14:32:59 +01:00
David Kaspar
e52e2bfdc9 Localise and add czech, swedish, german and PT translations 2024-09-05 15:09:19 +02:00
Vitor Pamplona
a34c763656 Merge pull request #1056 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-05 08:42:51 -04:00
Crowdin Bot
adf5bbe80f New Crowdin translations by GitHub Action 2024-09-05 12:39:02 +00:00
Vitor Pamplona
c4c542c739 Moves the last npub to hexkey for https://github.com/nostr-protocol/nips/pull/1341 2024-09-05 08:37:15 -04:00
David Kaspar
5f0236c015 change MediaCompressor API: mandatory mediaQuality 2024-09-05 14:28:12 +02:00
David Kaspar
023c0592e8 Add compression configuration to VideoScreen uploads 2024-09-05 13:09:17 +02:00
David Kaspar
c6d2f05f15 Styling 2024-09-05 11:50:52 +02:00
David Kaspar
08e9f6625a Merge branch 'main' into configurable-media-compression 2024-09-05 08:32:49 +01:00
David Kaspar
fcf5e972d6 Passing media quality from Slider to Compressor
setting media quality to MEDIUM where ui has no option for setting it (ChannelScreen, ChatroomScreen, NewUserMetadataViewModel)
2024-09-05 09:09:10 +02:00
Vitor Pamplona
20bf4dbc29 Merge pull request #1053 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-04 22:28:12 -04:00
Crowdin Bot
990c5df2b9 New Crowdin translations by GitHub Action 2024-09-05 02:21:58 +00:00
Vitor Pamplona
71af88725a Fixes play version 2024-09-04 22:19:28 -04:00
Vitor Pamplona
fb5f5c3eba no message 2024-09-04 22:18:00 -04:00
Vitor Pamplona
ead588314e - Inverts Layouts to place Navigation on top of Scaffold and allow custom scaffolds per route.
- Finishes Edge to Edge transition
- Fixes moving buttons on the full screen dialog for images and videos.
- Restructures screens into their own packages.
- Restructures navigation functions as a single object
- Fixes padding of the backup screen
- Refactors all TopBars to use default material 3 ones.
- Refactors navigation to improve clarity
- Removes unnecessary observers from the transition in the bottom nav layouts.
2024-09-04 22:17:42 -04:00
Vitor Pamplona
fb447e441a Merge pull request #1052 from greenart7c3/gcompatup
Fix token sanitization when using gcompatup
2024-09-04 13:37:42 -04:00
greenart7c3
3d61343905 Fix token sanitization when using gcompatup 2024-09-04 13:58:55 -03:00
David Kaspar
7e4d9fb708 added media quality slider 2024-09-04 12:32:33 +02:00
Vitor Pamplona
0217fd3eb3 Moves the profile zap to threads 2024-09-03 17:29:41 -04:00
Vitor Pamplona
96fa6f39b0 Fixes ime padding issues when using physical keyboards on the message screens. 2024-09-03 17:28:07 -04:00
Vitor Pamplona
b6bb214532 Fixes text field recompositions because of new keyboard actions 2024-09-03 15:55:52 -04:00
Vitor Pamplona
ee160b76a8 Updates zoomable and kotlin serialization 2024-09-03 15:54:54 -04:00
Vitor Pamplona
2fd9d6610a Normalizes cache directories in the Application class 2024-09-03 15:31:16 -04:00
David Kaspar
272f99be17 added logging 2024-09-03 20:44:39 +02:00
David Kaspar
b8246d7cd4 fixing type 2024-09-03 19:23:47 +02:00
David Kaspar
981d93f2fc making MediaCompressor configurable 2024-09-03 19:18:10 +02:00
Vitor Pamplona
1166fe92e9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-09-03 11:45:27 -04:00
Vitor Pamplona
b700d4e9bf Fixes missing Private Home/Outbox relay list after loading from backup 2024-09-03 11:43:17 -04:00
Vitor Pamplona
0b41d09fda Merge pull request #1050 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-02 16:00:29 -04:00
Crowdin Bot
61c145a68f New Crowdin translations by GitHub Action 2024-09-02 19:29:46 +00:00
Vitor Pamplona
4103342f99 Updates to AGP 8.6.0 2024-09-02 15:24:39 -04:00
Vitor Pamplona
e48ed607ff Fixes #1049 2024-09-02 14:56:24 -04:00
Vitor Pamplona
d5a98db67c v0.90.6 2024-09-02 10:05:07 -04:00
Vitor Pamplona
72412b9d03 Merge 2024-09-02 10:04:38 -04:00
Vitor Pamplona
b5b90c80e9 Revert "v0.90.6"
This reverts commit 2ca5186aef.
2024-09-02 10:03:15 -04:00
Vitor Pamplona
c1077f689d Merge pull request #1046 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-02 10:02:32 -04:00
Crowdin Bot
8b26a84564 New Crowdin translations by GitHub Action 2024-09-02 14:00:35 +00:00
Vitor Pamplona
2ca5186aef v0.90.6 2024-09-02 10:00:28 -04:00
Vitor Pamplona
064e211154 Merge pull request #1045 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-02 09:59:23 -04:00
Vitor Pamplona
2e8ab35f05 Merge pull request #1042 from greenart7c3/clickable_url
Long press to copy relay url
2024-09-02 09:59:14 -04:00
Crowdin Bot
df429b976f New Crowdin translations by GitHub Action 2024-09-02 13:59:11 +00:00
Vitor Pamplona
d40995c338 Merge pull request #1039 from davotoula/1038-Profile-Lightning-payment-is-100x-too-large
millisats were incorrectly multiplied by 1000
2024-09-02 09:57:45 -04:00
greenart7c3
bba7c257da long press to copy relay url in the relay icons and relay row 2024-09-02 09:52:39 -03:00
greenart7c3
a03e02bfeb Add a button to take a picture from camera 2024-09-02 08:42:46 -03:00
David Kaspar
a4a753e7b6 Rename amount to amountInMillisats to give visual clue to calling classes that millisats should be used 2024-09-02 12:26:26 +02:00
greenart7c3
53acbd8606 Long press to copy relay url 2024-09-02 07:15:59 -03:00
Vitor Pamplona
f89c1ff391 v0.90.5 2024-09-01 15:07:07 -04:00
Vitor Pamplona
471ee21749 Merge pull request #1041 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-09-01 12:38:26 -04:00
Crowdin Bot
24f7e043af New Crowdin translations by GitHub Action 2024-09-01 16:37:46 +00:00
Vitor Pamplona
34e82dab8b Merge pull request #1040 from davotoula/1037-tor-setting-reset-to-off-after-switching-away
1037 tor setting reset to off after switching away
2024-09-01 12:36:36 -04:00
David Kaspar
9fa1b8c541 Close "needsPassword.value" if statement earlier to make Orbot checkbox visible again 2024-09-01 17:09:03 +02:00
David Kaspar
1e61162f73 change the logic for saving of proxy state 2024-09-01 17:05:20 +02:00
David Kaspar
941266365e millisats were incorrectly multiplied by 1000 2024-08-31 19:24:12 +01:00
Vitor Pamplona
b1f1466e6b v0.90.4 2024-08-30 18:43:34 -04:00
Vitor Pamplona
55d53dbc4f Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-30 18:42:50 -04:00
Vitor Pamplona
aeaddf722b - Fixes caching bug of NIP-96 servers
- Allow NIP-96 setups with relative URLs
- Displays error messages if the server has sent in the body
- Adds a test case for both
2024-08-30 18:42:41 -04:00
Vitor Pamplona
b733fbce35 Merge pull request #1036 from Dawn11111111/patch-1
Typo Fix on Lnaddress TextField
2024-08-30 12:05:00 -04:00
Dawn
93e9e23730 Typo Fix on Lnaddress TextField
Removed 'i' from lightining :)
2024-08-30 09:45:13 -06:00
Vitor Pamplona
388ccdbe75 Speeds up the filter for NWC zap payments. 2024-08-29 12:32:34 -04:00
Vitor Pamplona
255f464046 Basic code for dynamic limits 2024-08-29 10:29:46 -04:00
Vitor Pamplona
8be74649ce Protects against empty nip04 content 2024-08-28 17:59:59 -04:00
Vitor Pamplona
b5709f9527 Adds basic support for NIP-46 events. 2024-08-28 17:39:25 -04:00
Vitor Pamplona
7d9efcd1d9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-28 16:07:47 -04:00
Vitor Pamplona
d44f5dad18 Merge pull request #1035 from greenart7c3/main
Show relay ping with the relay icon
2024-08-28 16:06:50 -04:00
greenart7c3
06f37ab81d Fix relay icons when using complete ui 2024-08-28 16:20:55 -03:00
Vitor Pamplona
cb4a73bb9c Enables decryption by nip04 and nip44 on NostrWalletConnect objects, NIP-51 lists and NIP-04 messages 2024-08-28 15:20:25 -04:00
greenart7c3
efde14a348 Show relay ping with the relay icon 2024-08-28 16:17:08 -03:00
Vitor Pamplona
0f86e3d8fb Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-28 09:20:48 -04:00
Vitor Pamplona
1407850e81 Fixes push registration with empty token ids 2024-08-28 09:20:40 -04:00
Vitor Pamplona
d9a87f6e7c Merge pull request #1034 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-28 08:33:37 -04:00
Crowdin Bot
5e0a49f225 New Crowdin translations by GitHub Action 2024-08-28 12:31:37 +00:00
Vitor Pamplona
3ebaf2bf1e Fixes ZapTheDevs card not disappearing after the donation. 2024-08-28 08:29:53 -04:00
Vitor Pamplona
9ef0ceed9d Merge pull request #1033 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-27 17:49:17 -04:00
Crowdin Bot
caa3760fd3 New Crowdin translations by GitHub Action 2024-08-27 21:46:25 +00:00
Vitor Pamplona
0b2a9e67c0 v0.90.3 2024-08-27 17:44:44 -04:00
Vitor Pamplona
931081c5e7 Exposes decryption crashing because it is used by Notification Services to check which account can decrypt a GiftWrap. 2024-08-27 17:43:12 -04:00
Vitor Pamplona
f7237b5666 Improves logs on new Notifications 2024-08-27 17:42:18 -04:00
Vitor Pamplona
d851637ea2 Fixes byte to string decoder for UnifiedPush 2024-08-27 17:41:56 -04:00
Vitor Pamplona
2d39b4edfe Only register for endpoints if it has changed from the one in memory 2024-08-27 17:41:37 -04:00
Vitor Pamplona
5001c5bef8 There is no need to extend onReceive 2024-08-27 17:40:53 -04:00
Vitor Pamplona
6ac1db9fc8 correctly passes the endpoint to remove it 2024-08-27 17:40:35 -04:00
Vitor Pamplona
52f33d79e0 Correctly registering the distributor at startup 2024-08-27 17:38:14 -04:00
Vitor Pamplona
1a9d91cdc0 Fixes correct saving of UnifiedPush endpoints 2024-08-27 17:37:53 -04:00
Vitor Pamplona
2f444e6ec5 Moves ChatMessage components to the chatroom package 2024-08-27 16:03:15 -04:00
Vitor Pamplona
4665c488d5 Adds a small border between the author and the message of a chat bubble 2024-08-27 15:57:56 -04:00
Vitor Pamplona
42ab826d02 v0.90.2 2024-08-27 15:01:50 -04:00
Vitor Pamplona
80a3f7424b fixes conversation tab 2024-08-27 14:52:22 -04:00
Vitor Pamplona
1bd3e347b3 Updates media 3 dependencies. 2024-08-27 14:51:30 -04:00
Vitor Pamplona
a332126e71 Quick fix for to not show error messages on Zap the Devs 2024-08-27 14:33:57 -04:00
Vitor Pamplona
d97abe6af9 v0.90.0 2024-08-27 13:48:36 -04:00
Vitor Pamplona
62d341bd8a Publishing new user's events 2024-08-27 11:49:43 -04:00
Vitor Pamplona
a95d2e903e Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt
2024-08-27 11:04:36 -04:00
Vitor Pamplona
2b5e62aaf7 - Improves rendering of the QR Code elements
- Makes the QR bigger
- Moves QR Code to rounder corners
- Moves QR Screen to use the arrow icon instead of the close button
2024-08-27 11:04:13 -04:00
Vitor Pamplona
1c3e82913f Merge pull request #1031 from greenart7c3/main
Use white background for qrcodes
2024-08-27 11:02:33 -04:00
Vitor Pamplona
ceaa20fabf Removes galleries live data 2024-08-27 08:34:25 -04:00
Vitor Pamplona
1bbabb747e Deleting Bundled updates for Account. 2024-08-26 19:12:39 -04:00
Vitor Pamplona
914c0a2926 Making sure scopes are cancelled for accountViewModel notification dot updates. 2024-08-26 19:12:28 -04:00
Vitor Pamplona
4b249156f9 Makes sure the accountViewModel is deleted when the user switches the screen, logs off or kills the activity. 2024-08-26 18:22:34 -04:00
Vitor Pamplona
7b8c7a1a78 Simplifies flow collection for new events. 2024-08-26 18:22:12 -04:00
Vitor Pamplona
66ed26dcea Finishes the migration of the service manager to the Application class 2024-08-26 18:21:53 -04:00
Vitor Pamplona
356740479c Moves to hold the feed itself in a stateflow 2024-08-26 17:48:27 -04:00
Vitor Pamplona
c88dded01b Improves stability of composables 2024-08-26 16:45:07 -04:00
Vitor Pamplona
7c974bc8ef adds a directory for metrics as well 2024-08-26 15:27:04 -04:00
Vitor Pamplona
efbc3e3e78 removes unecessary logs 2024-08-26 14:57:16 -04:00
Vitor Pamplona
fac803e208 Merge pull request #1032 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-26 14:56:04 -04:00
Crowdin Bot
484a3d6b32 New Crowdin translations by GitHub Action 2024-08-26 18:44:35 +00:00
Vitor Pamplona
4e3b6d0299 - Adds a sync Signer to facilitate library
- Separates Account actions from Account state in two objects
- Changes Startup procedures to start with Account state and not the full account object
- Moves scope for flows in Account from an Application-wide scope to ViewModel scope
- Removes all LiveData objects from Account in favor of flows from the state object
- Migrates settings saving logic to flows
- Migrates PushNotification services to work without Account and only Account Settings.
- Migrates the spam filter from LiveData to Flows
- Adds Default lists for NIP-65 inbox and outbox relays
- Adds Default lists for Search relays
- Adds local backup for UserMetadata objects
- Adds local backup for Mute lists
- Adds local backup for NIP-65 relays
- Adds local backup for DM Relays
- Adds local backup for private home relays
- Rewrites state flows initializers to avoid inconsistent startups
2024-08-26 14:42:55 -04:00
Vitor Pamplona
f3161ada8d Solves crash when opening the Relay screen with empty urls as relays. 2024-08-26 11:13:10 -04:00
greenart7c3
5c9b84476b Use white background for qrcodes 2024-08-26 07:21:19 -03:00
Vitor Pamplona
24ea0cabda Updates benchmark, composeBOM and firebaseBOM to the latest versions 2024-08-23 08:35:44 -04:00
Vitor Pamplona
8b617fbd60 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-22 17:27:04 -04:00
Vitor Pamplona
34149e6ce2 Merge pull request #1030 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-22 17:13:16 -04:00
Vitor Pamplona
315a662c10 Improve horizontal padding of the chat messages 2024-08-22 17:12:46 -04:00
Crowdin Bot
c373843e0f New Crowdin translations by GitHub Action 2024-08-22 21:03:22 +00:00
Vitor Pamplona
448ea796ef Improves caching of encrypted DMs:
- Migrates caching of decrypted value outside of the Event class
- Removes encrypted parts of NIP-17 from the cache
- Removes old NIP-04 messages from the cache
- Avoids deleting new NIP-17 plain text chats from memory
2024-08-22 17:01:37 -04:00
Vitor Pamplona
71b111ce8b Fixes the download of 1000s of NIP-65 relay lists because some relays consider empty lists as null and return everything. 2024-08-22 12:24:45 -04:00
Vitor Pamplona
ae686671ba Minimizing the number of filters for group chat metadata 2024-08-22 09:55:28 -04:00
Vitor Pamplona
6c45bb1ca1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-21 20:32:06 -04:00
Vitor Pamplona
a05ad6e686 Adds support for NIP-35 torrents and their comments 2024-08-21 20:31:59 -04:00
Vitor Pamplona
485f7becd4 Merge pull request #1029 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-21 19:18:29 -04:00
Vitor Pamplona
83f1e523ea Creates links to njump when events can't be found on Amethyst 2024-08-21 17:05:14 -04:00
Vitor Pamplona
a8a3c94115 clear flows before removing notes from the local cache 2024-08-21 16:32:11 -04:00
Vitor Pamplona
4d21857adf moves the processing of nip19 links to the default thread 2024-08-21 16:31:57 -04:00
Crowdin Bot
0e7f41efe8 New Crowdin translations by GitHub Action 2024-08-21 20:09:17 +00:00
Vitor Pamplona
8d9bf60dba Merge pull request #1028 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-21 16:08:51 -04:00
Vitor Pamplona
7c67924914 fixes imports 2024-08-21 16:07:42 -04:00
Crowdin Bot
7626839037 New Crowdin translations by GitHub Action 2024-08-21 19:59:14 +00:00
Vitor Pamplona
23825ceed5 Avoids bottom nav recompositions 2024-08-21 15:57:45 -04:00
Vitor Pamplona
8d8016e01b Simplifies nav bar selected calculations 2024-08-21 15:51:01 -04:00
Vitor Pamplona
6668e5a178 Moves accountViewModelMock to a more appropriate file 2024-08-21 15:14:28 -04:00
Vitor Pamplona
7d6bef9576 Moves heavy computations away from IO 2024-08-21 15:00:13 -04:00
Vitor Pamplona
1aea7fbd03 Avoids remembering edit lists that will never exist. 2024-08-21 14:58:01 -04:00
Vitor Pamplona
ef96d296f8 avoids double update to the viewmodel 2024-08-21 14:53:35 -04:00
Vitor Pamplona
d4da58e14a Improves logs when the UserMetadata cannot be parsed 2024-08-21 14:38:17 -04:00
Vitor Pamplona
f646866192 moves markasread from io to default 2024-08-21 09:20:43 -04:00
Vitor Pamplona
e0849b12e7 Merge pull request #1027 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-21 07:55:58 -04:00
Crowdin Bot
1271343582 New Crowdin translations by GitHub Action 2024-08-21 11:52:51 +00:00
Vitor Pamplona
d61d14ea75 Merge pull request #1026 from davotoula/chage-twitter-to-x
Change twitter proof to use x.com
2024-08-21 07:51:04 -04:00
David Kaspar
668d604aa9 use original twitter colour in x.xml because I can't get the theme switching working 2024-08-21 11:56:45 +01:00
Vitor Pamplona
6df2c3a0cf improving speed of chatlist rendering 2024-08-20 15:23:15 -04:00
Vitor Pamplona
b0c33808e6 Adds support for MOD reports 2024-08-20 15:07:28 -04:00
Vitor Pamplona
7bd3e0beab Improved padding and spacing for chat messages 2024-08-20 13:05:44 -04:00
Vitor Pamplona
3a70cfb9e2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-20 09:14:56 -04:00
Vitor Pamplona
17c758f8f8 Displays PoW of commitment if present. 2024-08-20 09:14:47 -04:00
Vitor Pamplona
7cb03b939f Merge pull request #1022 from KotlinGeekDev/fix-relay-set
Fix relay set event.
2024-08-20 08:57:34 -04:00
KotlinGeekDev
481ef91cb1 Change relay set kind to be NIP-51 consistent. 2024-08-20 12:13:42 +01:00
Vitor Pamplona
1183aa8d11 Changes fdroid push registration to re-register even if the saved distributor was already selected. 2024-08-19 17:58:50 -04:00
Vitor Pamplona
4023261400 Merge pull request #1020 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-19 12:07:35 -04:00
Crowdin Bot
f1433504e9 New Crowdin translations by GitHub Action 2024-08-19 16:00:50 +00:00
Vitor Pamplona
989cbb35d3 Disabling landscape rotation on full screen due to portrait videos. 2024-08-19 11:58:50 -04:00
Vitor Pamplona
9262b90096 Merge pull request #1019 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-19 11:40:22 -04:00
Crowdin Bot
ab3b7685cf New Crowdin translations by GitHub Action 2024-08-19 15:40:02 +00:00
Vitor Pamplona
40865ac97d Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-19 11:36:39 -04:00
Vitor Pamplona
ded67964b0 Merge pull request #1018 from KotlinGeekDev/fullscreen-video-behavior
Change tap to fullscreen video behavior for livestreams.
2024-08-19 11:34:47 -04:00
Vitor Pamplona
e991707ddf updates runner 2024-08-19 11:32:04 -04:00
KotlinGeekDev
1b429475aa Remove the manual testing/debugging code. Remove the LocalConfiguration API in favour of DeviceUtils.isLandscapeMetric. 2024-08-17 18:51:07 +01:00
KotlinGeekDev
9613afb7ea Fix ZoomableImageDialog. isFoldableOrLarge already takes into account the orientation. 2024-08-17 17:07:43 +01:00
KotlinGeekDev
26a12f2dbe Remove the external config method and use the built-in Compose API.
Add some documentation.
2024-08-17 17:03:07 +01:00
KotlinGeekDev
23da02f932 Merge remote-tracking branch 'origin/fullscreen-video-behavior' into fullscreen-video-behavior 2024-08-17 15:19:47 +01:00
KotlinGeekDev
59ff9007a7 Move DeviceUtils closer to call-site. Copy code from Material3-Adaptive library(to avoid experimental annotation propagation everywhere).
Implement windowIsLarge to take care of device type handling(sort-of). Add some funky debug methods to make sure windowIsLarge works.
2024-08-17 14:18:48 +00:00
KotlinGeekDev
0f817dbf00 Put device in landscape mode when fullscreen button is pressed. 2024-08-17 14:18:48 +00:00
KotlinGeekDev
7ab6322886 Introduce DeviceUtils, and add functions for changing orientation, and checking for Landscape mode. 2024-08-17 14:18:48 +00:00
KotlinGeekDev
223c0c1263 Initial implementation for auto-fullscreen on rotation to landscape mode. 2024-08-17 14:18:48 +00:00
KotlinGeekDev
d3a639a2ee Consistent naming and behavior for the video player controls. 2024-08-17 14:18:48 +00:00
KotlinGeekDev
2fb1cbe11f Move DeviceUtils closer to call-site. Copy code from Material3-Adaptive library(to avoid experimental annotation propagation everywhere).
Implement windowIsLarge to take care of device type handling(sort-of). Add some funky debug methods to make sure windowIsLarge works.
2024-08-17 15:14:24 +01:00
Vitor Pamplona
29c5b45764 Moves the default zap type to a state flow and avoids passing on to the screen when using the default value. 2024-08-16 17:45:53 -04:00
Vitor Pamplona
ec83ad9d20 Removing unecessary livedata objects for translation services 2024-08-16 17:10:02 -04:00
Vitor Pamplona
239f973e89 Moves lastread routes to mutableStateFlow 2024-08-16 16:42:00 -04:00
Vitor Pamplona
6c81fbbcf9 Fixes crash when the relay url of a user is duplicated. 2024-08-16 16:00:03 -04:00
Vitor Pamplona
75e45f37b2 Migrating livedata to flow for contact list updates of the user. 2024-08-16 15:59:43 -04:00
Vitor Pamplona
0bff3462b1 Migrates the pubkeyHex from keypair to the signer. 2024-08-16 15:58:23 -04:00
Vitor Pamplona
990db68a4d Fixes spacing of short quotes 2024-08-16 13:12:05 -04:00
Vitor Pamplona
4343c744db Displaying stacks in error messages if they are present 2024-08-16 11:14:34 -04:00
Vitor Pamplona
5cb6f97178 linting cleanup 2024-08-16 10:48:30 -04:00
KotlinGeekDev
a8efc9d348 Put device in landscape mode when fullscreen button is pressed. 2024-08-16 14:53:40 +01:00
Vitor Pamplona
e0b667fd2d Fixes order for the notification feed after refactoring 2024-08-16 09:41:38 -04:00
Vitor Pamplona
a8ad2437fe refactors the Watch feed to scroll to the top 2024-08-16 09:39:15 -04:00
KotlinGeekDev
83018e621e Introduce DeviceUtils, and add functions for changing orientation, and checking for Landscape mode. 2024-08-16 14:19:55 +01:00
Vitor Pamplona
68b15ba483 Refactors old user zap object. 2024-08-15 17:08:15 -04:00
Vitor Pamplona
7513036d47 Refactors sorting of Notifications 2024-08-15 16:16:36 -04:00
Vitor Pamplona
c82c594926 Fixes slow down when the last message on chat is a base64 image. 2024-08-15 16:02:28 -04:00
Vitor Pamplona
bd18d4b4b2 Adjusts Outbox filter as soon as a new NIP-65 relay list shows up 2024-08-15 15:17:41 -04:00
Vitor Pamplona
2f5b99a1c7 Merge pull request #1016 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-15 15:06:50 -04:00
Crowdin Bot
1be6e0ec0f New Crowdin translations by GitHub Action 2024-08-15 18:58:39 +00:00
Vitor Pamplona
f43c0e5cf5 Moves error messages from uploading procedures to the full error dialog instead of just a toast. 2024-08-15 14:55:25 -04:00
Vitor Pamplona
d6d8719c8d Fixes the centralization of the "and more" part of relay recommendations 2024-08-14 18:50:26 -04:00
Vitor Pamplona
7478899388 Fixes miscaching flows of the relay lists from follows. 2024-08-14 18:03:25 -04:00
Vitor Pamplona
6b193894bd Fixes miscache of hashtag following button 2024-08-14 17:16:02 -04:00
Vitor Pamplona
69f5371ab8 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-14 17:15:36 -04:00
Vitor Pamplona
526e3db3c1 Adds more information to when error messages are not available in the relay stats. 2024-08-14 15:50:05 -04:00
Vitor Pamplona
3c9e8854eb Merge pull request #1015 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-14 15:14:42 -04:00
Crowdin Bot
7ee0aa47a9 New Crowdin translations by GitHub Action 2024-08-14 19:07:04 +00:00
Vitor Pamplona
5fbf0462fa No need to check for the MainThread in every updateFeedWith 2024-08-14 15:04:51 -04:00
Vitor Pamplona
eda5bc50e6 Adds a destroy method to FollowList state for consistency 2024-08-14 10:45:57 -04:00
David Kaspar
d6d26d466b clean up code in MetadataEvent.kt
update x drawable
2024-08-14 08:11:28 +01:00
Vitor Pamplona
6acd3ca091 Moves follow list states to the AccountViewModel 2024-08-13 19:35:04 -04:00
Vitor Pamplona
c64d179f7f Migrates Notification Summary to the new state model 2024-08-13 18:56:53 -04:00
Vitor Pamplona
32bc8fe667 Fixes f-droid flavor after refactoring 2024-08-13 18:35:31 -04:00
Vitor Pamplona
0a8c792d53 Moves the notification screen to the new state model instead of viewModels 2024-08-13 18:32:20 -04:00
Vitor Pamplona
c32dc61ea1 Refactoring Moving feed status from ViewModel to State objects 2024-08-13 17:32:15 -04:00
Vitor Pamplona
3af2cb52cc adds a prune for giftwrapped messages 2024-08-13 15:11:15 -04:00
Vitor Pamplona
ddb1d7ff82 Adds context to highlight events 2024-08-13 15:08:32 -04:00
KotlinGeekDev
fd0523085d Initial implementation for auto-fullscreen on rotation to landscape mode. 2024-08-13 10:42:49 +01:00
Vitor Pamplona
1f9fbb0c86 Revers letter spacing in inline code on Markdow 2024-08-12 15:58:17 -04:00
Vitor Pamplona
5b95a1c177 Improved logs when clearing the cache for DMs 2024-08-12 15:55:25 -04:00
Vitor Pamplona
2e144ebdf8 Adds previews to test markdown rendering 2024-08-12 15:55:01 -04:00
Vitor Pamplona
ac5f550ff3 Fixes heading sizes on markdown 2024-08-12 15:54:20 -04:00
Vitor Pamplona
9af123b709 Improves the look of inlinde code in markdown 2024-08-12 15:53:45 -04:00
Vitor Pamplona
75efd2b372 Improves badge display 2024-08-12 11:18:22 -04:00
Vitor Pamplona
2cface9ac9 Inverts spam logs 2024-08-12 11:11:23 -04:00
Vitor Pamplona
58491a97ac Merge pull request #1012 from greenart7c3/main
show only 3 users in the recommended relays section
2024-08-12 09:07:21 -04:00
greenart7c3
7a86d78c50 show only 3 users in the recommended relays section 2024-08-12 09:05:08 -03:00
David Kaspar
97a00ff83c First pass updating Twitter verification to X 2024-08-12 11:07:48 +02:00
Vitor Pamplona
4c6842faf4 v0.89.10 2024-08-09 18:47:48 -04:00
Vitor Pamplona
fad8b9df0f Improved filter for notifications 2024-08-09 18:47:38 -04:00
Vitor Pamplona
cc52681635 Moves service manager to the Application class 2024-08-09 18:14:09 -04:00
Vitor Pamplona
939c8e95b0 Adds protections against filters with empty arrays because some relays consider that null as opposed to empty. 2024-08-09 18:13:19 -04:00
Vitor Pamplona
312501e527 AGP to 8.5.2 2024-08-09 15:25:52 -04:00
Vitor Pamplona
89638ff261 Deletes all events in maximum chunks of 200 elements to avoid the 65KB stringified JSON limit of many relays. 2024-08-09 14:35:00 -04:00
Vitor Pamplona
27d2e2309a Fixes the clickable regions to open additional relay info on the relay list dialog. 2024-08-09 13:44:44 -04:00
Vitor Pamplona
7eaa553ebe Allows users to select and copy the notice from the relay on the relay list dialog 2024-08-09 13:43:53 -04:00
KotlinGeekDev
8adddd0bdb Consistent naming and behavior for the video player controls. 2024-08-09 15:26:42 +01:00
Vitor Pamplona
d8f084f886 Merge pull request #1009 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-08 18:37:10 -04:00
Crowdin Bot
5c5a23dfbb New Crowdin translations by GitHub Action 2024-08-08 22:30:34 +00:00
Vitor Pamplona
282d4614c0 Adds zap amount cache for the memory space calculations 2024-08-08 18:19:48 -04:00
Vitor Pamplona
8ed78ea38f Marks username as deprecated 2024-08-08 18:18:54 -04:00
Vitor Pamplona
2e7b7c5c00 improves wording on the name of relays 2024-08-08 14:35:45 -04:00
Vitor Pamplona
68cad97819 Reducing the amount of CPU memory used for images to the default. 2024-08-08 11:51:35 -04:00
Vitor Pamplona
02f7b54661 Updates translate dependencies 2024-08-08 08:31:38 -04:00
Vitor Pamplona
e887232132 v0.89.9 2024-08-07 18:39:02 -04:00
Vitor Pamplona
7bcf4e3750 Removes logs from previous commit 2024-08-07 18:34:38 -04:00
Vitor Pamplona
ed0676a5f5 Fixes the order of bookmarks (keeps the order of the event, instead of the created at) 2024-08-07 18:24:35 -04:00
Vitor Pamplona
f731c654b0 Improves the async rendering of Base64 content 2024-08-07 18:24:14 -04:00
Vitor Pamplona
b026bffe4a Moves discovery and video lists to Outbox when Follows or relay lists are selected 2024-08-07 16:39:24 -04:00
Vitor Pamplona
a1aaec0b7d Adds support for selecting authors based on their Outbox relays when searching for notes authored by them 2024-08-07 15:46:27 -04:00
Vitor Pamplona
6f59097ac0 Aligns default note comparator to NIP-01's created at descending and then by id ascending 2024-08-07 15:44:35 -04:00
Vitor Pamplona
263df0c895 Renames PerRelayFilter to SincePerRelay
Creates PerRelay interface to allow other types of filters
2024-08-07 15:44:03 -04:00
Vitor Pamplona
1ca5713494 Merge pull request #1008 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-07 12:57:34 -04:00
Crowdin Bot
b706c4e871 New Crowdin translations by GitHub Action 2024-08-07 16:04:54 +00:00
Vitor Pamplona
e8574c10bb Keep them public to allow testing in these particular functions 2024-08-07 12:02:48 -04:00
Vitor Pamplona
7cb87ea9c5 Refactors to use native contains instead of custom lambdas on Ammolite's Filter 2024-08-07 11:58:00 -04:00
Vitor Pamplona
07e5132943 Refactors Ammolite Filters to be regular ones and creates a PerRelayFilter for the use on Amethyst. 2024-08-07 11:40:01 -04:00
Vitor Pamplona
2b1e3cfc93 Renames the MinimumRelayList to RecommendationProcessor 2024-08-07 11:39:14 -04:00
Vitor Pamplona
c17e1f0e1f removes logs from previous commit 2024-08-06 17:14:22 -04:00
Vitor Pamplona
e88d1d0d00 Adds haptic feedback to draft deletion swipe 2024-08-06 17:13:06 -04:00
Vitor Pamplona
5fdff97cf8 Moves the ContactList cache lists to AccountViewModel, where it can be disposed more efficiently. 2024-08-06 15:36:04 -04:00
Vitor Pamplona
e5328d7975 Improves the accuracy of the Event memory counter. 2024-08-06 15:35:14 -04:00
Vitor Pamplona
71b45b96fb Adds event factory performance test 2024-08-06 11:04:30 -04:00
Vitor Pamplona
dee28f3aa8 Merge pull request #1006 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-05 17:26:13 -04:00
Crowdin Bot
3a118bb939 New Crowdin translations by GitHub Action 2024-08-05 20:59:07 +00:00
Vitor Pamplona
971c92b27a Adds extension possibility to Quartz's event factory 2024-08-05 16:57:21 -04:00
Vitor Pamplona
4c70065843 Moves DataSource dispatcher from IO to Default 2024-08-05 16:02:35 -04:00
Vitor Pamplona
eab69337ff Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-08-05 16:02:19 -04:00
Vitor Pamplona
ab772eb65e Makes stringRes Stable for compose 2024-08-05 15:48:06 -04:00
Vitor Pamplona
7a6720a7ab Merge pull request #1005 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-05 15:31:47 -04:00
Crowdin Bot
0f3dc18f7a New Crowdin translations by GitHub Action 2024-08-05 19:16:01 +00:00
Vitor Pamplona
84df86f4b6 Removes mutiny NWC button :( 2024-08-05 15:13:35 -04:00
Vitor Pamplona
a0289a0cb4 Moves Relay viewModels to Default thread 2024-08-05 15:08:08 -04:00
Vitor Pamplona
3c034ca8af v0.89.8 2024-08-02 16:34:51 -04:00
Vitor Pamplona
0c8c3003aa Merge pull request #1004 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-08-02 13:37:05 -04:00
Crowdin Bot
919dabfefa New Crowdin translations by GitHub Action 2024-08-02 16:39:19 +00:00
Vitor Pamplona
fd06193e93 Notifies relay failures after marking them as closed. 2024-08-02 12:36:54 -04:00
Vitor Pamplona
b02fa43628 Print relay writes on benchmark as well. 2024-08-02 12:35:52 -04:00
Vitor Pamplona
7fdf38d17b Normalizes logs for the DataSources 2024-08-02 12:35:25 -04:00
Vitor Pamplona
63e036a9bb Minimizes relay updates when rotating status events for each user. 2024-08-02 12:32:44 -04:00
Vitor Pamplona
fc98442f8b Adds support for CashuB tokens using CBOR 2024-07-31 17:36:48 -04:00
Vitor Pamplona
3bbb780d2b Simplifies the drawing of gallery entires without e tags. 2024-07-31 16:02:18 -04:00
Vitor Pamplona
42ceb0c404 Fixes test case for ImageUploaders 2024-07-31 15:59:07 -04:00
Vitor Pamplona
046bd7a0e9 Merge pull request #1003 from greenart7c3/main
Add delete all drafts button
2024-07-31 14:58:26 -04:00
Vitor Pamplona
0af0f745bf Fixes the need for main thread note loading for galleries 2024-07-31 14:57:44 -04:00
Vitor Pamplona
b24999d78c Basic refactoring to minimize the need for user-space params in the VideoView 2024-07-31 14:43:10 -04:00
Vitor Pamplona
d1278a4477 Better rendering for live streams that are planned but already finished. 2024-07-31 14:16:44 -04:00
greenart7c3
609ccaa635 Add delete all drafts button 2024-07-31 13:51:02 -03:00
Vitor Pamplona
4bae04ad31 Merge pull request #1001 from believethehype/better_gallery_view
Gallery: use FileHeader Info for rendering / faster loading
2024-07-31 10:41:19 -04:00
Believethehype
8bca729edd Merge remote-tracking branch 'upstream/main' into better_gallery_view 2024-07-31 16:36:25 +02:00
Vitor Pamplona
2b2f04f724 Adds the callbackUri to the display uncited hashtags so that it can reuse the previously parsed post. 2024-07-31 10:21:13 -04:00
Vitor Pamplona
231af1d3d8 Fixes #993 2024-07-31 10:17:19 -04:00
Believethehype
d1b44c0ac0 remove print 2024-07-31 12:59:23 +02:00
Believethehype
f06c3f7376 massive speed up of rendering the gallery 2024-07-31 12:55:02 +02:00
Believethehype
b8e10b040d cleanup 2024-07-31 12:02:39 +02:00
Believethehype
1a4396dbe3 removes unneded variable 2024-07-31 11:51:44 +02:00
Believethehype
856eb0da05 create GalleryFeed earlier 2024-07-31 11:40:59 +02:00
Believethehype
ceaeb8e0b9 use GalleryContentView instead of 2024-07-31 11:24:05 +02:00
Vitor Pamplona
b29ccd9746 Refactors the lambda used for content resolvers. 2024-07-30 18:16:28 -04:00
Vitor Pamplona
f82364939f Adds icons in SVG to the docs 2024-07-30 18:16:06 -04:00
Vitor Pamplona
286583bba2 fixing the need for a new coroutine startup 2024-07-30 13:50:08 -04:00
Vitor Pamplona
bc8e5aa458 Merge pull request #997 from VASHvic/fix-clipboard-copy-edited-notes
Fix copying to clipboard edited notes
2024-07-30 13:47:49 -04:00
Vitor Pamplona
b393fb042f Merge pull request #999 from believethehype/nutrendering
Show mint in cashu token rendering
2024-07-30 13:47:14 -04:00
Vitor Pamplona
0258c5aac2 Releasing Flows if not subscribed anymore. 2024-07-30 13:22:48 -04:00
Vitor Pamplona
a03a11c4cc Updates depedencies 2024-07-30 13:17:53 -04:00
Vitor Pamplona
3896bbab34 Merge pull request #1000 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-30 09:17:19 -04:00
Crowdin Bot
5459f31000 New Crowdin translations by GitHub Action 2024-07-29 23:24:58 +00:00
Vitor Pamplona
143c3a1444 Testing that both 02 and 03 compressed keys are valid Nostr keys and can encrypt and decrypt to each other. 2024-07-29 19:23:22 -04:00
Believethehype
bfbcf11fa3 Update CashuRedeem.kt 2024-07-29 20:42:17 +02:00
VASH
25823b1f39 Fix copying to clipboard edited notes 2024-07-27 13:05:20 +02:00
Believethehype
cb02955ac6 fix order of Share ImageAction 2024-07-24 07:49:44 +02:00
Vitor Pamplona
b440661e0d Adds the highlight to the base URL so that when the user opens the link, it highlights on the page. 2024-07-23 12:56:53 -04:00
Vitor Pamplona
38ba456a22 Signs for just one auth event to register with the push notification service instead of the dozens of events, one per relay. 2024-07-23 10:15:36 -04:00
Vitor Pamplona
bca941e045 enables crossfading between images 2024-07-23 08:47:20 -04:00
Vitor Pamplona
dbaadb4595 v0.89.7 2024-07-22 18:10:15 -04:00
Vitor Pamplona
231ca44e1c Avoids recomposition of the entire line when changing status. 2024-07-22 18:07:44 -04:00
Vitor Pamplona
ca00c87a6e Prints memory use in descending order by size 2024-07-22 16:59:58 -04:00
Vitor Pamplona
2a28111583 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-07-22 14:27:23 -04:00
Vitor Pamplona
4def80c914 updates the EOSE of users even if no user is found in the relay. 2024-07-22 14:27:10 -04:00
Vitor Pamplona
916ece1edb Merge pull request #992 from greenart7c3/main
Remove focus from status text field when closing the drawer
2024-07-22 14:00:24 -04:00
Vitor Pamplona
3c1f57fac0 Sorts keys before making the filter. 2024-07-22 13:57:31 -04:00
greenart7c3
e166c36731 Remove focus from status text field when closing the drawer 2024-07-22 14:45:39 -03:00
Vitor Pamplona
a636f4a786 updates coil and jackson libs 2024-07-22 11:31:57 -04:00
Vitor Pamplona
bb127506c7 Merge pull request #985 from believethehype/main
Store additional File Metadata in Gallery Entries
2024-07-22 08:10:26 -04:00
Vitor Pamplona
92c704d227 Merge pull request #986 from greenart7c3/main
Add sendAndWaitForResponse to Client.kt
2024-07-22 08:10:11 -04:00
greenart7c3
e764ebbcb2 if the relay just connected send the event 2024-07-22 07:49:54 -03:00
greenart7c3
68fb34a8ad Merge branch 'main' into main 2024-07-22 07:36:43 -03:00
greenart7c3
02683ba829 Merge remote-tracking branch 'origin/main' 2024-07-22 07:34:36 -03:00
greenart7c3
754a1b3a87 change the log of onRelayStateChange 2024-07-22 07:33:21 -03:00
Vitor Pamplona
4b58da7be7 Merge pull request #991 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-21 15:22:31 -04:00
Crowdin Bot
1ac9c850e4 New Crowdin translations by GitHub Action 2024-07-21 18:32:48 +00:00
Vitor Pamplona
3cc4dc1414 Merge pull request #990 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-21 14:31:54 -04:00
Vitor Pamplona
62ae43e863 v0.89.6 2024-07-21 14:31:17 -04:00
Crowdin Bot
8149a93f2f New Crowdin translations by GitHub Action 2024-07-21 18:31:01 +00:00
Vitor Pamplona
5a60c3a595 Fixes stringResource cache 2024-07-21 14:29:05 -04:00
Vitor Pamplona
2ef2f0d1a5 adds the key to avoid applying to a separate navcontroller 2024-07-21 11:32:08 -04:00
Vitor Pamplona
52fef33662 run faster animations 2024-07-21 11:18:56 -04:00
Vitor Pamplona
a5c6f86b35 v0.89.5 2024-07-20 16:27:27 -04:00
Vitor Pamplona
b80008d695 Adds more memory stat dumping for production apps 2024-07-20 16:27:19 -04:00
Vitor Pamplona
fa06aefbf1 increases disk cache to 1GB and adds a memory cache policy. 2024-07-20 15:32:34 -04:00
Vitor Pamplona
ab9ede1e26 Merge pull request #987 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-20 15:08:00 -04:00
Crowdin Bot
479b1ab20a New Crowdin translations by GitHub Action 2024-07-20 19:04:48 +00:00
Vitor Pamplona
8ed3f84cb7 Fixes screen rendering below the 3-button navigation 2024-07-20 15:02:57 -04:00
Vitor Pamplona
c128e43fe5 v0.89.4 2024-07-19 17:10:22 -04:00
Vitor Pamplona
f9ddf76686 action bar fix 2024-07-19 17:09:56 -04:00
Vitor Pamplona
b6cb839fe7 v0.89.3 2024-07-19 16:44:30 -04:00
Vitor Pamplona
2b6a48f391 Fixes issue with super large follow lists. 2024-07-19 16:40:46 -04:00
Vitor Pamplona
d1da863d3c Making sure an empty relay list uses the default list from Amethyst 2024-07-19 16:14:14 -04:00
greenart7c3
99b7a8005a Merge branch 'main' into main 2024-07-19 14:13:36 -03:00
greenart7c3
3f3367258f return true if at least one relay replied with success 2024-07-19 13:29:09 -03:00
Vitor Pamplona
15ab955a7d Fixes alt of NIP-96 server lists 2024-07-19 11:55:46 -04:00
greenart7c3
5cf8b5aa12 change proguard to see if now the client.listener works 2024-07-19 12:49:04 -03:00
greenart7c3
eba6d03b53 check if listeners work now 2024-07-19 12:17:26 -03:00
greenart7c3
de85239046 check if listeners work now 2024-07-19 11:56:30 -03:00
Vitor Pamplona
9176151971 Fixes auth infinite loop with nostr.wine 2024-07-19 10:45:06 -04:00
greenart7c3
cbd8acbd01 check eose too 2024-07-19 11:14:31 -03:00
greenart7c3
19c9bfa819 Merge remote-tracking branch 'origin/main' 2024-07-19 11:14:18 -03:00
greenart7c3
80c369a6ae change default timeout to 15 seconds 2024-07-19 10:56:35 -03:00
greenart7c3
d0190c87a2 add more logs, add optional listener, fix await call 2024-07-19 10:56:35 -03:00
greenart7c3
62a2cba18b fix lint 2024-07-19 10:56:35 -03:00
greenart7c3
59b3ff1b94 Added sendAndWaitForResponse to Client.kt 2024-07-19 10:56:35 -03:00
greenart7c3
35423e1bb6 change default timeout to 15 seconds 2024-07-19 10:52:36 -03:00
greenart7c3
e125690000 add more logs, add optional listener, fix await call 2024-07-19 10:50:59 -03:00
Vitor Pamplona
eba5ecfd91 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-07-19 09:47:04 -04:00
Vitor Pamplona
3e43a75acd Fixes the keyboard overriding parts of the screen 2024-07-19 09:46:58 -04:00
greenart7c3
04bf9c07ac fix lint 2024-07-19 08:05:04 -03:00
greenart7c3
ac58f99560 Added sendAndWaitForResponse to Client.kt 2024-07-19 07:58:12 -03:00
Believethehype
df328ff139 Store additional File Metadata in Gallery Entries 2024-07-18 17:53:37 +02:00
Vitor Pamplona
60ba7786a0 Merge pull request #984 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-18 10:54:01 -04:00
Crowdin Bot
1773ad5a88 New Crowdin translations by GitHub Action 2024-07-18 14:30:23 +00:00
Vitor Pamplona
26faa2bc69 Migrating to edge to edge in preparation for Android 15 2024-07-18 10:28:19 -04:00
Vitor Pamplona
75c7d40f4e v0.89.2 2024-07-17 19:34:56 -04:00
Vitor Pamplona
aa4a3fe9af Doesn't show people lists that have 0 people in it. 2024-07-17 19:33:47 -04:00
Vitor Pamplona
a32f907d6c Fixes incorrectly marking EOSEs for addressables 2024-07-17 19:26:46 -04:00
Vitor Pamplona
d30e613b17 Fixes TopNav people lists not loading 2024-07-17 17:24:06 -04:00
Vitor Pamplona
0477f390c2 Merge pull request #983 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-17 16:54:43 -04:00
Crowdin Bot
15e5e2834a New Crowdin translations by GitHub Action 2024-07-17 20:39:46 +00:00
Vitor Pamplona
b1399d1c13 Separates init and restart from observables. 2024-07-17 16:35:48 -04:00
Vitor Pamplona
45446b4125 Fixes bug on not updating observablesByKindAndAuthor 2024-07-17 16:35:03 -04:00
Vitor Pamplona
094d7e71e2 Merge pull request #982 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-17 09:16:29 -04:00
Crowdin Bot
75423eaaab New Crowdin translations by GitHub Action 2024-07-17 13:13:57 +00:00
Vitor Pamplona
5e42fa5346 Adds additional logs for the size of observables in the Local Cache 2024-07-17 09:09:38 -04:00
Vitor Pamplona
bc6d5060cb Moves frame dropping logs from 20ms to 33ms give than many videos make the UI run at 30fps 2024-07-17 09:09:19 -04:00
Vitor Pamplona
e578327c5b Fixes the inability to click on the relay tab on User Profiles 2024-07-17 09:08:41 -04:00
Vitor Pamplona
ce839ed6d9 v0.89.1 2024-07-16 19:23:37 -04:00
Vitor Pamplona
197484dc1b Removes unnecessary id that was causing crashes. 2024-07-16 19:23:26 -04:00
Vitor Pamplona
60e8ae7b9a v0.89.0 2024-07-16 18:14:04 -04:00
Vitor Pamplona
64ae32d005 Logs when the main looper calls an amethyst handler 2024-07-16 14:54:58 -04:00
Vitor Pamplona
87b8e18f1c Improves logging message for dropped frames 2024-07-16 11:47:41 -04:00
Vitor Pamplona
47a9a6cc4e no message 2024-07-16 09:49:30 -04:00
Vitor Pamplona
1a313a9f78 Adds performance monitors for framedrops in benchmark mode 2024-07-16 09:42:17 -04:00
Vitor Pamplona
3554350ac1 Merge pull request #980 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-16 09:10:47 -04:00
Crowdin Bot
e2e912c81f New Crowdin translations by GitHub Action 2024-07-16 12:48:38 +00:00
Vitor Pamplona
fa71161996 Corrects the name of the live data on Local Cache. 2024-07-16 08:42:05 -04:00
Vitor Pamplona
8de4d461d4 Reduces interruptions to the main thread 2024-07-16 08:41:48 -04:00
Vitor Pamplona
f1e3f6a592 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-07-15 18:16:43 -04:00
Vitor Pamplona
234ef679c8 updates mockk 2024-07-15 18:16:32 -04:00
Vitor Pamplona
072000254f Merge pull request #979 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-15 18:08:57 -04:00
Crowdin Bot
34321af07a New Crowdin translations by GitHub Action 2024-07-15 21:57:29 +00:00
Vitor Pamplona
7da559a0c9 Fixes tests 2024-07-15 17:55:46 -04:00
Vitor Pamplona
0564a7e1f1 Adds dynamic relay pool recommendations to the relay list. 2024-07-15 17:34:08 -04:00
Vitor Pamplona
347b16ee8b Fixes the post button disappeering when the Relay Settings top label is too large 2024-07-15 15:34:08 -04:00
Vitor Pamplona
edb834db38 Fixes a bug on clicking the user profile but loading the wrong one 2024-07-15 15:32:59 -04:00
Vitor Pamplona
97d0cf56a7 Merge pull request #978 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-12 14:00:38 -04:00
Crowdin Bot
ad83d362da New Crowdin translations by GitHub Action 2024-07-12 17:58:49 +00:00
Vitor Pamplona
f15f6df2ae avoids processing anything else than src for spotless linting 2024-07-12 13:55:45 -04:00
Vitor Pamplona
567a86c8a4 Moving compiler to 12GB of ram. 2024-07-12 09:56:46 -04:00
Vitor Pamplona
16c82ed69c Updates firebase dependencies 2024-07-12 08:55:27 -04:00
Vitor Pamplona
a543ee54dc Moves AGP to 8.5.1 2024-07-11 18:27:04 -04:00
Vitor Pamplona
128831109a Merge pull request #977 from believethehype/gallery_alternative_version
GalleryEntryEvent: add relay hint to etag
2024-07-11 12:05:19 -04:00
Vitor Pamplona
9641940bd9 Updates relays that are sending old versions of replaceables or events that have been already deleted. 2024-07-11 11:36:59 -04:00
Vitor Pamplona
ad449ba239 Activates the top nav list building when deletion events are received as well 2024-07-11 11:34:40 -04:00
Vitor Pamplona
31e2fcdc21 Request updates from relays when showing the People Lists in the top bar. 2024-07-11 11:33:31 -04:00
Vitor Pamplona
e28b385038 Removes overloading ambiguity 2024-07-11 10:26:59 -04:00
Vitor Pamplona
8d9ecb2054 Merge pull request #960 from greenart7c3/base64
Convert base64 text to images/gifs
2024-07-11 10:26:15 -04:00
greenart7c3
c57c190344 Merge branch 'main' into base64 2024-07-11 10:42:16 -03:00
Believethehype
7dd319eb25 remove print 2024-07-11 10:40:39 +02:00
Believethehype
b4a059c742 Merge branch 'gallery_alternative_version' of https://github.com/believethehype/amethyst into gallery_alternative_version 2024-07-11 10:08:28 +02:00
Believethehype
43d140175c Merge remote-tracking branch 'upstream/main' into gallery_alternative_version 2024-07-11 08:19:29 +02:00
Vitor Pamplona
1a70a02b44 Fixes scoping issues with flattenMerge 2024-07-10 16:04:39 -04:00
believethehype
a476298c07 Merge branch 'main' into gallery_alternative_version 2024-07-10 21:58:29 +02:00
Believethehype
a0a7f28304 GalleryEntryEvent: add relay hint to etag 2024-07-10 21:53:25 +02:00
Vitor Pamplona
17109e7406 Merge pull request #973 from believethehype/gallery_alternative_version
Profile Gallery
2024-07-10 13:37:14 -04:00
believethehype
c50343cd04 Merge branch 'main' into gallery_alternative_version 2024-07-10 19:35:30 +02:00
Vitor Pamplona
8feac270f4 Removes unnecessary logs. 2024-07-09 18:00:25 -04:00
Vitor Pamplona
c7061066e9 Adds an outbox cache to the Relay class in order to resend events after authentication. 2024-07-09 17:30:16 -04:00
Vitor Pamplona
217870244b No need for material dependencies anymore. 2024-07-09 11:10:34 -04:00
Vitor Pamplona
6fd2282818 increasing memory for kotlin build 2024-07-09 10:55:02 -04:00
Vitor Pamplona
b360c3292f removes unnecessary minification for benchmark's debug flavor 2024-07-09 10:45:51 -04:00
Vitor Pamplona
2a9e1bb08c solves build slowdown on spotless dependencies. 2024-07-09 10:42:50 -04:00
Vitor Pamplona
aa4e6c051e formatting 2024-07-09 10:42:29 -04:00
Vitor Pamplona
f6b947430a Removes the bugfix for reproducible builds since it has been fixed. 2024-07-09 10:42:22 -04:00
Vitor Pamplona
4521317779 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-07-09 09:30:07 -04:00
Vitor Pamplona
25ba353488 Merge pull request #974 from KotlinGeekDev/fix-poll-options-behavior
Fix poll options behavior
2024-07-09 09:29:27 -04:00
Vitor Pamplona
2074b34ec3 Trying maxMetaspaceSize to see if Github Actions can build it. 2024-07-08 13:55:27 -04:00
Vitor Pamplona
f63731c9b6 Merge pull request #976 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-08 13:50:13 -04:00
Crowdin Bot
fc057f2b29 New Crowdin translations by GitHub Action 2024-07-08 17:34:57 +00:00
Vitor Pamplona
af4dfbf965 Making sure posts have content to appear in the feed. 2024-07-08 13:33:16 -04:00
Vitor Pamplona
63c5098558 Pausing discovery top nav list watcher. 2024-07-08 13:33:01 -04:00
Believethehype
dcad12f836 load older notes in feed 2024-07-07 22:01:16 +02:00
KotlinGeekDev
08e84b056e Some cleanup. 2024-07-07 00:06:44 +01:00
KotlinGeekDev
f489deb808 Implement custom element removal with reordering. Works nicely, from observation. 2024-07-06 20:57:06 +01:00
Believethehype
511a7030b8 use alternative gallery using 1163 events instead of 10011 lists.
(This might still need some tuning but works in general)
2024-07-06 00:11:35 +02:00
Believethehype
06a1862d17 add relay info 2024-07-05 13:35:36 +02:00
greenart7c3
b730b8c6f6 Fix linter 2024-07-05 06:56:04 -03:00
greenart7c3
21a45844df Merge branch 'main' into base64 2024-07-05 06:35:52 -03:00
greenart7c3
2480b43778 Decode in the io thread 2024-07-05 06:31:58 -03:00
Believethehype
ad6dcfc030 use url tag, add relay hint 2024-07-05 08:29:49 +02:00
Believethehype
c81324843c move to G Tag for Gallery entries 2024-07-04 20:41:14 +02:00
Believethehype
a55da5f62e linting 2024-07-03 23:28:15 +02:00
Believethehype
9b26bdac5c move GalleryUrl to GalleryListEvent, merge recent changes 2024-07-03 23:11:59 +02:00
Believethehype
4bda6fa0cc Merge remote-tracking branch 'upstream/main' into gallery 2024-07-03 23:03:13 +02:00
Vitor Pamplona
1eaa22fe51 increasing gradle's memory to see if CI passes. 2024-07-03 14:37:59 -04:00
Vitor Pamplona
8e212bbb55 changing JVM of actions to zulu to see if java/kotlin compilation errors improve. 2024-07-03 14:16:16 -04:00
Vitor Pamplona
fdf0b69e5d Lint formatting 2024-07-03 13:49:57 -04:00
Vitor Pamplona
4b5ab8eb35 Removes unnecessary dependencies for ammolite 2024-07-03 13:49:43 -04:00
Vitor Pamplona
349d6cfc22 Migrates sdk requirements to the version catalog. 2024-07-03 13:49:18 -04:00
Vitor Pamplona
33b8267de7 Fixes linting 3 2024-07-03 11:35:39 -04:00
Vitor Pamplona
713bd59435 Fixes Linting 2 2024-07-03 11:33:38 -04:00
Vitor Pamplona
8c34394d86 Fixes linting problems 2024-07-03 11:21:22 -04:00
Vitor Pamplona
6e1de6fd26 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-07-03 11:06:10 -04:00
Vitor Pamplona
8d88efd27b Migrating Lint to 1.3.1 2024-07-03 11:06:02 -04:00
Vitor Pamplona
6aeaeea5d1 Merge pull request #970 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-03 11:03:30 -04:00
Crowdin Bot
3153296161 New Crowdin translations by GitHub Action 2024-07-03 15:02:50 +00:00
Vitor Pamplona
692f7c4270 Upgrades to Kotlin 2.0.0 2024-07-03 11:00:45 -04:00
believethehype
cc8d73e119 Merge branch 'main' into gallery 2024-07-03 13:35:30 +02:00
Vitor Pamplona
27c2c71879 Merge pull request #967 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-02 17:33:10 -04:00
Crowdin Bot
464beccd95 New Crowdin translations by GitHub Action 2024-07-02 21:32:31 +00:00
Vitor Pamplona
7519fc2f10 Adds Malware Report type 2024-07-02 17:30:03 -04:00
Vitor Pamplona
01816b0389 Adds a check to make sure the NIP-96 uploader is a url before adding to the list.
Solves NPE on the model if they are not urls
2024-07-02 17:29:16 -04:00
Believethehype
cc8e432336 make sure only owner of images get delete dialog in Gallery 2024-07-02 11:42:10 +02:00
Believethehype
d1ed73f78b Gallery: get rid of dtag, 2024-07-02 11:36:26 +02:00
Believethehype
7a76bf2ae5 remove unneeded dtag from 10011 2024-07-02 11:31:06 +02:00
KotlinGeekDev
6efb970794 Use keys of map entries, as they are more reliable(from testing). Extract pollViewModel.pollOptions into a separate variable. 2024-07-02 09:51:34 +01:00
believethehype
a0a8010305 Merge branch 'main' into gallery 2024-07-02 08:50:44 +02:00
Vitor Pamplona
9ed1dbac72 Updates lifecycle to 2.8.3 2024-07-01 17:13:37 -04:00
Vitor Pamplona
c7280cf1be Observes and shows only the user's selected NIP96 servers for uploading options 2024-07-01 17:12:01 -04:00
Believethehype
0ed87db3b3 moved to gallery tag 2024-07-01 22:45:50 +02:00
Vitor Pamplona
20bc814211 Fixes tag. From "relay" to "server" on kind 10096 2024-07-01 16:40:37 -04:00
believethehype
0c92c57b79 Merge branch 'main' into gallery 2024-07-01 22:40:31 +02:00
Vitor Pamplona
e61404146b Requests a NIP-96 compliant server 2024-07-01 16:34:44 -04:00
Vitor Pamplona
07dbcbf5c3 Adds FileServer events to be downloaded for the active account. 2024-07-01 16:34:28 -04:00
Believethehype
8421dbfa4c use r tag instead of g (reserved for geotag) 2024-07-01 22:06:40 +02:00
Believethehype
d2af7ba407 Merge remote-tracking branch 'upstream/main' into gallery 2024-07-01 22:03:30 +02:00
Vitor Pamplona
d0718fcda7 Merge pull request #953 from KotlinGeekDev/custom-media-servers
Support for custom media upload servers
2024-07-01 15:30:49 -04:00
believethehype
2372acbb2a Merge branch 'main' into gallery 2024-07-01 20:59:13 +02:00
greenart7c3
9374798f92 Revert "Use Base64Requester"
This reverts commit 2bd05d80b0.
2024-07-01 15:48:22 -03:00
greenart7c3
e96015bcbc Parse base64 inside the wordIdentifier function 2024-07-01 15:07:18 -03:00
greenart7c3
2bd05d80b0 Use Base64Requester 2024-07-01 14:50:19 -03:00
greenart7c3
3a7b503890 Show base64 images 2024-07-01 14:25:29 -03:00
Vitor Pamplona
d20707f4df Removes unused params 2024-07-01 12:51:45 -04:00
Vitor Pamplona
b01d308d65 Merge pull request #966 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-01 12:43:21 -04:00
Crowdin Bot
e0f5c730a6 New Crowdin translations by GitHub Action 2024-07-01 15:50:05 +00:00
Vitor Pamplona
b9086de336 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-07-01 11:48:06 -04:00
Vitor Pamplona
405d3d24a0 Removes decryption for DVM responses since it doesn't encrypt statuses 2024-07-01 11:47:58 -04:00
Vitor Pamplona
236de87574 Fixes unable to see original post when clicking on the edit button 2024-07-01 11:47:30 -04:00
Believethehype
b12d14d6f5 add a shortpreview to not mess up grid layout on load 2024-07-01 09:19:16 +02:00
Believethehype
1d42440e26 add gallery entry to dictionary 2024-07-01 08:21:00 +02:00
Believethehype
ec065a6595 readd regular video elements for regular playing, use isVideo funtion to check 2024-06-30 23:14:37 +02:00
Believethehype
0d83511509 Merge branch 'gallery' of https://github.com/believethehype/amethyst into gallery 2024-06-30 22:29:32 +02:00
Believethehype
9fe08f6c45 allow videos in the gallery 2024-06-30 22:29:11 +02:00
Believethehype
557f2e1a83 Merge remote-tracking branch 'upstream/main' into gallery 2024-06-30 18:02:43 +02:00
Vitor Pamplona
efced4670b Merge pull request #961 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-30 10:03:49 -04:00
Crowdin Bot
c89596809a New Crowdin translations by GitHub Action 2024-06-30 14:00:05 +00:00
Vitor Pamplona
7ccc93f17f Merge pull request #959 from greenart7c3/main
add onerror when websocket is null
2024-06-30 09:58:40 -04:00
greenart7c3
f50e511da0 Cut the text when the content length is greater than SHORT_TEXT_LENGTH and theres no spaces and new lines 2024-06-30 10:20:14 -03:00
greenart7c3
3d497ccf4d add onerror when websocket is null 2024-06-30 09:35:17 -03:00
Vitor Pamplona
5c21e219fe v0.88.8 2024-06-29 19:57:53 -04:00
Vitor Pamplona
58796ac31d - Adds created_at as the time of the notification instead of the receiving time
- Displays Zap Push notifications even if the zapped note is not available yet.
2024-06-29 18:51:41 -04:00
Vitor Pamplona
66317f5593 Better nip44 error messages for notifications 2024-06-29 16:20:49 -04:00
KotlinGeekDev
2f276cb155 Remove TODO, since a basic UI is already implemented. 2024-06-29 21:13:17 +01:00
Vitor Pamplona
b1b915b405 Merge pull request #958 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-29 15:43:01 -04:00
Crowdin Bot
052f9fa21f New Crowdin translations by GitHub Action 2024-06-29 19:38:24 +00:00
Vitor Pamplona
893b237d6a v0.88.7 2024-06-29 15:36:33 -04:00
Vitor Pamplona
e367f51458 merge 2024-06-29 15:34:16 -04:00
Vitor Pamplona
fa44b74f09 Adds a try/catch when receiving notifications to make sure it gets decrypted by the correct account. 2024-06-29 15:33:39 -04:00
believethehype
bb00616275 Merge branch 'main' into gallery 2024-06-29 15:31:54 +02:00
Believethehype
581d11702c Merge remote-tracking branch 'upstream/main' into gallery 2024-06-29 15:31:20 +02:00
Believethehype
16eb6097ce actually use the image from the 10011 event 2024-06-29 15:08:58 +02:00
Vitor Pamplona
2051a6b2f3 Merge pull request #956 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-29 08:10:25 -04:00
Crowdin Bot
02320548e3 New Crowdin translations by GitHub Action 2024-06-29 12:00:54 +00:00
Vitor Pamplona
44a0b59989 Merge pull request #955 from davotoula/update_translations
Update translations
2024-06-29 07:59:40 -04:00
Vitor Pamplona
3288c35bcd Merge pull request #954 from davotoula/additional-html-entities-for-url-preview
Additional html entities for url preview
2024-06-29 07:59:14 -04:00
David Kaspar
bbe0b86690 Update SE, CZ, DE, PT translations 2024-06-29 08:59:45 +01:00
David Kaspar
afbabfd2d3 Update SE, CZ, DE, PT translations 2024-06-29 08:55:50 +01:00
David Kaspar
d404a09f8e additional html entities 8220, 8230
"8230
2024-06-29 08:30:05 +01:00
David Kaspar
30e9d4b3cd additional html entities x2d, middot 2024-06-29 08:30:05 +01:00
KotlinGeekDev
4238978373 Merge remote-tracking branch 'origin/custom-media-servers' into custom-media-servers 2024-06-29 01:14:20 +01:00
KotlinGeekDev
7f3c7ea69c Remove unnecessary files. 2024-06-29 01:13:16 +01:00
KotlinGeekDev
a4202c96d2 Merge branch 'main' into custom-media-servers 2024-06-29 00:10:08 +00:00
KotlinGeekDev
593f817b6d Try fix(again). 2024-06-29 00:56:08 +01:00
Vitor Pamplona
2230d1467c Fixes kind3 relay lists connecting with Search and Wallet connect types. 2024-06-28 19:39:56 -04:00
KotlinGeekDev
af3a52c75e Try fixing this again. 2024-06-29 00:22:06 +01:00
KotlinGeekDev
f9b1cbc9ba Merge branch 'vitorpamplona:main' into custom-media-servers 2024-06-28 23:13:24 +00:00
Vitor Pamplona
0df37d6694 v0.88.6 2024-06-28 19:03:23 -04:00
KotlinGeekDev
9aa511b398 Refactor: location of files. 2024-06-29 00:02:44 +01:00
KotlinGeekDev
5257b1e1cc Merge branch 'main' into custom-media-servers 2024-06-28 22:50:59 +00:00
Vitor Pamplona
4e9e123666 The AccountDataSource will already authenticate for the ZapPayment Relay. 2024-06-28 18:44:27 -04:00
KotlinGeekDev
e7fec7caab Merge branch 'main' into custom-media-servers
merge main branch
2024-06-28 23:43:46 +01:00
KotlinGeekDev
9a4d829f73 Remove TODO. Make isModified private. 2024-06-28 23:12:04 +01:00
KotlinGeekDev
2423ad6e87 [Squash] Use string resources. Ugly solution to avoid duplicates. 2024-06-28 23:08:08 +01:00
KotlinGeekDev
dc40856bc2 Ugly solution to avoid duplicates. 2024-06-28 23:04:25 +01:00
believethehype
5cbea6999e Merge pull request #2 from believethehype/gallery2
Gallery2
2024-06-28 23:57:32 +02:00
Believethehype
0d18a89177 cleanup 2024-06-28 23:53:40 +02:00
Believethehype
b935f60fcb Merge remote-tracking branch 'upstream/main' into gallery2 2024-06-28 23:40:37 +02:00
Believethehype
45df7713ce remove text preview from images 2024-06-28 23:38:41 +02:00
KotlinGeekDev
b26355097d Refactor renderMediaServerList to use callbacks. Use addServerList. 2024-06-28 22:26:24 +01:00
KotlinGeekDev
feee845d31 Add convenience function for adding default Amethyst list. 2024-06-28 22:19:21 +01:00
KotlinGeekDev
660be862d6 Merge remote-tracking branch 'origin/custom-media-servers' into custom-media-servers 2024-06-28 22:18:05 +01:00
KotlinGeekDev
b5047f213e Move media server list rendering to separate function. Fix padding for rendered items. Rename onDelete to onAddOrDelete() for component reuse. 2024-06-28 22:17:42 +01:00
Vitor Pamplona
1fafb6a503 updates create release script to match new directories 2024-06-28 16:58:45 -04:00
Vitor Pamplona
7c6ec49312 Merge pull request #952 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-28 16:33:38 -04:00
Crowdin Bot
6eb7b45990 New Crowdin translations by GitHub Action 2024-06-28 20:27:40 +00:00
Vitor Pamplona
7c7f704173 v0.88.5 2024-06-28 16:25:44 -04:00
Believethehype
f331398316 gallery is visualised 2024-06-28 21:13:52 +02:00
KotlinGeekDev
6c88029fe3 Mve media server list rendering to separate function. Fix padding for rendered items. Rename onDelete to onAddOrDelete() for component reuse. 2024-06-28 20:01:41 +01:00
KotlinGeekDev
aae7236106 Introduce MediaServerEdit component. 2024-06-28 19:57:57 +01:00
KotlinGeekDev
963a42becf Make first character in derived name uppercase, for ...aesthetics. 2024-06-28 19:56:05 +01:00
Vitor Pamplona
f0044afeab Fixes bug on string resources. 2024-06-28 14:06:08 -04:00
Vitor Pamplona
f9c3ff22e3 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-28 13:28:16 -04:00
Vitor Pamplona
347b82ef7f Fixes when the logged in accounts only include the current account. 2024-06-28 13:28:05 -04:00
Believethehype
a42762de53 alternative attempt.. 2024-06-28 19:14:36 +02:00
Vitor Pamplona
27e5d135bc Merge pull request #949 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-28 11:09:07 -04:00
Believethehype
e073e58241 Update GalleryListEvent.kt 2024-06-28 16:51:09 +02:00
Crowdin Bot
a57ba2550c New Crowdin translations by GitHub Action 2024-06-28 14:43:45 +00:00
Vitor Pamplona
a9cbbf66bd Adds uploading error messages for common HTTP status codes. 2024-06-28 10:40:55 -04:00
Believethehype
91caacd36d added first attempt to add user galleries. can read 10011 events and shows profile tab
What works:
- can load existing 10011 lists and show images in profile tab

What doesn't work:
- gallery view is broken
- can't load notes to click images to get to original note
- adding media to gallery crashes amethyst atm
- no functionality to delete media from gallery yet.
2024-06-28 16:01:56 +02:00
Vitor Pamplona
e181296a91 adds auth.nostr1.com as an option 2024-06-27 20:12:56 -04:00
Vitor Pamplona
6a626c9865 Renders relay lists in the feed. 2024-06-27 20:12:04 -04:00
Vitor Pamplona
7095c475c0 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-27 19:03:36 -04:00
Vitor Pamplona
c18df3a157 Fixes extra } when rendering hashtags from Markdown 2024-06-27 19:03:29 -04:00
Vitor Pamplona
aee63de507 Merge pull request #948 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-27 17:03:48 -04:00
Crowdin Bot
2f9438d48d New Crowdin translations by GitHub Action 2024-06-27 21:00:06 +00:00
Vitor Pamplona
700974a5ae Updates testing libs 2024-06-27 16:57:57 -04:00
Vitor Pamplona
15b8a8027c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-27 16:51:39 -04:00
Vitor Pamplona
6c0b22f129 Fixes error when trying to decrypt the PrivateOutboxRelayList event for an account that is not currently active. 2024-06-27 16:51:33 -04:00
Vitor Pamplona
4ee3694292 Fixes error when trying to decrypt the PrivateOutboxRelayList event for an account that is not currently active. 2024-06-27 16:49:05 -04:00
Vitor Pamplona
989099c791 Refactors PlaybackService 2024-06-27 16:47:46 -04:00
Vitor Pamplona
a89440cba2 Moves to non-deterministic signatures 2024-06-27 15:28:09 -04:00
Vitor Pamplona
39c9b4d50f Adjusting default zaps for the Zap the Devs button 2024-06-27 12:19:55 -04:00
KotlinGeekDev
0a6d9a7764 Normalize server URL before adding it, Part 2. 2024-06-27 17:17:49 +01:00
Vitor Pamplona
372eaabb50 Swipe to delete drafts. 2024-06-27 12:13:53 -04:00
KotlinGeekDev
409b63b374 Normalize server URL before adding it. 2024-06-27 16:23:27 +01:00
Vitor Pamplona
ca5c0d6ee4 Adds author picture when writing posts and replies. 2024-06-27 10:00:17 -04:00
Vitor Pamplona
faca3615fc Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-26 17:06:35 -04:00
Vitor Pamplona
7e31401b86 updates versions 2024-06-26 17:06:30 -04:00
Vitor Pamplona
0fad7b313b Fixing alignment of the close button on Chat's reply preview 2024-06-26 16:20:25 -04:00
Vitor Pamplona
930d7d0fed Merge pull request #947 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-26 15:51:10 -04:00
Crowdin Bot
fb59499a0d New Crowdin translations by GitHub Action 2024-06-26 19:45:14 +00:00
Vitor Pamplona
6e256e465f Merge pull request #946 from greenart7c3/main
check for localhost when adding a new relay
2024-06-26 15:44:00 -04:00
greenart7c3
e358387ba1 check for localhost when adding a new relay 2024-06-26 16:33:24 -03:00
KotlinGeekDev
cd9ba9befe Use divider to separate custom and hardcoded server options. Introduce new padding value. 2024-06-26 16:09:56 +01:00
Vitor Pamplona
d6ab376f2d Fixes position of the hash verification icon on NIP-95 images. 2024-06-25 18:26:11 -04:00
Vitor Pamplona
cc6551b11b Removes old highlight rendering functions 2024-06-25 18:19:43 -04:00
Vitor Pamplona
5e417dd890 Fixes the rendering of highlights when no user is present and includes options to render by e tags 2024-06-25 18:19:14 -04:00
Vitor Pamplona
95694ac355 Merge pull request #944 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-25 14:53:29 -04:00
Crowdin Bot
215fba83cf New Crowdin translations by GitHub Action 2024-06-25 18:50:11 +00:00
Vitor Pamplona
a8a2bda9af rearranges the crypto package into separate nips and reduces the amount of circular dependencies. 2024-06-25 12:13:17 -04:00
Vitor Pamplona
79ace7f18c Renames JsonFilter to just Filter and adds a matching function 2024-06-25 10:06:06 -04:00
Vitor Pamplona
c448e75953 Updates all dependencies 2024-06-25 08:44:32 -04:00
Vitor Pamplona
683af698c6 Adjusts the size of the reply button on chats 2024-06-24 20:42:22 -04:00
Vitor Pamplona
fe19ddb259 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-24 19:22:23 -04:00
Vitor Pamplona
4c3c7e7cba Fixes not showing relay icons when sending messages with Amber. 2024-06-24 19:22:15 -04:00
Vitor Pamplona
f5e08e47a8 Merge pull request #942 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-24 19:14:51 -04:00
Crowdin Bot
782df3bcb5 New Crowdin translations by GitHub Action 2024-06-24 23:12:34 +00:00
Vitor Pamplona
1b2f7e21f6 Makes sure to download the updated list of relays when looking at a user. 2024-06-24 18:52:04 -04:00
Vitor Pamplona
4943e1c7ed Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-24 18:07:33 -04:00
Vitor Pamplona
0f066d0817 Saves a copy of the NIP65 and NIP17 relay lists locally 2024-06-24 18:06:53 -04:00
Vitor Pamplona
839cadea35 Saves a copy of the NIP65 and NIP17 relay lists locally 2024-06-24 17:56:24 -04:00
Vitor Pamplona
8cef7bcb75 Logging as a Relay error message when the relay does not accept an event. 2024-06-24 17:55:31 -04:00
Vitor Pamplona
62ac05ae81 Merge pull request #941 from vitorpamplona/changes-app-dir-to-amethyst
changes app directory to amethyst
2024-06-24 17:53:11 -04:00
Vitor Pamplona
4609d6e343 changes app directory to amethyst 2024-06-24 14:13:55 -04:00
Vitor Pamplona
e70e8f8437 Merge pull request #940 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-24 13:39:31 -04:00
Crowdin Bot
1b57c5636d New Crowdin translations by GitHub Action 2024-06-24 17:36:59 +00:00
Vitor Pamplona
9065f8eea2 Merge pull request #939 from greenart7c3/new_package_relay
Moves relay classes to a new package
2024-06-24 13:35:41 -04:00
greenart7c3
8c3e87339a remove commented code 2024-06-24 14:03:01 -03:00
greenart7c3
9ee2452430 Fix build 2024-06-24 11:32:03 -03:00
greenart7c3
266dabdd7a fix shared prefs key 2024-06-24 11:24:13 -03:00
greenart7c3
21898482de Fix merge 2024-06-24 11:22:41 -03:00
greenart7c3
c281654e97 Merge branch 'main' into new_package_relay 2024-06-24 11:16:03 -03:00
greenart7c3
c0ee8e2d61 fix proguard rules 2024-06-24 11:10:13 -03:00
greenart7c3
66e4021d7b Move all relay and datasource to ammolite package 2024-06-24 10:52:34 -03:00
greenart7c3
b00781ec32 check if HttpClientManager appears in the jar 2024-06-24 09:19:14 -03:00
greenart7c3
f08a41d7a5 proguard 2024-06-24 08:33:57 -03:00
greenart7c3
6eb0f153fa fix build 2024-06-24 07:36:11 -03:00
greenart7c3
a6bc40515b Move httpclient to another package 2024-06-24 07:13:06 -03:00
KotlinGeekDev
67ef25f538 Merge branch 'vitorpamplona:main' into custom-media-servers 2024-06-22 18:28:55 +00:00
Vitor Pamplona
c37adfc6a2 v0.88.4 2024-06-21 19:53:49 -04:00
Vitor Pamplona
d6764e08fd Bigger reaction panel 2024-06-21 19:53:02 -04:00
Vitor Pamplona
ed80511cb6 v0.88.3 2024-06-21 19:02:10 -04:00
Vitor Pamplona
08da97e36d Updates version of FirebaseBom 2024-06-21 18:39:27 -04:00
Vitor Pamplona
a14ab59e78 Converts LiveData for Content-Sensitivity and Transitive Hidden Users into Flow to avoid locking the main thread while scrolling.
Requests flows on the Default thread.
2024-06-21 18:38:31 -04:00
Vitor Pamplona
f7c60b3745 Making Metered Wifi behave like Mobile for heavy content. 2024-06-21 17:04:00 -04:00
Vitor Pamplona
f8a77d634c Adds a check to see if the receiver has NIP-17 relays setup and if so activates NIP-17 message. 2024-06-21 15:05:41 -04:00
Vitor Pamplona
fdd1bb9810 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-21 13:35:25 -04:00
Vitor Pamplona
8a1aefeda7 Adds an option to change reactions from the popup 2024-06-21 13:31:48 -04:00
Vitor Pamplona
64813a4623 Merge pull request #938 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-21 13:25:56 -04:00
Crowdin Bot
e86f0c54f3 New Crowdin translations by GitHub Action 2024-06-21 17:20:26 +00:00
Vitor Pamplona
d2f06f5310 Removing the X to delete reaction in favor of a Selected background 2024-06-21 13:18:48 -04:00
Vitor Pamplona
7a22b3df71 Realigning reactions 2024-06-21 12:51:16 -04:00
Vitor Pamplona
97671c570c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-21 12:49:47 -04:00
Vitor Pamplona
accad0c77a Adds a Share option with NIP-19 nprofile icon on the User Profile screen
Moves to NProfile instead of NPub to cite users.
Adds Npub or NIP-05 to the QR Screen
2024-06-21 12:49:35 -04:00
KotlinGeekDev
a19deb2076 Merge branch 'vitorpamplona:main' into custom-media-servers 2024-06-21 13:55:16 +00:00
KotlinGeekDev
bba7dba17c Introduce MediaServersListView and add it to Drawer. 2024-06-21 14:52:08 +01:00
Vitor Pamplona
985274980a Merge pull request #937 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-21 09:29:05 -04:00
Crowdin Bot
4470d01229 New Crowdin translations by GitHub Action 2024-06-21 13:26:29 +00:00
Vitor Pamplona
35a9c07636 Adds pubkey to the host filter in live events. 2024-06-21 09:24:01 -04:00
Vitor Pamplona
3fa5877840 Fixes formatting 2024-06-20 17:39:20 -04:00
Vitor Pamplona
56bbfe60bb Merge pull request #923 from VASHvic/redesign_emoji_selector
redesign emoji selector
2024-06-20 17:36:23 -04:00
Vitor Pamplona
d2f604960e Merge branch 'main' into redesign_emoji_selector 2024-06-20 17:35:41 -04:00
Vitor Pamplona
76a4236b70 Merge pull request #933 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-20 16:49:07 -04:00
Crowdin Bot
fece7411c9 New Crowdin translations by GitHub Action 2024-06-20 20:32:46 +00:00
Vitor Pamplona
0e1e379c7d v0.88.2 2024-06-20 16:30:33 -04:00
Vitor Pamplona
b39ac214a5 Limits the amount of zaps on the user profile 2024-06-20 16:30:12 -04:00
Vitor Pamplona
9e9e0667cd Reverts old migration of accounts due the fact that some people were still in the old model 2024-06-20 16:29:48 -04:00
Vitor Pamplona
82f99faec8 Fixes name instead of uri 2024-06-20 12:22:07 -04:00
Vitor Pamplona
d07a767fa5 Merge pull request #931 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-20 09:55:47 -04:00
Crowdin Bot
ec19038e5c New Crowdin translations by GitHub Action 2024-06-20 13:50:41 +00:00
Vitor Pamplona
eaa09055fc v0.88.1 2024-06-20 09:48:10 -04:00
Vitor Pamplona
0d956c2f39 Removes the need to exclude byte-buddy due to jackson's earlier version bug. 2024-06-20 09:47:43 -04:00
Vitor Pamplona
3a4e2e9623 Don't use DM kinds to get information about other logged-in accounts 2024-06-20 09:47:11 -04:00
Vitor Pamplona
d33befe1ad Removes account migration and unused keys for shared preferences. 2024-06-20 09:42:10 -04:00
KotlinGeekDev
b6fd067d48 Merge branch 'vitorpamplona:main' into custom-media-servers 2024-06-20 12:46:25 +00:00
Vitor Pamplona
d28f20519b v0.88.0 2024-06-19 16:34:01 -04:00
Vitor Pamplona
239f390e7c Fixes Fdroid tests 2024-06-19 15:43:10 -04:00
Vitor Pamplona
857d16582a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-19 15:40:23 -04:00
Vitor Pamplona
aae47aa2e1 Fixing Options Button 2024-06-19 15:40:14 -04:00
Vitor Pamplona
f115111c49 Centralizes stringResource calls to cache them. 2024-06-19 15:12:07 -04:00
Vitor Pamplona
83058500d6 Merge pull request #930 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-19 13:08:48 -04:00
Crowdin Bot
2cb1e70170 New Crowdin translations by GitHub Action 2024-06-19 16:38:15 +00:00
Vitor Pamplona
73513713eb Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-19 12:36:29 -04:00
Vitor Pamplona
7bf5c03637 removing the state needs for rendering text. 2024-06-19 12:36:22 -04:00
Vitor Pamplona
d39d1239f6 Moves uncited hashtag parsing to a thread 2024-06-19 12:36:09 -04:00
Vitor Pamplona
a0271a2982 Moves ClickableBox to it's own file 2024-06-19 12:35:25 -04:00
Vitor Pamplona
2484927c22 Merge pull request #929 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-19 08:54:42 -04:00
Crowdin Bot
bff662b85d New Crowdin translations by GitHub Action 2024-06-19 12:53:07 +00:00
Vitor Pamplona
f293f6c169 Merge pull request #928 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-19 08:52:07 -04:00
Vitor Pamplona
e9456af15c Fixes the inconsistency of button animations in the reaction row 2024-06-19 08:48:41 -04:00
Crowdin Bot
157fad6a84 New Crowdin translations by GitHub Action 2024-06-19 12:48:33 +00:00
Vitor Pamplona
6c137a5174 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-19 08:39:09 -04:00
Vitor Pamplona
6550bdefd5 Thinner Zap button 2024-06-18 19:34:43 -04:00
Vitor Pamplona
614e9ff661 Correctly tints the search button 2024-06-18 19:33:22 -04:00
Vitor Pamplona
5da9e430b0 Get's rid of the view count. 2024-06-18 19:33:04 -04:00
Vitor Pamplona
fbd32bf426 Merge pull request #927 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-18 19:07:45 -04:00
Vitor Pamplona
f28d96d303 removes remember from LoadUrlPreview 2024-06-18 19:00:26 -04:00
Vitor Pamplona
442322f708 Removes the alpha channel for the performance placeholder painter 2024-06-18 19:00:10 -04:00
Vitor Pamplona
a463e8889c Adds an outlined Zap icon to match the other icons 2024-06-18 18:59:41 -04:00
Crowdin Bot
224e54cc72 New Crowdin translations by GitHub Action 2024-06-18 22:23:30 +00:00
Vitor Pamplona
a23cbd1a38 Formatting 2024-06-18 18:21:27 -04:00
Vitor Pamplona
548dd9e34f Makes sure the isAcceptable runs on IO thread 2024-06-18 18:20:58 -04:00
Vitor Pamplona
c27f4a62b6 Switches Social Icons to faster SVG versions 2024-06-18 18:15:02 -04:00
Vitor Pamplona
e18945b378 Adds performance mode for older devices. 2024-06-18 09:13:48 -04:00
Vitor Pamplona
08869ac350 removes derived states and some remember functions for performance. 2024-06-18 09:11:32 -04:00
Vitor Pamplona
5d5c3ae3e3 Faster calculation of uncited hashtags. 2024-06-17 17:22:39 -04:00
Vitor Pamplona
0fa6dce42c Fixes giftwrap date mismatch 2024-06-17 16:20:40 -04:00
Vitor Pamplona
aad95595eb Quick Refactoring of the Author UI components. 2024-06-17 14:37:31 -04:00
Vitor Pamplona
24fede3768 Merge pull request #925 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-16 19:43:00 -04:00
Crowdin Bot
a8b6118e2d New Crowdin translations by GitHub Action 2024-06-16 23:37:55 +00:00
Vitor Pamplona
719be8469f Fix test cases 2024-06-16 19:36:41 -04:00
Vitor Pamplona
1496052bba Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-15 12:36:12 -04:00
Vitor Pamplona
545df83177 Simple refactoring 2024-06-15 12:34:03 -04:00
Vitor Pamplona
80d506be37 Allows login with NIP-05 address 2024-06-15 12:33:43 -04:00
Vitor Pamplona
ae0ae413e6 Fixes Go Button on new user screen 2024-06-15 12:33:06 -04:00
VASH
43339b8d36 redesign emoji selector 2024-06-15 18:27:05 +02:00
Vitor Pamplona
6a9f9e3f81 Merge pull request #922 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-15 11:36:25 -04:00
Crowdin Bot
010402ebd5 New Crowdin translations by GitHub Action 2024-06-15 15:31:05 +00:00
Vitor Pamplona
c07a0065a1 Upgrades Compose, Lifecycle and Fragments 2024-06-15 11:28:20 -04:00
Vitor Pamplona
704af55636 Merge pull request #921 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-15 09:48:42 -04:00
Crowdin Bot
475e1c6fe0 New Crowdin translations by GitHub Action 2024-06-15 13:47:01 +00:00
Vitor Pamplona
21c397b5e0 Merge pull request #920 from davotoula/915-NIP05-verification-follows-redirects-against-specification
Set redirects to false for httpclient used during nip05
2024-06-15 09:45:49 -04:00
David Kaspar
29a002e451 Set redirects to false for httpclient used during nip05 2024-06-15 09:51:05 +02:00
Vitor Pamplona
035affadb9 Merge pull request #919 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-14 12:32:49 -04:00
Vitor Pamplona
79e3142f2d Merge branch 'main' into l10n_crowdin_translations 2024-06-14 12:32:41 -04:00
Crowdin Bot
bf880983a7 New Crowdin translations by GitHub Action 2024-06-14 16:30:32 +00:00
Vitor Pamplona
115746353e Merge pull request #917 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-14 12:29:22 -04:00
Vitor Pamplona
2fd4521e1d Merge pull request #918 from davotoula/correct_spelling
Correct the spelling of a few translation keywords
2024-06-14 12:29:05 -04:00
Crowdin Bot
3a8a9e296e New Crowdin translations by GitHub Action 2024-06-14 16:01:39 +00:00
Vitor Pamplona
86ebb78823 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-14 11:59:24 -04:00
Vitor Pamplona
bda0e37767 Merge pull request #916 from greenart7c3/main
Fix hidden notes when hidden words is empty
2024-06-14 11:59:07 -04:00
David Kaspar
900cc03e4f correct spelling 2024-06-14 17:55:41 +02:00
David Kaspar
013357ae6b correct spelling 2024-06-14 17:51:33 +02:00
David Kaspar
a58a67d404 correct spelling 2024-06-14 17:50:28 +02:00
Vitor Pamplona
2ded31dd55 Improves the speed of RobohashAsync by avoiding remember and base64 state. 2024-06-14 11:42:27 -04:00
greenart7c3
dad455f9a0 Fix hidden notes when hidden words if empty 2024-06-14 12:35:17 -03:00
Vitor Pamplona
8b87f04268 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-14 09:34:59 -04:00
Vitor Pamplona
627c8d1663 Moves to AGP 8.5 and Gradle 8.7 2024-06-14 09:34:48 -04:00
Vitor Pamplona
287a64be11 Merge pull request #914 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-14 08:39:45 -04:00
Crowdin Bot
8f24eeaf4a New Crowdin translations by GitHub Action 2024-06-14 12:33:24 +00:00
Vitor Pamplona
39efc2ef9b Adjusting zap calculations 2024-06-14 08:31:06 -04:00
Vitor Pamplona
52a24d5223 Removing unnecessary modifiers and remember calls 2024-06-14 08:22:38 -04:00
Vitor Pamplona
ca8d32b837 Moves chatroom user group away from immutable sets 2024-06-14 08:22:24 -04:00
Vitor Pamplona
f508f45e16 Fixes Url Preview Crop when the image is too small or to big for the preview card. 2024-06-14 08:16:51 -04:00
Vitor Pamplona
d7a22f609a Small optimizations: removing animations, avoiding remembers 2024-06-13 19:08:20 -04:00
Vitor Pamplona
2503cbaeac - Unifies All Badge rendering
- Constraints badge layouts on Notes
- Creates a flow cache to speed up Note relay layout
- Speeds up NoteQuickActionMenus
- Optimizes NIP-11 pings
- Better error messages when NIP-11 fails
2024-06-13 16:50:19 -04:00
Vitor Pamplona
6bc305dbe6 Refactoring 2024-06-13 16:38:48 -04:00
Vitor Pamplona
26cbc0754e Creates a Loading state to avoid requesting NIP-11 documents twice. 2024-06-13 13:31:51 -04:00
KotlinGeekDev
ed5d7335a0 Simplify addServer() and removeServer(). Introduce saveFileServers() for saving changes, and broadcasting them. 2024-06-13 11:08:44 +01:00
KotlinGeekDev
76e3b5abe7 Add rudimentary implementations for addServer() and removeServer(). 2024-06-13 02:19:18 +01:00
Vitor Pamplona
45445c03e5 - Merges Hidden and Reporting flows
- Removes Report Live data
- Refactors Full Bleed design
- Unifies Hidden and Report checks between the Video Feed, the Full Bleed Design and the Card layout.
2024-06-12 16:54:39 -04:00
Vitor Pamplona
0d00f2f80a No need to remember createdAts 2024-06-12 16:49:10 -04:00
Vitor Pamplona
27f437020e Merge pull request #911 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-12 15:10:24 -04:00
Crowdin Bot
2345c45ebf New Crowdin translations by GitHub Action 2024-06-12 19:07:56 +00:00
Vitor Pamplona
42408978c4 - Moves away from persistent collections (slower)
- Adds a hashcode cash for spam and blocked user public keys
- Adds a cache for isHidden
- Moves isHidden composable from LiveData to Flow
2024-06-12 15:05:46 -04:00
Vitor Pamplona
a716d13c69 Slightly faster is content sensitive method 2024-06-12 14:56:51 -04:00
Vitor Pamplona
77393b433e formatting 2024-06-12 12:41:31 -04:00
Vitor Pamplona
a7c06b46a7 Use navigateUp instead of popBackStack to avoid closing the app on double clicks 2024-06-12 12:40:43 -04:00
Vitor Pamplona
1de55bb16f Migrates to AGP 8.4.2 2024-06-12 08:01:54 -04:00
Vitor Pamplona
20520bbce4 Fixes the centered url on videos without active playback 2024-06-11 19:15:02 -04:00
Vitor Pamplona
42e11d239d Fixes the padding of the reaction row on quotes 2024-06-11 18:51:47 -04:00
Vitor Pamplona
957ad1e0db Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-11 18:20:35 -04:00
Vitor Pamplona
3123685f88 Tries to solve notification dots active for hidden posts. 2024-06-11 18:20:27 -04:00
Vitor Pamplona
31e8782b1a adds a native map for tags in the Event class to avoid creating sublists before mapping tags 2024-06-11 18:19:52 -04:00
Vitor Pamplona
cafad6a003 Refactoring 2024-06-11 18:19:07 -04:00
Vitor Pamplona
b785e0d505 Removes old Robohash 2024-06-11 18:17:51 -04:00
Vitor Pamplona
226153a359 Merge pull request #910 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-11 15:23:06 -04:00
Crowdin Bot
b70aa54aa3 New Crowdin translations by GitHub Action 2024-06-11 19:07:46 +00:00
Vitor Pamplona
fc96d89a44 Reducing double launch or co-routines 2024-06-11 15:05:57 -04:00
Vitor Pamplona
dd71b931fc Reducing memory recreation by only setting the current subscription as after EOSE 2024-06-11 15:05:38 -04:00
Vitor Pamplona
d6abdf16e0 Increasing the download limit for reactions to event 2024-06-11 15:05:11 -04:00
Vitor Pamplona
66e18e17c5 Adds relays to zap request:
- sender, receiver, author
2024-06-11 13:32:13 -04:00
Vitor Pamplona
7b2da8312d Adds two extra nevent test cases. 2024-06-11 13:31:45 -04:00
KotlinGeekDev
353a30efe4 Introduce MediaServersViewModel. Basic things for now. 2024-06-11 16:46:49 +01:00
Vitor Pamplona
c71c87e946 Better rounding for zaps 2024-06-11 11:30:55 -04:00
Vitor Pamplona
9ca6151313 - Adds the NIP-65 relay to zap split tag instead of kind3 relays.
- Rounds percentages to the 2 decimals.
2024-06-11 11:23:20 -04:00
Vitor Pamplona
b322efc286 Fixes padding of the zap raiser 2024-06-11 11:22:21 -04:00
Vitor Pamplona
b775c1bf74 Avoids showing error message if devs have removed their lnadress on the Zap the Devs card. 2024-06-11 11:22:09 -04:00
Vitor Pamplona
a6b2671b1c Fixes padding of the zap split section on the master note 2024-06-11 11:20:44 -04:00
Vitor Pamplona
abc5d0ea2f Fixes border of the zap card 2024-06-11 10:46:15 -04:00
Vitor Pamplona
3c25d91b98 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-11 10:36:52 -04:00
Vitor Pamplona
3d8aace55a Merge pull request #909 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-11 10:36:44 -04:00
Vitor Pamplona
bd6202f8e1 - Adds support for AVIF images
- Removes gray border in image urls that couldn't be loaded.
2024-06-11 10:25:11 -04:00
Crowdin Bot
c1875e1389 New Crowdin translations by GitHub Action 2024-06-11 14:03:00 +00:00
Vitor Pamplona
3a63c1d1ab Fixes alignment of reactions 2024-06-11 10:00:26 -04:00
Vitor Pamplona
609faf245a Reverts save button 2024-06-10 20:01:37 -04:00
Vitor Pamplona
bfd28c4b44 Reverts save button 2024-06-10 20:01:11 -04:00
Vitor Pamplona
70a8488f12 Merge pull request #907 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-10 19:59:47 -04:00
Crowdin Bot
cda6e4722c New Crowdin translations by GitHub Action 2024-06-10 23:54:07 +00:00
Vitor Pamplona
c4ecf85618 Reverting the spotlight on the Save button 2024-06-10 19:51:52 -04:00
Vitor Pamplona
6ed1f9d369 Merge pull request #906 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-10 09:59:32 -04:00
Crowdin Bot
aea765ec54 New Crowdin translations by GitHub Action 2024-06-10 13:52:45 +00:00
Vitor Pamplona
165c3bb852 Fixes not centered Blank Notes 2024-06-10 09:50:37 -04:00
Vitor Pamplona
2e9683491f Fixes scrollable drawer for all screen sizes 2024-06-10 09:50:24 -04:00
Vitor Pamplona
eea8b99622 v0.87.6 2024-06-07 20:16:52 -04:00
Vitor Pamplona
0db6a0d27b Fixes relay drops and difficulty in connecting.
Refactors the use of relay objects to avoid creating full state relay connections only once
2024-06-07 20:13:15 -04:00
Vitor Pamplona
723e575b4b Fixes the LazyColumn for Hashtags on Profile 2024-06-07 19:28:20 -04:00
Vitor Pamplona
b182d409b6 Fixes benchmark tests 2024-06-07 18:50:32 -04:00
Vitor Pamplona
043e37030e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-07 13:46:11 -04:00
Vitor Pamplona
8d730040b8 Improved description for public inbox 2024-06-07 13:46:04 -04:00
Vitor Pamplona
794a300c74 Merge pull request #904 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-07 12:42:58 -04:00
Crowdin Bot
91fd490ab7 New Crowdin translations by GitHub Action 2024-06-07 15:54:44 +00:00
Vitor Pamplona
0e1de03db8 Only updates video position when the video is ready to play 2024-06-07 11:52:39 -04:00
Vitor Pamplona
7906a94ca0 Moves the video release to a thread without damaging ON_PAUSE, ON_RELEASE order. 2024-06-07 11:42:15 -04:00
Vitor Pamplona
6ce019a86a Larger search limits for profiles 2024-06-07 10:37:13 -04:00
Vitor Pamplona
d8939779e8 Improves UI buttons of the Image Dialog 2024-06-07 10:36:56 -04:00
Vitor Pamplona
313d87a58b Removes debug marker 2024-06-06 18:43:11 -04:00
Vitor Pamplona
057e61bfe1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-06 18:39:48 -04:00
Vitor Pamplona
c94c882c3a restructures video and image views to avoid buttons getting on top of one another. 2024-06-06 18:39:39 -04:00
Vitor Pamplona
673f632563 Refactoring 2024-06-06 12:43:59 -04:00
Vitor Pamplona
1a56b2bf68 Log messages when saving to disk. 2024-06-05 18:21:20 -04:00
Vitor Pamplona
2e9964e0e8 Merge pull request #903 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-05 16:37:28 -04:00
Crowdin Bot
b80eb32991 New Crowdin translations by GitHub Action 2024-06-05 20:30:27 +00:00
Vitor Pamplona
ad281c7607 BugFix crashing with old keyboards: Forces transformations to have middle positions before the last cursor position. 2024-06-05 16:28:44 -04:00
Vitor Pamplona
3c1a918b2d Moving attestations local cache to a flow 2024-06-05 13:24:31 -04:00
Vitor Pamplona
53e6677558 Adds Hashtag search and display to Reposted Notes. 2024-06-05 11:58:02 -04:00
Vitor Pamplona
b8deb34544 Avoid using DM relays to get chats 2024-06-05 09:34:03 -04:00
Vitor Pamplona
d9cffa87ef When broadcasting privately, avoids keeping the connection open for other activities. 2024-06-05 08:33:11 -04:00
Vitor Pamplona
ae418f9d45 v0.87.5 2024-06-04 18:46:45 -04:00
Vitor Pamplona
d10b4c6bde - Re-normalizes all relays urls before connecting to reduce duplications
- Normalizes all new relays in the edit screens.
2024-06-04 18:30:01 -04:00
Vitor Pamplona
f1e516662c Adds flare.pub videos to the media tab on Amethyst 2024-06-04 17:12:14 -04:00
Vitor Pamplona
1bf8165641 Improving the detection of image types on the Media Feed. 2024-06-04 16:38:31 -04:00
Vitor Pamplona
807af3b613 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-04 16:01:50 -04:00
Vitor Pamplona
e36ec0a0a3 removes extra char on xml 2024-06-04 16:01:37 -04:00
Vitor Pamplona
3172b69271 Merge pull request #902 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-04 15:55:27 -04:00
Crowdin Bot
81ee13bebc New Crowdin translations by GitHub Action 2024-06-04 19:47:45 +00:00
Vitor Pamplona
a6a8138c7f - Performance Improvements: Simplifies intrinsic size calculations for Image Previews and Videos
- Improves rendering of chat messages
- Fixes the jittering from resizing Videos and Images during loading.
- Fixes video cut off when loading
- Fixes blurhash covering videos in landscape
- Fixes landscape video centralization
- Fixes chat using Two Pane layout for landscape and regular for portrait
2024-06-04 15:45:59 -04:00
Vitor Pamplona
16142f61b8 normalization refactoring 2024-06-04 10:54:06 -04:00
Vitor Pamplona
7193228dd2 Fixes size of the incognitoIcon on chat messages 2024-06-03 19:32:46 -04:00
2957 changed files with 237950 additions and 119904 deletions

View File

@@ -24,9 +24,13 @@ A clear and concise description of what you expected to happen.
If applicable, add a video and/or screenshots to help explain your problem.
**Device (please complete the following information):**
- Phone Brand/Model [e.g. Pixel 7 Pro]:
- Android Version [e.g. 33]:
- App Version [e.g. v0.20.3]:
- Phone Brand/Model [e.g. Pixel 7 Pro]:
- Android Version [e.g. 34]:
- App Version [e.g. v0.94.3]:
- App Flavour [e.g. Google Play or FDroid]:
- Amber Version (if using it to sign):
**Bounty (in Bitcoin sats) offered for a solution**
Incentivize developers to work on your issue. Describe clear milestones to claim payment.
The size of the bounty is proportional to how much this matters to you. If no bounty is offered,
not even a small one, this bug will not be worked on because it doesn't matter to you. We prioritize
bug fixing to issues that have bounties, even small ones.

View File

@@ -7,14 +7,10 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Bounty (in Bitcoin sats) offered for the implementation**
Incentivize developers to work on your feature. Describe clear milestones to claim payment.
**Additional context**
Add any other context, video, or screenshots about the feature request here.
The size of the bounty is proportional to how much this matters to you. If no bounty is offered,
not even a small one, this feature will not be coded because it doesn't actually matter to you.
We prioritize feature development by its bounty size compared to how much work is required to get it done.

View File

@@ -15,14 +15,14 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
distribution: 'zulu'
java-version: 21
- name: Cache gradle
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
@@ -40,17 +40,47 @@ jobs:
if: ${{ always() }} # IMPORTANT: run Android Test Report regardless
- name: Build APK (gradle)
run: ./gradlew assembleDebug --no-daemon
run: ./gradlew assembleDebug
- name: Upload APK
uses: actions/upload-artifact@v2
- name: Upload Play APK
uses: actions/upload-artifact@v4
with:
name: Debug APK
path: app/build/outputs/apk/debug/app-debug.apk
name: Play Debug APK
path: amethyst/build/outputs/apk/play/debug/amethyst-play-universal-debug.apk
- name: Upload FDroid APK
uses: actions/upload-artifact@v4
with:
name: FDroid Debug APK
path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-debug.apk
- name: Build APK (gradle)
run: ./gradlew assembleBenchmark
- name: Upload Play APK Benchmark
uses: actions/upload-artifact@v4
with:
name: Play Benchmark APK
path: amethyst/build/outputs/apk/play/debug/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
- name: Upload Compose Reports
uses: actions/upload-artifact@v4
with:
name: Compose Reports
path: amethyst/build/compose_compiler
- name: Tests
run: ./gradlew test --no-daemon
- name: Upload Test Results
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Build Reports
path: app/build/reports
name: Test Reports
path: amethyst/build/reports

View File

@@ -12,14 +12,14 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
distribution: 'zulu'
java-version: 21
- name: Cache gradle
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
@@ -32,26 +32,26 @@ jobs:
- name: Sign AAB (Google Play)
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/bundle/playRelease
releaseDirectory: amethyst/build/outputs/bundle/playRelease
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "34.0.0"
BUILD_TOOLS_VERSION: "36.0.0"
- name: Sign AAB (F-Droid)
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/bundle/fdroidRelease
releaseDirectory: amethyst/build/outputs/bundle/fdroidRelease
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "34.0.0"
BUILD_TOOLS_VERSION: "36.0.0"
- name: Build APK
run: ./gradlew assembleRelease --stacktrace
@@ -59,26 +59,26 @@ jobs:
- name: Sign APK (Google Play)
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/play/release
releaseDirectory: amethyst/build/outputs/apk/play/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "34.0.0"
BUILD_TOOLS_VERSION: "36.0.0"
- name: Sign APK (F-Droid)
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/fdroid/release
releaseDirectory: amethyst/build/outputs/apk/fdroid/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "34.0.0"
BUILD_TOOLS_VERSION: "36.0.0"
- name: Create Release
id: create_release
@@ -99,7 +99,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -110,7 +110,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -121,7 +121,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -132,7 +132,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-arm64-v8a-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -143,7 +143,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-armeabi-v7a-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -155,7 +155,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -166,7 +166,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -177,7 +177,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -188,7 +188,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-arm64-v8a-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -199,7 +199,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-armeabi-v7a-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -213,7 +213,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/playRelease/app-play-release.aab
asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
asset_content_type: application/zip
@@ -225,6 +225,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
asset_content_type: application/zip

View File

@@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v4
- name: crowdin action
uses: crowdin/github-action@v1
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: true

4
.gitignore vendored
View File

@@ -12,6 +12,9 @@
/.idea/ktlint-plugin.xml
/.idea/ktfmt.xml
/.idea/studiobot.xml
/.idea/other.xml
/.idea/runConfigurations.xml
/.idea/ChatHistory_schema_v2.xml
.DS_Store
/build
/captures
@@ -41,6 +44,7 @@ out/
.gradle
.gradle/
build/
.kotlin/
# Local configuration file (sdk path, etc)
local.properties

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

1
.idea/icon.svg generated Normal file
View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 170.16 186.2"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}</style><linearGradient id="linear-gradient" x1="80.19" y1="57.17" x2="91.66" y2="57.17" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#652d80"/><stop offset="1" stop-color="#2598cf"/></linearGradient><linearGradient id="linear-gradient-2" x1="335.87" y1="297.64" x2="506.02" y2="297.64" xlink:href="#linear-gradient"/></defs><title>Amethyst_svg_monogram</title><ellipse class="cls-1" cx="85.93" cy="57.17" rx="5.74" ry="5.64"/><path class="cls-2" d="M506,347.08c-.2-.61-46.85-111.69-59.68-142.5-15.62,0-48.61,0-71.88,0-6.83,16.89-24.46,59.94-38.6,94.62,7.75,18.49,14.71,35.1,20.08,47.91l29.27,0a2,2,0,0,0,1.87-2.35c-.25-5.91-16.34-27.39-11.54-50.63,1.28-5,2.26-10.08,3.66-15.06a175.77,175.77,0,0,1,13.44-34.8c.89-1.69,1.3-2.45,3.35-1.17,4.37,2.72,9.25,2.56,14.11,2.21,4.19-.91,6.23-2.92,13.91-.88,1.52,0,3,0,4.55.14,3.69.29,7.15,1.17,9.37,4.51,2.42,3.65,2.81,7.78,2.42,12-.59,6.31-.17,12.19,5.17,16.64a57.52,57.52,0,0,0,6,4c2.65,1.7,5.85,2.44,8.12,4.8,1.34,1.39,2.13,2.87,1.55,4.85s-2.13,2.91-4.17,3.13c-5.16.56-10.2-.49-15.27-1.1-.66-.08-1.31-.13-2-.17a11.47,11.47,0,0,1-3.81.13l-1.19,0a26.7,26.7,0,0,0-5.9,1.41c-4.78,1.74-9.13,3.66-14.77,3.56a4.32,4.32,0,0,0-2.05.89c-4.42,3.93-7.08,8.89-4.87,16.14,6.06,16.93,21.61,57.77,28.29,75.4-.19-11.94-.24-33.32-.28-43.7C461.83,347.1,502.74,347.08,506,347.08Z" transform="translate(-335.87 -204.54)"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,6 +1,34 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
@@ -9,6 +37,10 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
@@ -33,6 +65,10 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />

6
.idea/kotlinNotebook.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinNotebookOptionsProvider">
<option name="shouldAddProjectLibrariesToClasspath" value="true" />
</component>
</project>

8
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JsCompilerArguments">
<option name="moduleKind" value="plain" />
</component>
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.24" />
<option name="version" value="2.1.0" />
</component>
</project>

View File

@@ -4,33 +4,39 @@
Effective as of Jun 12, 2023
The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open-source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them.
The Amethyst app for Android does not collect or process any personal information from its users.
The app may collect a per-device token, your public key, and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts are only stored locally on the device when it's required for the functionality and performance of Amethyst. This data is strictly confidential and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app.
The app is used to browse third-party Nostr servers (called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open-source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them.
Amethyst offers a few options to upload pictures and videos in order to post them online. You can choose the server at your discretion. Similar to relays, such services are independent of the app and have their own privacy policy and terms of use.
The app may collect a per-device token, your public key, and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts is only stored locally on the device when it's required for the functionality and performance of Amethyst. This data is strictly confidential and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app.
### Privacy with Relays services
Amethyst offers several options for uploading pictures and videos to post online. You can choose the server at your discretion. Similar to relays, such services are independent of the app and have their own privacy policy and terms of use.
Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
### Privacy with Relay services
Your Internet Protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g., a VPN) from trackers online.
The relay can also see which public keys you are using and what information you are requesting from the network. Your public key is tied to your IP address and your relay filters.
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of the content inside Private Zaps and Private DMs.
While the content of direct messages (DMs) is only visible to you, and your DM Nostr counterparty, everyone can see when you and your counterparty are DM-ing each other. Image uploads in the DM screen use one of the chosen image servers and simply paste the image link on the DM text. Your uploaded pictures are available to anyone with that direct link.
While the content of direct messages (DMs) is only visible to you and your DM Nostr counterparty, everyone can see when you and your counterparty are DM-ing each other. Image uploads in the DM screen use one of the chosen image servers and simply paste the image link into the DM text. Your uploaded pictures are available to anyone with that direct link.
### Visibility & Permanence of Your Content on Nostr Relays
#### Information Visibility
Content that you share can be shared with other relays by any user of the network.
Information that you share is publicly visible to anyone reading from relays that have your information. Your information may also be visible to Nostr users who do not share relays with you.
The information you share is publicly visible to anyone reading from relays that have access to your information. Your information may also be visible to Nostr users who do not share relays with you.
#### Information Permanence
Information shared on Nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted.
## Child safety standards
Amethyst does not knowingly collect information from children. The app has no age verification because it collects no personal information from anyone. The application is 17+. We rely on Google Play's age verification to make sure the user downloading the app is an adult. Since we do not control which relays the user connects to, there is no content moderation beyond the standard block post, block account, and report post and/or account that will hide the content from the user.
## Terms of Use
### For versions downloaded from Google's Play Store

View File

@@ -17,17 +17,18 @@ Join the social network you control.
[![JitPack version](https://jitpack.io/v/vitorpamplona/amethyst.svg)](https://jitpack.io/#vitorpamplona/amethyst)
[![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/vitorpamplona/amethyst)
## Download and Install
[<img src="./docs/design/zapstore.svg"
alt="Get it on Zap Store"
height="70">](https://github.com/zapstore/zapstore/releases)
[<img src="./docs/design/obtainium.png"
alt="Get it on Obtaininum"
height="70">](https://github.com/ImranR98/Obtainium)
[<img src="https://github.com/machiav3lli/oandbackupx/raw/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub"
height="70">](https://github.com/vitorpamplona/amethyst/releases)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="70">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play"
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
@@ -61,7 +62,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
- [x] Bech Encoding support (NIP-19)
- [x] Command Results (NIP-20)
- [x] URI Support (NIP-21)
- [x] Long-form Content (NIP-23)
- [x] Long-form Content (NIP-23) (view only)
- [x] User Profile Fields / Relay list (NIP-24)
- [x] Reactions (NIP-25)
- [ ] Delegated Event Signing (NIP-26, Will not implement)
@@ -105,7 +106,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
- [x] Video Events (NIP-71)
- [x] Moderated Communities (NIP-72)
- [ ] Zap Goals (NIP-75)
- [ ] Arbitrary Custom App Data (NIP-78)
- [x] Arbitrary Custom App Data (NIP-78)
- [x] Highlights (NIP-84)
- [x] Notify Request (NIP-88/Draft)
- [x] Recommended Application Handlers (NIP-89)
@@ -134,7 +135,8 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
- [ ] Signed Filters (NIP-xx/Draft)
- [ ] Key Migration (NIP-xx/Draft)
- [ ] Time-based Sync (NIP-xx/Draft)
- [ ] Image/Video Capture in the app
- [x] Image Capture in the app
- [ ] Video Capture in the app
- [ ] Local Database
- [ ] Workspaces
- [ ] Infinity Scroll
@@ -173,7 +175,7 @@ Lastly, the user's account information (private key/pub key) is stored in the An
## Setup
Make sure to have the following pre-requisites installed:
1. Java 17+
1. Java 21+
2. Android Studio
3. Android 8.0+ Phone or Emulation setup
@@ -226,7 +228,7 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
- `KEY_PASSWORD` <- `<your password>`
- `KEY_STORE_PASSWORD` <- `<your key store password>`
- `SIGNING_KEY` <- the data from `<my-release-key.keystore>`
3. Change the `versionCode` and `versionName` on `app/build.gradle`
3. Change the `versionCode` and `versionName` on `amethyst/build.gradle`
4. Commit and push.
5. Tag the commit with `v{x.x.x}`
6. Let the [Create Release GitHub Action](https://github.com/vitorpamplona/amethyst/actions/workflows/create-release.yml) build a new `aab` file.
@@ -249,10 +251,84 @@ dependencyResolutionManagement {
}
```
Add the dependency
Add the dependency by using one of the versions [here](https://jitpack.io/#vitorpamplona/amethyst/)
```gradle
implementation('com.github.vitorpamplona.amethyst:quartz:v0.85.1')
implementation('com.github.vitorpamplona.amethyst:quartz:<tag, commit, -SNAPSHOT>')
```
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 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
the `NostrSignerInternal` and `NostrSignerExternal` classes.
```kt
val signer = NostrSignerInternal(keyPair)
val amberSigner = NostrSignerExternal(
pubKey = keyPair.pubKey.toHexKey(),
packageName = signerPackageName,
contentResolver = appContext.contentResolver,
)
```
Create a single NostrClient for the entire application and control which relays it will access by
registering subscriptions and sending events. The pool will automatically changed based on filters +
outbox events.
You will need a coroutine scope to process events and if you are using OKHttp, we offer a basic
wrapper to create the socket connections themselves.
```kt
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
val rootClient = OkHttpClient.Builder().build()
val socketBuilder = BasicOkHttpWebSocket.Builder { url -> rootClient }
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)
client.sendIfExists(authedEvent, relay.url)
}
```
To manage subscriptions, the suggested approach is to use subscriptions in the Application class.
```kt
val metadataSub = RelayClientSubscription(
client = client,
filter = {
val filters = listOf(
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(signer.pubkey)
)
)
val signerOutboxRelays = listOfNotNull(
RelayUrlNormalizer.normalizeOrNull("wss://relay1.com"),
RelayUrlNormalizer.normalizeOrNull("wss://relay2.com")
)
signerOutboxRelays.associateWith { filters }
}
) { event ->
/* consume event */
}
```
## Contributing

363
amethyst/build.gradle Normal file
View File

@@ -0,0 +1,363 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.googleServices)
alias(libs.plugins.jetbrainsComposeCompiler)
alias(libs.plugins.serialization)
}
def getCurrentBranch() {
try {
def branch = 'git rev-parse --abbrev-ref HEAD'.execute().text.trim()
return branch
} catch (Exception e) {
println "Could not determine git branch: ${e.message}"
return "unknown"
}
}
def generateVersionName(String baseVersion) {
def currentBranch = getCurrentBranch()
if (currentBranch == "main" || currentBranch == "master" || currentBranch == "unknown" || currentBranch == "HEAD") {
return baseVersion
} else {
// Clean branch name for version (replace special characters)
def cleanBranch = currentBranch.replaceAll(/[^a-zA-Z0-9\-_]/, "-")
// Limit branch name to maximum 20 characters
if (cleanBranch.length() > 20) {
cleanBranch = cleanBranch.substring(0, 20)
}
return "${baseVersion}-${cleanBranch}"
}
}
android {
namespace = 'com.vitorpamplona.amethyst'
compileSdk = libs.versions.android.compileSdk.get().toInteger()
defaultConfig {
applicationId = "com.vitorpamplona.amethyst"
minSdk = libs.versions.android.minSdk.get().toInteger()
targetSdk = libs.versions.android.targetSdk.get().toInteger()
versionCode = 425
versionName = generateVersionName("1.01.0")
buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
resourceConfigurations += [
'ar',
'ar-rSA',
'bn-rBD',
'cs',
'cs-rCZ',
'cy-rGB',
'da-rDK',
'de',
'de-rDE',
'el-rGR',
'en-rGB',
'eo',
'eo-rUY',
'es',
'es-rES',
'es-rMX',
'es-rUS',
'et-rEE',
'fa',
'fa-rIR',
'fi-rFI',
'fo-rFO',
'fr',
'fr-rCA',
'fr-rFR',
'gu-rIN',
'hi-rIN',
'hr-rHR',
'hu',
'hu-rHU',
'in',
'in-rID',
'it-rIT',
'iw-rIL',
'ja',
'ja-rJP',
'kk-rKZ',
'ko-rKR',
'ks-rIN',
'ku-rTR',
'lt-rLT',
'ne-rNP',
'nl',
'nl-rBE',
'nl-rNL',
'pcm-rNG',
'pl-rPL',
'pt-rBR',
'pt-rPT',
'ru',
'ru-rRU',
'ru-rUA',
'sa-rIN',
'sl-rSI',
'so-rSO',
'sr-rSP',
'ss-rZA',
'sv-rSE',
'sw-rKE',
'sw-rTZ',
'ta',
'ta-rIN',
'th',
'th-rTH',
'tr',
'tr-rTR',
'uk',
'uk-rUA',
'ur-rIN',
'uz-rUZ',
'vi-rVN',
'zh',
'zh-rCN',
'zh-rHK',
'zh-rSG',
'zh-rTW'
]
}
buildTypes {
release {
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), 'proguard-rules.pro'
minifyEnabled = true
}
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
resValue "string", "app_name", "@string/app_name_debug"
}
create("benchmark") {
initWith(getByName("release"))
applicationIdSuffix '.benchmark'
versionNameSuffix '-BENCHMARK'
resValue "string", "app_name", "@string/app_name_benchmark"
profileable = true
signingConfig = signingConfigs.debug
}
}
flavorDimensions = ["channel"]
productFlavors {
play {
getIsDefault().set(true)
dimension "channel"
}
fdroid {
dimension "channel"
}
}
splits {
abi {
enable = true
reset()
include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
universalApk = true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
buildFeatures {
compose = true
buildConfig = true
}
packagingOptions {
resources {
excludes += ['/META-INF/{AL2.0,LGPL2.1}', '**/libscrypt.dylib']
}
}
lint {
disable 'MissingTranslation'
}
testOptions {
unitTests.returnDefaultValues = true
}
}
// TODO: until google merges and unifiedpush updates https://github.com/tink-crypto/tink-java-apps/pull/5
configurations.all {
def tink = "com.google.crypto.tink:tink-android:1.17.0"
resolutionStrategy {
force(tink)
dependencySubstitution {
substitute module('com.google.crypto.tink:tink') using module(tink)
}
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_21
}
}
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
dependencies {
implementation platform(libs.androidx.compose.bom)
implementation project(path: ':quartz')
implementation project(path: ':commons')
implementation project(path: ':ammolite')
implementation libs.androidx.core.ktx
implementation libs.androidx.activity.compose
implementation libs.androidx.ui
implementation libs.androidx.ui.graphics
implementation libs.androidx.ui.tooling.preview
// Needs this to open gallery / image upload
implementation libs.androidx.fragment.ktx
// Navigation
implementation libs.androidx.navigation.compose
// Material 3 Design
implementation libs.androidx.material3
implementation libs.androidx.material.icons
// Adaptive Layout / Two Pane
implementation libs.androidx.material3.windowSize
implementation libs.accompanist.adaptive
// Lifecycle
implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.lifecycle.runtime.compose
implementation libs.androidx.lifecycle.viewmodel.compose
// Zoomable images
implementation libs.zoomable
// Biometrics
implementation libs.androidx.biometric.ktx
// Websockets API
implementation libs.okhttp
implementation libs.okhttpCoroutines
// Encrypted Key Storage
implementation libs.androidx.security.crypto.ktx
implementation libs.androidx.datastore.preferences
// view videos
implementation libs.androidx.media3.exoplayer
implementation libs.androidx.media3.exoplayer.hls
implementation libs.androidx.media3.ui
implementation libs.androidx.media3.session
// important for proxy / tor
implementation libs.androidx.media3.datasource.okhttp
// Load images from the web.
implementation libs.coil.compose
// view gifs
implementation libs.coil.gif
// view svgs
implementation libs.coil.svg
// enables network for coil
implementation libs.coil.okhttp
// Permission to upload pictures:
implementation libs.accompanist.permissions
// For QR generation
implementation libs.zxing
implementation libs.zxing.embedded
// Markdown
//implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
//implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
//implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
// Markdown (With fix for full-image bleeds)
implementation libs.markdown.ui
implementation libs.markdown.ui.material3
implementation libs.markdown.commonmark
// Language picker and Theme chooser
implementation libs.androidx.appcompat
implementation libs.androidx.window.core.android
// Local model for language identification
playImplementation libs.google.mlkit.language.id
// Google services model the translate text
playImplementation libs.google.mlkit.translate
// PushNotifications
playImplementation platform(libs.firebase.bom)
playImplementation libs.firebase.messaging
//PushNotifications(FDroid)
fdroidImplementation libs.unifiedpush
// Charts
implementation libs.vico.charts.core
implementation libs.vico.charts.compose
implementation libs.vico.charts.views
implementation libs.vico.charts.m3
// GeoHash
implementation libs.drfonfon.geohash
// Waveform visualizer
implementation libs.audiowaveform
// Video compression lib
implementation libs.abedElazizShe.image.compressor
// Image compression lib
implementation libs.zelory.video.compressor
// Cbor for cashuB format
implementation libs.kotlinx.serialization.cbor
implementation libs.tor.android
implementation libs.jtorctl
testImplementation libs.junit
testImplementation libs.mockk
testImplementation libs.kotlinx.coroutines.test
androidTestImplementation platform(libs.androidx.compose.bom)
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.junit.ktx
androidTestImplementation libs.androidx.espresso.core
debugImplementation platform(libs.androidx.compose.bom)
debugImplementation libs.androidx.ui.tooling
debugImplementation libs.androidx.ui.test.manifest
implementation libs.androidx.camera.core
implementation libs.androidx.camera.camera2
implementation libs.androidx.camera.lifecycle
implementation libs.androidx.camera.view
implementation libs.androidx.camera.extensions
}

View File

@@ -0,0 +1,97 @@
{
"project_info": {
"project_number": "768341258853",
"project_id": "amethyst-3057a",
"storage_bucket": "amethyst-3057a.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:768341258853:android:5d07c35a37b24ff36b8c8c",
"android_client_info": {
"package_name": "com.vitorpamplona.amethyst"
}
},
"oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:768341258853:android:3270514bee61de546b8c8c",
"android_client_info": {
"package_name": "com.vitorpamplona.amethyst.benchmark"
}
},
"oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:768341258853:android:e0200680f552484d6b8c8c",
"android_client_info": {
"package_name": "com.vitorpamplona.amethyst.debug"
}
},
"oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

65
amethyst/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,65 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# preserve the line number information for debugging stack traces.
-dontobfuscate
-keepattributes LocalVariableTable
-keepattributes LocalVariableTypeTable
-keepattributes *Annotation*
-keepattributes SourceFile
-keepattributes LineNumberTable
-keepattributes Signature
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes EnclosingMethod
-keepattributes MethodParameters
-keepparameternames
-keepdirectories libs
# Keep all names
-keepnames class ** { *; }
# Keep All enums
-keep enum ** { *; }
# preserve access to native classses
-keep class fr.acinq.secp256k1.** { *; }
# JNA For Libsodium
-keep class com.goterl.lazysodium.** { *; }
# libscrypt
-keep class com.lambdaworks.codec.** { *; }
-keep class com.lambdaworks.crypto.** { *; }
-keep class com.lambdaworks.jni.** { *; }
-keep class info.guardianproject.** { *; }
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
-keep class com.sun.jna.ToNativeConverter { *; }
-keep class com.sun.jna.NativeMapped { *; }
-keep class com.sun.jna.CallbackReference { *; }
-keep class com.sun.jna.ptr.IntByReference { *; }
-keep class com.sun.jna.NativeLong { *; }
-keep class com.sun.jna.Structure { *; }
-keep class com.sun.jna.Structure$* { *; }
-keep class com.sun.jna.Native$ffi_callback { *; }
-keep class * implements com.sun.jna.Structure$* { *; }
-keep class * implements com.sun.jna.Native$* { *; }
-keep class com.sun.jna.Native {
private static com.sun.jna.NativeMapped fromNative(java.lang.Class, java.lang.Object);
private static com.sun.jna.NativeMapped fromNative(java.lang.reflect.Method, java.lang.Object);
private static java.lang.Class nativeType(java.lang.Class);
private static java.lang.Object toNative(com.sun.jna.ToNativeConverter, java.lang.Object);
private static java.lang.Object fromNative(com.sun.jna.FromNativeConverter, java.lang.Object, java.lang.reflect.Method);
}
# JSON parsing
-keep class com.vitorpamplona.quartz.** { *; }
-keep class com.vitorpamplona.amethyst.** { *; }
-keep class com.vitorpamplona.ammolite.** { *; }

View File

@@ -0,0 +1,102 @@
/**
* 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 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.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CashuBTest {
val cashuTokenA = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9"
val cashuTokenB1 = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA"
val cashuTokenB2 = "cashuBpGFkb01pbmliaXRzIHJ1bGVzIWFteEpodHRwOi8vbGJ1dGxoNWxmZ2dxNXI3eHBpd2hyYWpkbDdzeHB1cGdhZ2F6eGw2NXc0YzVjZzcyd3RvZmFzYWQub25pb246MzMzOGF0gaJhaUgAm7I9OpEuTmFwg6NhYRhAYWNYIQPfWR0mG80XbGnj6DO8q1NIyjHSGGIEkoWTA6H16HTpx2FzeEA3YThkY2Y5YjNlOGEyNDdjZTMzOWU3MzY5ZTliNGExOWYzMWVhY2I2OWQ4YjBjNjVkYWFlYjcyZDFhY2I5YWQzo2FhGCBhY1ghA55SwCFBc46dwnjbkb87Mzo30T2EE9Ws_nemuFneDegGYXN4QDlkODFjMWEyNjE2ODUzYWQ4MDQ5Y2JjZDFjN2MyNDdhZGQ4M2IzNzM4Mjg2MjBiYWMyZmQ3ZjNlNWE1OGFjZWKjYWEEYWNYIQM-yAQQTR2t6pIAmfmGM8Wxy7ajKVLOaUg7TrV8o-EdVWFzeEBmNmViMTI4ZmJlMDM3MTEzZTkzZjM3NjllYTYwMTk1NmY1N2NkZWNhNTYwOGY0NWUzMDhhZDU0ZmQ4YTQxNWVhYXVjc2F0"
@Test()
fun parseCashuA() {
runBlocking {
val parsed = (CashuParser().parse(cashuTokenA) as GenericLoadable.Loaded<List<CashuToken>>).loaded[0]
assertEquals(cashuTokenA, parsed.token)
assertEquals("https://8333.space:3338", parsed.mint)
assertEquals(10, parsed.totalAmount)
assertEquals(2, parsed.proofs[0].amount)
assertEquals("407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837", parsed.proofs[0].secret)
assertEquals("009a1f293253e41e", parsed.proofs[0].id)
assertEquals("02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea", parsed.proofs[0].C)
assertEquals(8, parsed.proofs[1].amount)
assertEquals("fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be", parsed.proofs[1].secret)
assertEquals("009a1f293253e41e", parsed.proofs[1].id)
assertEquals("029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059", parsed.proofs[1].C)
}
}
@Test()
fun parseCashuB() =
runBlocking {
val parsed = (CashuParser().parse(cashuTokenB1) as GenericLoadable.Loaded<List<CashuToken>>).loaded
assertEquals(cashuTokenB1, parsed[0].token)
assertEquals("http://localhost:3338", parsed[0].mint)
assertEquals(1, parsed[0].totalAmount)
assertEquals(1, parsed[0].proofs[0].amount)
assertEquals("acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388", parsed[0].proofs[0].secret)
assertEquals("00ffd48b8f5ecf80", parsed[0].proofs[0].id)
assertEquals("0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf", parsed[0].proofs[0].C)
assertEquals(3, parsed[1].totalAmount)
assertEquals(2, parsed[1].proofs[0].amount)
assertEquals("1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee", parsed[1].proofs[0].secret)
assertEquals("00ad268c4d1f5826", parsed[1].proofs[0].id)
assertEquals("023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d", parsed[1].proofs[0].C)
assertEquals(1, parsed[1].proofs[1].amount)
assertEquals("56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57", parsed[1].proofs[1].secret)
assertEquals("00ad268c4d1f5826", parsed[1].proofs[1].id)
assertEquals("0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63", parsed[1].proofs[1].C)
}
@Test()
fun parseCashuB2() =
runBlocking {
val parsed = (CashuParser().parse(cashuTokenB2) as GenericLoadable.Loaded<List<CashuToken>>).loaded
assertEquals(cashuTokenB2, parsed[0].token)
assertEquals("http://lbutlh5lfggq5r7xpiwhrajdl7sxpupgagazxl65w4c5cg72wtofasad.onion:3338", parsed[0].mint)
assertEquals(100, parsed[0].totalAmount)
assertEquals(64, parsed[0].proofs[0].amount)
assertEquals("7a8dcf9b3e8a247ce339e7369e9b4a19f31eacb69d8b0c65daaeb72d1acb9ad3", parsed[0].proofs[0].secret)
assertEquals("009bb23d3a912e4e", parsed[0].proofs[0].id)
assertEquals("03df591d261bcd176c69e3e833bcab5348ca31d218620492859303a1f5e874e9c7", parsed[0].proofs[0].C)
assertEquals(32, parsed[0].proofs[1].amount)
assertEquals("9d81c1a2616853ad8049cbcd1c7c247add83b373828620bac2fd7f3e5a58aceb", parsed[0].proofs[1].secret)
assertEquals("009bb23d3a912e4e", parsed[0].proofs[1].id)
assertEquals("039e52c02141738e9dc278db91bf3b333a37d13d8413d5acfe77a6b859de0de806", parsed[0].proofs[1].C)
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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 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 okhttp3.OkHttpClient
import okhttp3.Request
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DMFileDecryptionTest {
// Key cache to download and decrypt encrypted files before caching them.
val keyCache = EncryptionKeyCache()
val url = "https://cdn.satellite.earth/812fd4cf9d4d4b59c141ecd6a6c08c7571b5872237ad6477916cb2d119b5cacd"
val cipher =
AESGCM(
"7b184b3849e161027bab1aac9f2d96eb22bfe02c8dc34cad5d2cc436a64f191d".hexToByteArray(),
"3936923383652e3f0d72bfd40568b435".hexToByteArray(),
)
val decryptedSize = 1277122
val expectedMimeType = "video/mp4"
@Test
fun runDownloadAndDecryptVideo() {
val client =
OkHttpClient
.Builder()
.addNetworkInterceptor(EncryptedBlobInterceptor(keyCache))
.build()
keyCache.add(url, cipher, "video/mp4")
val request =
Request
.Builder()
.url(url)
.get()
.build()
client.newCall(request).execute().use {
assertEquals(decryptedSize, it.body.bytes().size)
assertEquals(expectedMimeType, it.body.contentType().toString())
}
}
}

View File

@@ -0,0 +1,296 @@
/**
* 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.graphics.Bitmap
import android.graphics.Color
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor
import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.service.uploads.ImageDownloader
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
import com.vitorpamplona.amethyst.service.uploads.nip96.ServerInfoRetriever
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
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
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import org.junit.Assert
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
@RunWith(AndroidJUnit4::class)
class ImageUploadTesting {
companion object {
val accountSettings = AccountSettings(KeyPair())
val signer = NostrSignerInternal(accountSettings.keyPair)
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val cache = LocalCache
val blossomServerListState =
BlossomServerListState(
signer = signer,
cache = cache,
scope = scope,
settings = accountSettings,
)
}
val client =
OkHttpClient
.Builder()
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(DefaultContentTypeInterceptor("Amethyst/${BuildConfig.VERSION_NAME}"))
.build()
private suspend fun getBitmap(): ByteArray {
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
for (x in 0 until bitmap.width) {
for (y in 0 until bitmap.height) {
bitmap.setPixel(x, y, Color.rgb(Random.nextInt(), Random.nextInt(), Random.nextInt()))
}
}
val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos)
return baos.toByteArray()
}
private suspend fun testBase(server: ServerName) {
if (server.type == ServerType.NIP96) {
testNip96(server)
} else {
testBlossom(server)
}
}
private suspend fun testBlossom(server: ServerName) {
val paylod = getBitmap()
val initialHash = sha256(paylod).toHexKey()
val inputStream = paylod.inputStream()
val result =
BlossomUploader()
.upload(
inputStream = inputStream,
hash = initialHash,
length = paylod.size,
baseFileName = "filename.png",
contentType = "image/png",
alt = null,
sensitiveContent = null,
serverBaseUrl = server.baseUrl,
okHttpClient = { client },
httpAuth = blossomServerListState::createBlossomUploadAuth,
context = InstrumentationRegistry.getInstrumentation().targetContext,
)
assertEquals("image/png", result.type)
assertEquals(paylod.size.toLong(), result.size)
assertEquals(initialHash, result.sha256)
assertEquals("${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png"))
val imageData: ByteArray =
ImageDownloader().waitAndGetImage(result.url!!, { client })?.bytes
?: run {
fail("${server.name}: Should not be null")
return
}
val downloadedHash = sha256(imageData).toHexKey()
assertEquals(initialHash, downloadedHash)
}
private suspend fun testNip96(server: ServerName) {
val serverInfo =
ServerInfoRetriever()
.loadInfo(
server.baseUrl,
{ client },
)
val payload = getBitmap()
val inputStream = payload.inputStream()
val result =
Nip96Uploader()
.upload(
inputStream = inputStream,
length = payload.size.toLong(),
contentType = "image/png",
alt = null,
sensitiveContent = null,
server = serverInfo,
okHttpClient = { client },
onProgress = {},
httpAuth = { url, method, body ->
signer.sign(HTTPAuthorizationEvent.build(url, method, body))
},
context = InstrumentationRegistry.getInstrumentation().targetContext,
)
val url = result.url!!
val size = result.size?.toInt()
val dim = result.dimension
val hash = result.sha256
Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http"))
val imageData: ByteArray =
ImageDownloader().waitAndGetImage(url, { client })?.bytes
?: run {
fail("${server.name}: Should not be null")
return
}
val prepared =
FileHeader.prepare(
imageData,
"image/png",
null,
)
prepared.fold(
onSuccess = {
if (dim != null) {
assertEquals("${server.name}: Invalid dimensions", it.dim.toString(), dim.toString())
}
if (size != null) {
assertEquals("${server.name}: Invalid size", it.size, size)
}
},
onFailure = { fail("${server.name}: It should not fail") },
)
// delay(1000)
// assertTrue(Nip96Uploader(account).delete(ox, contentType, serverInfo))
}
@Test
fun runTestOnDefaultServers() =
runBlocking {
DEFAULT_MEDIA_SERVERS.forEach {
// skip paid servers and primal server is buggy.
if (!it.name.contains("Paid") && !it.name.contains("Primal")) {
testBase(it)
}
}
}
@Test()
fun testNostrCheck() =
runBlocking {
testBase(ServerName("nostrcheck.me", "https://nostrcheck.me", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testNostrage() =
runBlocking {
testBase(ServerName("nostrage", "https://nostrage.com", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testSove() =
runBlocking {
testBase(ServerName("sove", "https://sove.rent", ServerType.NIP96))
}
@Ignore("Not Working anymore")
@Test()
fun testNostrBuild() =
runBlocking {
testBase(ServerName("nostr.build", "https://nostr.build", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testSovbit() =
runBlocking {
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() =
runBlocking {
testBase(ServerName("nostpic.com", "https://nostpic.com", ServerType.NIP96))
}
@Test(expected = RuntimeException::class)
fun testSprovoostNl() =
runBlocking {
testBase(ServerName("sprovoost.nl", "https://img.sprovoost.nl/", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testNostrOnch() =
runBlocking {
testBase(ServerName("nostr.onch.services", "https://nostr.onch.services", ServerType.NIP96))
}
@Ignore("Changes sha256")
fun testPrimalBlossom() =
runBlocking {
testBase(ServerName("primal.net", "https://blossom.primal.net", ServerType.Blossom))
}
@Test()
@Ignore("Not Working anymore/ Timeout")
fun testNostrCheckBlossom() =
runBlocking {
testBase(ServerName("nostrcheck", "https://cdn.nostrcheck.me", ServerType.Blossom))
}
@Ignore("Requires Payment")
fun testSatelliteBlossom() =
runBlocking {
testBase(ServerName("satellite", "https://cdn.satellite.earth", ServerType.Blossom))
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -21,20 +21,19 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpBitcoinExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.ots.OpenTimestamps
import com.vitorpamplona.quartz.signers.NostrSignerInternal
import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache
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 junit.framework.TestCase.assertEquals
import org.junit.Assert
import org.junit.Before
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class OkHttpOtsTest {
@@ -43,53 +42,54 @@ class OkHttpOtsTest {
val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}"
val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6"
val otsCache = OtsBlockHeightCache()
@Before
fun setup() {
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
}
val resolver =
OtsResolver(
OkHttpBitcoinExplorer(
baseAPI = OkHttpBitcoinExplorer.MEMPOOL_API_URL,
client = OkHttpClient.Builder().build(),
cache = otsCache,
),
OkHttpCalendarBuilder { OkHttpClient.Builder().build() },
)
@Test
fun verifyNostrEvent() {
val ots = Event.fromJson(otsEvent) as OtsEvent
println(ots.info())
assertEquals(1707688818L, ots.verify())
val ots = JsonMapper.fromJson(otsEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1707688818L, ots.verify(resolver))
}
@Test
fun verifyNostrEvent2() {
val ots = Event.fromJson(otsEvent2) as OtsEvent
println(ots.info())
assertEquals(1706322179L, ots.verify())
val ots = JsonMapper.fromJson(otsEvent2) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1706322179L, ots.verify(resolver))
}
@Test
fun verifyNostrPendingEvent() {
val ots = Event.fromJson(otsPendingEvent) as OtsEvent
println(ots.info())
assertEquals(null, ots.verify())
val ots = JsonMapper.fromJson(otsPendingEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(null, ots.verify(resolver))
}
@Test
fun createOTSEventAndVerify() {
val signer = NostrSignerInternal(KeyPair())
var ots: OtsEvent? = null
val countDownLatch = CountDownLatch(1)
val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver)
val otsFile = OtsEvent.stamp(otsEvent2Digest)
val ots =
runBlocking {
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile))
}
OtsEvent.create(otsEvent2Digest, otsFile, signer) {
ots = it
countDownLatch.countDown()
}
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
println(ots!!.toJson())
println(ots!!.info())
println(ots.toJson())
println(resolver.info(ots.otsByteArray()))
// Should not be valid because we need to wait for confirmations
assertEquals(null, ots!!.verify())
assertEquals(null, ots.verify(resolver))
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,62 @@
/**
* 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 com.vitorpamplona.amethyst.ui.navigation.findParameterValue
import junit.framework.TestCase.assertEquals
import org.junit.Test
import java.net.URI
class URIParserTest {
@Test
fun testNEventWithAccount() {
val test = "nevent1qqsp9wg5r3vkuv5al0459h3h44yxh6r0y7chgvu3pkdxhcj99t3msrgpzdmhxue69uhhwmm59e6hg7r09ehkuef0qgsdhcrqt2w8x9et446j8ge8kgmd2h4ykc6wsrnc4yqnmdu3lr74ktqrqsqqqqqp578kku?account=npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"
val testUri = URI(test)
// assertEquals("nostr", testUri.scheme)
assertEquals(null, testUri.authority)
assertEquals(null, testUri.host)
assertEquals(-1, testUri.port)
assertEquals(null, testUri.userInfo)
assertEquals("nevent1qqsp9wg5r3vkuv5al0459h3h44yxh6r0y7chgvu3pkdxhcj99t3msrgpzdmhxue69uhhwmm59e6hg7r09ehkuef0qgsdhcrqt2w8x9et446j8ge8kgmd2h4ykc6wsrnc4yqnmdu3lr74ktqrqsqqqqqp578kku", testUri.path)
assertEquals(null, testUri.fragment)
assertEquals("account=npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z", testUri.rawQuery)
assertEquals("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z", testUri.findParameterValue("account"))
}
@Test
fun testNotificationsWithAccount() {
val test = "notifications?account=npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"
val testUri = URI(test)
// assertEquals("nostr", testUri.scheme)
assertEquals(null, testUri.authority)
assertEquals(null, testUri.host)
assertEquals(-1, testUri.port)
assertEquals(null, testUri.userInfo)
assertEquals("notifications", testUri.path)
assertEquals(null, testUri.fragment)
assertEquals("account=npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z", testUri.rawQuery)
assertEquals("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z", testUri.findParameterValue("account"))
}
}

View File

@@ -0,0 +1,262 @@
/**
* 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 androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.TransformedText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class UrlUserTagTransformationTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.vitorpamplona.amethyst", appContext.packageName.removeSuffix(".debug"))
}
fun debugCursor(
original: String,
transformedText: TransformedText,
offset: Int,
): String {
val offsetTransformed = transformedText.offsetMapping.originalToTransformed(offset)
val originalWithCursor = original.substring(0, offset) + "|" + original.substring(offset, original.length)
val transformedWithCursor = transformedText.text.text.substring(0, offsetTransformed) + "|" + transformedText.text.text.substring(offsetTransformed, transformedText.text.text.length)
return "$originalWithCursor $transformedWithCursor"
}
fun debugCursorReverse(
original: String,
transformedText: TransformedText,
offsetTransformed: Int,
): String {
val offset = transformedText.offsetMapping.transformedToOriginal(offsetTransformed)
val originalWithCursor = original.substring(0, offset) + "|" + original.substring(offset, original.length)
val transformedWithCursor = transformedText.text.text.substring(0, offsetTransformed) + "|" + transformedText.text.text.substring(offsetTransformed, transformedText.text.text.length)
return "$originalWithCursor $transformedWithCursor"
}
@Test
fun testKeepTransformedIndexFullyInsideTransformedText() {
val user =
LocalCache.getOrCreateUser(
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.info = UserMetadata()
user.info?.displayName = "Vitor Pamplona"
val original = "@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"
val transformedText =
buildAnnotatedStringWithUrlHighlighting(
AnnotatedString(original),
Color.Red,
)
val expected = "@Vitor Pamplona"
assertEquals(expected, transformedText.text.text)
assertEquals("|@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z |@Vitor Pamplona", debugCursor(original, transformedText, 0))
assertEquals("@|npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z |@Vitor Pamplona", debugCursor(original, transformedText, 1))
assertEquals("@n|pub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z |@Vitor Pamplona", debugCursor(original, transformedText, 2))
assertEquals("@np|ub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z |@Vitor Pamplona", debugCursor(original, transformedText, 3))
assertEquals("@npu|b1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z |@Vitor Pamplona", debugCursor(original, transformedText, 4))
assertEquals("@npub|1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @|Vitor Pamplona", debugCursor(original, transformedText, 5))
assertEquals("@npub1|gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @|Vitor Pamplona", debugCursor(original, transformedText, 6))
assertEquals("@npub1g|cxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @|Vitor Pamplona", debugCursor(original, transformedText, 7))
assertEquals("@npub1gc|xzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @|Vitor Pamplona", debugCursor(original, transformedText, 8))
assertEquals("@npub1gcx|zte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @V|itor Pamplona", debugCursor(original, transformedText, 9))
assertEquals("@npub1gcxz|te5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @V|itor Pamplona", debugCursor(original, transformedText, 10))
assertEquals("@npub1gcxzt|e5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @V|itor Pamplona", debugCursor(original, transformedText, 11))
assertEquals("@npub1gcxzte|5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @V|itor Pamplona", debugCursor(original, transformedText, 12))
assertEquals("@npub1gcxzte5|zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vi|tor Pamplona", debugCursor(original, transformedText, 13))
assertEquals("@npub1gcxzte5z|lkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vi|tor Pamplona", debugCursor(original, transformedText, 14))
assertEquals("@npub1gcxzte5zl|kncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vi|tor Pamplona", debugCursor(original, transformedText, 15))
assertEquals("@npub1gcxzte5zlk|ncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vi|tor Pamplona", debugCursor(original, transformedText, 16))
assertEquals("@npub1gcxzte5zlkn|cx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vi|tor Pamplona", debugCursor(original, transformedText, 17))
assertEquals("@npub1gcxzte5zlknc|x26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vit|or Pamplona", debugCursor(original, transformedText, 18))
assertEquals("@npub1gcxzte5zlkncx|26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vit|or Pamplona", debugCursor(original, transformedText, 19))
assertEquals("@npub1gcxzte5zlkncx2|6j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vit|or Pamplona", debugCursor(original, transformedText, 20))
assertEquals("@npub1gcxzte5zlkncx26|j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vit|or Pamplona", debugCursor(original, transformedText, 21))
assertEquals("@npub1gcxzte5zlkncx26j|68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vito|r Pamplona", debugCursor(original, transformedText, 22))
assertEquals("@npub1gcxzte5zlkncx26j6|8ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vito|r Pamplona", debugCursor(original, transformedText, 23))
assertEquals("@npub1gcxzte5zlkncx26j68|ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vito|r Pamplona", debugCursor(original, transformedText, 24))
assertEquals("@npub1gcxzte5zlkncx26j68e|z60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vito|r Pamplona", debugCursor(original, transformedText, 25))
assertEquals("@npub1gcxzte5zlkncx26j68ez|60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor| Pamplona", debugCursor(original, transformedText, 26))
assertEquals("@npub1gcxzte5zlkncx26j68ez6|0fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor| Pamplona", debugCursor(original, transformedText, 27))
assertEquals("@npub1gcxzte5zlkncx26j68ez60|fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor| Pamplona", debugCursor(original, transformedText, 28))
assertEquals("@npub1gcxzte5zlkncx26j68ez60f|zkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor| Pamplona", debugCursor(original, transformedText, 29))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fz|kvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor |Pamplona", debugCursor(original, transformedText, 30))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzk|vtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor |Pamplona", debugCursor(original, transformedText, 31))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkv|tkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor |Pamplona", debugCursor(original, transformedText, 32))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvt|km9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor |Pamplona", debugCursor(original, transformedText, 33))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtk|m9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor |Pamplona", debugCursor(original, transformedText, 34))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm|9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor P|amplona", debugCursor(original, transformedText, 35))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9|e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor P|amplona", debugCursor(original, transformedText, 36))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e|0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor P|amplona", debugCursor(original, transformedText, 37))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0|vrwdcvsjakxf9mu9qewqlfnj5z @Vitor P|amplona", debugCursor(original, transformedText, 38))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0v|rwdcvsjakxf9mu9qewqlfnj5z @Vitor Pa|mplona", debugCursor(original, transformedText, 39))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vr|wdcvsjakxf9mu9qewqlfnj5z @Vitor Pa|mplona", debugCursor(original, transformedText, 40))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrw|dcvsjakxf9mu9qewqlfnj5z @Vitor Pa|mplona", debugCursor(original, transformedText, 41))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwd|cvsjakxf9mu9qewqlfnj5z @Vitor Pa|mplona", debugCursor(original, transformedText, 42))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdc|vsjakxf9mu9qewqlfnj5z @Vitor Pam|plona", debugCursor(original, transformedText, 43))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcv|sjakxf9mu9qewqlfnj5z @Vitor Pam|plona", debugCursor(original, transformedText, 44))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvs|jakxf9mu9qewqlfnj5z @Vitor Pam|plona", debugCursor(original, transformedText, 45))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsj|akxf9mu9qewqlfnj5z @Vitor Pam|plona", debugCursor(original, transformedText, 46))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsja|kxf9mu9qewqlfnj5z @Vitor Pamp|lona", debugCursor(original, transformedText, 47))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjak|xf9mu9qewqlfnj5z @Vitor Pamp|lona", debugCursor(original, transformedText, 48))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakx|f9mu9qewqlfnj5z @Vitor Pamp|lona", debugCursor(original, transformedText, 49))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf|9mu9qewqlfnj5z @Vitor Pamp|lona", debugCursor(original, transformedText, 50))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9|mu9qewqlfnj5z @Vitor Pamp|lona", debugCursor(original, transformedText, 51))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9m|u9qewqlfnj5z @Vitor Pampl|ona", debugCursor(original, transformedText, 52))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu|9qewqlfnj5z @Vitor Pampl|ona", debugCursor(original, transformedText, 53))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9|qewqlfnj5z @Vitor Pampl|ona", debugCursor(original, transformedText, 54))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9q|ewqlfnj5z @Vitor Pampl|ona", debugCursor(original, transformedText, 55))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qe|wqlfnj5z @Vitor Pamplo|na", debugCursor(original, transformedText, 56))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qew|qlfnj5z @Vitor Pamplo|na", debugCursor(original, transformedText, 57))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewq|lfnj5z @Vitor Pamplo|na", debugCursor(original, transformedText, 58))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewql|fnj5z @Vitor Pamplo|na", debugCursor(original, transformedText, 59))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlf|nj5z @Vitor Pamplon|a", debugCursor(original, transformedText, 60))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfn|j5z @Vitor Pamplon|a", debugCursor(original, transformedText, 61))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj|5z @Vitor Pamplon|a", debugCursor(original, transformedText, 62))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5|z @Vitor Pamplon|a", debugCursor(original, transformedText, 63))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z| @Vitor Pamplona|", debugCursor(original, transformedText, 64))
assertEquals("|@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z |@Vitor Pamplona", debugCursorReverse(original, transformedText, 0))
assertEquals("@npu|b1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @|Vitor Pamplona", debugCursorReverse(original, transformedText, 1))
assertEquals("@npub1gc|xzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @V|itor Pamplona", debugCursorReverse(original, transformedText, 2))
assertEquals("@npub1gcxzte|5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vi|tor Pamplona", debugCursorReverse(original, transformedText, 3))
assertEquals("@npub1gcxzte5zlkn|cx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vit|or Pamplona", debugCursorReverse(original, transformedText, 4))
assertEquals("@npub1gcxzte5zlkncx26|j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vito|r Pamplona", debugCursorReverse(original, transformedText, 5))
assertEquals("@npub1gcxzte5zlkncx26j68e|z60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor| Pamplona", debugCursorReverse(original, transformedText, 6))
assertEquals("@npub1gcxzte5zlkncx26j68ez60f|zkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor |Pamplona", debugCursorReverse(original, transformedText, 7))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtk|m9e0vrwdcvsjakxf9mu9qewqlfnj5z @Vitor P|amplona", debugCursorReverse(original, transformedText, 8))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0|vrwdcvsjakxf9mu9qewqlfnj5z @Vitor Pa|mplona", debugCursorReverse(original, transformedText, 9))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwd|cvsjakxf9mu9qewqlfnj5z @Vitor Pam|plona", debugCursorReverse(original, transformedText, 10))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsj|akxf9mu9qewqlfnj5z @Vitor Pamp|lona", debugCursorReverse(original, transformedText, 11))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9|mu9qewqlfnj5z @Vitor Pampl|ona", debugCursorReverse(original, transformedText, 12))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9q|ewqlfnj5z @Vitor Pamplo|na", debugCursorReverse(original, transformedText, 13))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewql|fnj5z @Vitor Pamplon|a", debugCursorReverse(original, transformedText, 14))
assertEquals("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z| @Vitor Pamplona|", debugCursorReverse(original, transformedText, 15))
assertEquals(0, transformedText.offsetMapping.originalToTransformed(0))
assertEquals(15, transformedText.offsetMapping.originalToTransformed(64))
}
@Test
fun transformationText() {
val user =
LocalCache.getOrCreateUser(
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.info = UserMetadata()
user.info?.displayName = "Vitor Pamplona"
val transformedText =
buildAnnotatedStringWithUrlHighlighting(
AnnotatedString("New Hey @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"),
Color.Red,
)
assertEquals("New Hey @Vitor Pamplona", transformedText.text.text)
assertEquals(0, transformedText.offsetMapping.originalToTransformed(0)) // Before N
assertEquals(4, transformedText.offsetMapping.originalToTransformed(4)) // Before H
assertEquals(8, transformedText.offsetMapping.originalToTransformed(8)) // Before @
assertEquals(8, transformedText.offsetMapping.originalToTransformed(9)) // Before n
assertEquals(8, transformedText.offsetMapping.originalToTransformed(10)) // Before p
assertEquals(8, transformedText.offsetMapping.originalToTransformed(11)) // Before u
assertEquals(8, transformedText.offsetMapping.originalToTransformed(12)) // Before b
assertEquals(9, transformedText.offsetMapping.originalToTransformed(13)) // Before 1
assertEquals(22, transformedText.offsetMapping.originalToTransformed(71))
assertEquals(23, transformedText.offsetMapping.originalToTransformed(72))
assertEquals(0, transformedText.offsetMapping.transformedToOriginal(0))
assertEquals(4, transformedText.offsetMapping.transformedToOriginal(4))
assertEquals(8, transformedText.offsetMapping.transformedToOriginal(8))
assertEquals(12, transformedText.offsetMapping.transformedToOriginal(9))
assertEquals(72, transformedText.offsetMapping.transformedToOriginal(23))
assertEquals(73, transformedText.offsetMapping.transformedToOriginal(24))
}
@Test
fun transformationTextTwoKeys() {
val user =
LocalCache.getOrCreateUser(
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.info = UserMetadata()
user.info?.displayName = "Vitor Pamplona"
val transformedText =
buildAnnotatedStringWithUrlHighlighting(
AnnotatedString(
"New Hey @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z and @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
),
Color.Red,
)
assertEquals("New Hey @Vitor Pamplona and @Vitor Pamplona", transformedText.text.text)
assertEquals(8, transformedText.offsetMapping.originalToTransformed(11))
assertEquals(8, transformedText.offsetMapping.originalToTransformed(12))
assertEquals(9, transformedText.offsetMapping.originalToTransformed(13))
assertEquals(22, transformedText.offsetMapping.originalToTransformed(70)) // Before 5
assertEquals(22, transformedText.offsetMapping.originalToTransformed(71)) // Before z
assertEquals(23, transformedText.offsetMapping.originalToTransformed(72)) // Before <space>
assertEquals(24, transformedText.offsetMapping.originalToTransformed(73)) // Before a
assertEquals(25, transformedText.offsetMapping.originalToTransformed(74)) // Before n
assertEquals(26, transformedText.offsetMapping.originalToTransformed(75)) // Before d
assertEquals(27, transformedText.offsetMapping.originalToTransformed(76)) // Before <space>
assertEquals(28, transformedText.offsetMapping.originalToTransformed(77)) // Before @
assertEquals(28, transformedText.offsetMapping.originalToTransformed(78)) // Before n
assertEquals(67, transformedText.offsetMapping.transformedToOriginal(22)) // Before a
assertEquals(72, transformedText.offsetMapping.transformedToOriginal(23)) // Before <space>
assertEquals(73, transformedText.offsetMapping.transformedToOriginal(24)) // Before a
assertEquals(74, transformedText.offsetMapping.transformedToOriginal(25)) // Before n
assertEquals(75, transformedText.offsetMapping.transformedToOriginal(26)) // Before d
assertEquals(76, transformedText.offsetMapping.transformedToOriginal(27)) // Before <space>
assertEquals(77, transformedText.offsetMapping.transformedToOriginal(28)) // Before @
}
}

View File

@@ -0,0 +1,5 @@
<resources>
<string name="app_name">Amethyst</string>
<string name="app_name_debug">Amy Debug</string>
<string name="app_name_benchmark">Amy Benchmark</string>
</resources>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -27,6 +27,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CancellationException
@RunWith(AndroidJUnit4::class)
class TranslationsTest {
@@ -124,7 +125,10 @@ class TranslationsTest {
"Have you seen this: lnbc12u1p3lvjeupp5a5ecgp45k6pa8tu7rnkgzfuwdy3l5ylv3k5tdzrg4cr8rj2f364sdq5g9kxy7fqd9h8vmmfvdjscqzpgxqyz5vqsp5zuzyetf33aphetf0e80w7tztw6dfsjs4lmvya4cyk8umfsx00qts9qyyssqke9hphcr36zvcav8wr502g0mhfhxpy8m9tt36zttg8vldm2qxw039ulccr8nwy3hjg2sw5vk65e99lwuhrhw0nuya2u57qszltvx7egp74jydn I think I have to pay",
"pt",
)
}
@Test(expected = CancellationException::class)
fun testTranslationLnInvoice2() {
assertTranslateContains(
"lnbc10u1p3l0wg0pp5y5y3vxt3429m28uuq56uqhwxadftn67yaarq06h3y9nqapz72n6sdqqxqyjw5q9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqsp5y2tazp42xde3c0tdsz30zqcekrt0lzrneszdtagy2qn7vs0d3p5qrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glcll7jdvcln4lhw5qqqqlgqqqqqeqqjqdau9jzseecmvmh03h88xyf5f980xx45fmn0cej654v5jr79ye36pww90jwdda38damlmgt54v8rn6q9kywtw057rh4v3wwrmn8fajagqnssr7v",
"Test lnbc10u1p3l0wg0pp5y5y3vxt3429m28uuq56uqhwxadftn67yaarq06h3y9nqapz72n6sdqqxqyjw5q9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqsp5y2tazp42xde3c0tdsz30zqcekrt0lzrneszdtagy2qn7vs0d3p5qrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glcll7jdvcln4lhw5qqqqlgqqqqqeqqjqdau9jzseecmvmh03h88xyf5f980xx45fmn0cej654v5jr79ye36pww90jwdda38damlmgt54v8rn6q9kywtw057rh4v3wwrmn8fajagqnssr7v",
@@ -132,7 +136,7 @@ class TranslationsTest {
)
}
@Test
@Test(expected = CancellationException::class)
fun testNostrEvents() {
assertTranslateContains(
"nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy",
@@ -162,4 +166,15 @@ class TranslationsTest {
"pt",
)
}
@Test
fun testHttp() {
val text = "https://m.primal.net/MdDd.png \nRunning... \uD83D\uDE01 nostr:npub126ntw5mnermmj0znhjhgdk8lh2af72sm8qfzq48umdlnhaj9kuns3le9ll nostr:npub1getal6ykt05fsz5nqu4uld09nfj3y3qxmv8crys4aeut53unfvlqr80nfm"
assertTranslateContains(
"https://m.primal.net/MdDd.png",
text,
"pt",
)
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".Amethyst">
<receiver
android:exported="true"
android:enabled="true"
android:name=".service.notifications.PushMessageReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED"/>
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,25 @@
/**
* 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.service.lang
object LanguageTranslatorService {
fun clear() {}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -38,13 +38,11 @@ interface PushDistributorActions {
}
object PushDistributorHandler : PushDistributorActions {
private val appContext = Amethyst.instance.applicationContext
private val unifiedPush: UnifiedPush = UnifiedPush
private var endpointInternal = ""
val endpoint = endpointInternal
fun getSavedEndpoint() = endpoint
fun getSavedEndpoint() = endpointInternal
fun setEndpoint(newEndpoint: String) {
endpointInternal = newEndpoint
@@ -55,15 +53,13 @@ object PushDistributorHandler : PushDistributorActions {
endpointInternal = ""
}
override fun getSavedDistributor(): String {
return unifiedPush.getSavedDistributor(appContext) ?: ""
}
fun appContext(): Context = Amethyst.instance.applicationContext
override fun getSavedDistributor(): String = unifiedPush.getSavedDistributor(appContext()) ?: ""
fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty()
override fun getInstalledDistributors(): List<String> {
return unifiedPush.getDistributors(appContext)
}
override fun getInstalledDistributors(): List<String> = unifiedPush.getDistributors(appContext())
fun formattedDistributorNames(): List<String> {
val distributorsArray = getInstalledDistributors().toTypedArray()
@@ -73,35 +69,34 @@ object PushDistributorHandler : PushDistributorActions {
try {
val ai =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
appContext.packageManager.getApplicationInfo(
appContext().packageManager.getApplicationInfo(
it,
PackageManager.ApplicationInfoFlags.of(
PackageManager.GET_META_DATA.toLong(),
),
)
} else {
appContext.packageManager.getApplicationInfo(it, 0)
appContext().packageManager.getApplicationInfo(it, 0)
}
appContext.packageManager.getApplicationLabel(ai)
appContext().packageManager.getApplicationLabel(ai)
} catch (e: PackageManager.NameNotFoundException) {
it
}
as String
}
.toTypedArray()
}.toTypedArray()
return distributorsNameArray.toList()
}
override fun saveDistributor(distributor: String) {
unifiedPush.saveDistributor(appContext, distributor)
unifiedPush.registerApp(appContext)
unifiedPush.saveDistributor(appContext(), distributor)
unifiedPush.register(appContext())
}
override fun removeSavedDistributor() {
unifiedPush.safeRemoveDistributor(appContext)
unifiedPush.removeDistributor(appContext())
}
fun forceRemoveDistributor(context: Context) {
unifiedPush.forceRemoveDistributor(context)
UnifiedPush.removeDistributor(context)
}
}

View File

@@ -0,0 +1,129 @@
/**
* 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.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 kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.MessagingReceiver
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage
class PushMessageReceiver : MessagingReceiver() {
companion object {
private val TAG = "Amethyst-OSSPushReceiver"
}
private val appContext = Amethyst.instance.applicationContext
private val scope = Amethyst.instance.applicationIOScope
private val eventCache = LruCache<String, String>(100)
private val pushHandler = PushDistributorHandler
override fun onMessage(
context: Context,
message: PushMessage,
instance: String,
) {
val messageStr = message.content.decodeToString()
Log.d(TAG, "New message $messageStr for Instance: $instance")
scope.launch {
try {
parseMessage(messageStr)?.let {
receiveIfNew(it)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.d(TAG, "Message could not be parsed: ${e.message}")
}
}
}
private suspend fun parseMessage(message: String): GiftWrapEvent? {
(Event.fromJson(message) as? GiftWrapEvent)?.let {
return it
}
return null
}
private suspend fun receiveIfNew(event: GiftWrapEvent) {
if (eventCache.get(event.id) == null) {
eventCache.put(event.id, event.id)
EventNotificationConsumer(appContext).consume(event)
}
}
override fun onNewEndpoint(
context: Context,
endpoint: PushEndpoint,
instance: String,
) {
val sanitizedEndpoint = if (endpoint.url.endsWith("?up=1")) endpoint.url.dropLast(5) else endpoint.url
if (sanitizedEndpoint != pushHandler.getSavedEndpoint()) {
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint")
pushHandler.setEndpoint(sanitizedEndpoint)
scope.launch(Dispatchers.IO) {
PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) {
Amethyst.instance.okHttpClients.getHttpClient(Amethyst.instance.torManager.isSocksReady())
}
notificationManager().getOrCreateZapChannel(appContext)
notificationManager().getOrCreateDMChannel(appContext)
}
} else {
Log.d(TAG, "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint")
}
}
override fun onRegistrationFailed(
context: Context,
reason: FailedReason,
instance: String,
) {
Log.d(TAG, "Registration failed for Instance: $instance")
pushHandler.forceRemoveDistributor(context)
}
override fun onUnregistered(
context: Context,
instance: String,
) {
val removedEndpoint = pushHandler.getSavedEndpoint()
Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance")
Log.d(TAG, "App is unregistered. ")
pushHandler.forceRemoveDistributor(context)
pushHandler.removeEndpoint()
}
fun notificationManager(): NotificationManager =
ContextCompat.getSystemService(appContext, NotificationManager::class.java)
as NotificationManager
}

View File

@@ -0,0 +1,72 @@
/**
* 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.service.notifications
import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.service.retryIfException
import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
object PushNotificationUtils {
var lastToken: String? = null
var hasInit: List<AccountInfo>? = null
private val pushHandler = PushDistributorHandler
suspend fun checkAndInit(
accounts: List<AccountInfo>,
okHttpClient: (String) -> OkHttpClient,
) = with(Dispatchers.IO) {
if (!pushHandler.savedDistributorExists()) return
val currentDistributor = PushDistributorHandler.getSavedDistributor()
PushDistributorHandler.saveDistributor(currentDistributor)
val token = pushHandler.getSavedEndpoint()
if (hasInit?.equals(accounts) == true && lastToken == token) {
return@with
}
registerToken(token, accounts, okHttpClient)
}
suspend fun checkAndInit(
token: String,
accounts: List<AccountInfo>,
okHttpClient: (String) -> OkHttpClient,
) = with(Dispatchers.IO) {
// initializes if the accounts are different or if the token has changed
if (hasInit?.equals(accounts) == true && lastToken == token) {
return@with
}
registerToken(token, accounts, okHttpClient)
}
private suspend fun registerToken(
token: String,
accounts: List<AccountInfo>,
okHttpClient: (String) -> OkHttpClient,
) = retryIfException("RegisterAccounts") {
RegisterAccounts(accounts, okHttpClient).go(token)
lastToken = token
hasInit = accounts.toList()
}
}

View File

@@ -0,0 +1,209 @@
/**
* 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.ui.components
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
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.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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) {
val notificationPermissionState =
checkifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel)
if (notificationPermissionState.status.isGranted) {
if (!sharedPreferencesViewModel.sharedPrefs.dontShowPushNotificationSelector) {
val context = LocalContext.current
var distributorPresent by remember {
mutableStateOf(PushDistributorHandler.savedDistributorExists())
}
if (!distributorPresent) {
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
if (readableListWithExplainer.size > 1) {
SpinnerSelectionDialog(
title = stringRes(id = R.string.select_push_server),
options = readableListWithExplainer,
onSelect = { index ->
if (list[index] == "None") {
PushDistributorHandler.forceRemoveDistributor(context)
sharedPreferencesViewModel.dontAskForNotificationPermissions()
sharedPreferencesViewModel.dontShowPushNotificationSelector()
} else {
val fullDistributorName = list[index]
PushDistributorHandler.saveDistributor(fullDistributorName)
}
distributorPresent = true
Log.d("Amethyst", "NotificationScreen: Distributor registered.")
},
onDismiss = {
distributorPresent = true
Log.d("Amethyst", "NotificationScreen: Distributor dialog dismissed.")
},
)
} else {
AlertDialog(
onDismissRequest = { distributorPresent = true },
title = { Text(stringRes(R.string.push_server_install_app)) },
text = {
val content = stringRes(R.string.push_server_install_app_description)
val astNode =
remember {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
}
RichText(
style = RichTextStyle().resolveDefaults(),
renderer = null,
) {
BasicMarkdown(astNode)
}
},
confirmButton = {
Row(
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
TextButton(
onClick = {
distributorPresent = true
sharedPreferencesViewModel.dontShowPushNotificationSelector()
},
) {
Text(stringRes(R.string.quick_action_dont_show_again_button))
}
Button(
onClick = { distributorPresent = true },
contentPadding = PaddingValues(horizontal = 16.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
)
Spacer(Modifier.width(8.dp))
Text(stringRes(R.string.error_dialog_button_ok))
}
}
}
},
)
}
}
}
}
}
}
@Composable
fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, ImmutableList<TitleExplainer>) -> Unit) {
val currentDistributor = PushDistributorHandler.getSavedDistributor().ifBlank { null } ?: "None"
val list =
remember {
PushDistributorHandler.getInstalledDistributors().plus("None").toImmutableList()
}
val readableListWithExplainer =
PushDistributorHandler
.formattedDistributorNames()
.mapIndexed { index, name ->
TitleExplainer(
name,
stringRes(id = R.string.push_server_uses_app_explainer, list[index]),
)
}.plus(
TitleExplainer(
stringRes(id = R.string.push_server_none),
stringRes(id = R.string.push_server_none_explainer),
),
).toImmutableList()
onInner(
currentDistributor,
list,
readableListWithExplainer,
)
}
@Composable
fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) {
val context = LocalContext.current
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
SettingsRow(
R.string.push_server_title,
R.string.push_server_explainer,
selectedItens = readableListWithExplainer,
selectedIndex = list.indexOf(currentDistributor),
) { index ->
if (list[index] == "None") {
sharedPreferencesViewModel.dontAskForNotificationPermissions()
sharedPreferencesViewModel.dontShowPushNotificationSelector()
PushDistributorHandler.forceRemoveDistributor(context)
} else {
PushDistributorHandler.saveDistributor(list[index])
}
}
}
}

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.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
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
@Composable
fun TranslatableRichTextViewer(
content: String,
canPreview: Boolean,
quotesLeft: Int,
modifier: Modifier = Modifier,
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
id: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: INav,
) = ExpandableRichTextViewer(
content,
canPreview,
quotesLeft,
modifier,
tags,
backgroundColor,
id,
callbackUri,
accountViewModel,
nav,
)
@Composable
fun TranslatableRichTextViewer(
content: String,
id: String,
accountViewModel: AccountViewModel,
displayText: @Composable (String) -> Unit,
) = displayText(content)

View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="org.torproject.android"/>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrsigner" />
</intent>
</queries>
<!-- Doesn't require a camera -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- To connect with relays -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- To Upload media (old devices) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- To read QRs that contain nostr:<NIP19> -->
<!-- To take pictures -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- To record audio messages -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- To read NFCs that contain nostr:<NIP19> -->
<uses-permission android:name="android.permission.NFC" />
<!-- To know receive notifications when the app connects/disconnects from the web -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<!-- Audio/Video Playback -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- Keeps screen on while playing videos -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- This notification permission is needed for some phones -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Adds Geohash to posts if active -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Old permission to access media -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<application
android:allowBackup="false"
android:name=".Amethyst"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:enableOnBackInvokedCallback="true"
android:supportsRtl="true"
android:theme="@style/Theme.Amethyst"
android:largeHeap="true"
android:usesCleartextTraffic="true"
android:hardwareAccelerated="true"
android:localeConfig="@xml/locales_config"
tools:targetApi="34">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:launchMode="singleInstance"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|screenSize|screenLayout"
android:taskAffinity=".service.playback.pip.PipVideoActivity"
android:theme="@style/Theme.Amethyst">
<intent-filter android:label="Amethyst">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostr" />
</intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="nostr" />
</intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrwalletconnect" />
<data android:scheme="nostr+walletconnect" />
</intent-filter>
<intent-filter android:label="New Post">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter android:label="New Post">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter android:label="New Post">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter android:label="njump.me">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="njump.me" />
</intent-filter>
<intent-filter android:label="Primal">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="primal.net" />
<data android:pathPrefix="/e/" />
<data android:pathPrefix="/p/" />
<data android:pathPrefix="/a/" />
</intent-filter>
<intent-filter android:label="Yakihonne">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="yakihonne.com" />
<data android:pathPrefix="/notes/" />
<data android:pathPrefix="/users/" />
<data android:pathPrefix="/videos/" />
<data android:pathPrefix="/article/" />
</intent-filter>
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation"
tools:ignore="DiscouragedApi" />
<activity
android:name=".service.playback.pip.PipVideoActivity"
android:autoRemoveFromRecents="true"
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize|keyboardHidden|keyboard|uiMode"
android:supportsPictureInPicture="true"
android:launchMode="singleInstance"
android:exported="false"
android:resizeableActivity="true"
android:theme="@style/noAnimTheme"
/>
<service
android:name=".service.playback.service.PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="true"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<receiver
android:name=".service.notifications.PokeyReceiver"
android:exported="true"
tools:ignore="ExportedReceiver"
>
<intent-filter>
<action android:name="com.shared.NOSTR" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,226 @@
/**
* 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.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.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.ots.OtsBlockHeightCache
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 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 Amethyst : Application() {
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)
// 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 ->
if (torProxySettingsAnchor.isDM(url)) {
okHttpClientForRelaysForDms.getHttpClient(torProxySettingsAnchor.useProxy(url))
} else {
okHttpClientForRelays.getHttpClient(torProxySettingsAnchor.useProxy(url))
}
}
// 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) }
// Application-wide ots verification cache
val otsVerifCache by lazy { VerificationStateCache() }
// Application-wide block height request cache
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
override fun onCreate() {
super.onCreate()
Log.d("AmethystApp", "onCreate $this")
instance = this
if (isDebug) {
Logging.setup()
}
// initializes diskcache on an IO thread.
applicationIOScope.launch {
diskCache
videoCache
}
// registers to receive events
pokeyReceiver.register(this)
}
override fun onTerminate() {
super.onTerminate()
Log.d("AmethystApp", "onTerminate $this")
pokeyReceiver.unregister(this)
applicationIOScope.cancel("Application onTerminate $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.
*
* @param level the memory-related event that was raised.
*/
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
}
}

View File

@@ -0,0 +1,199 @@
/**
* 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.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 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() }}") }
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")
val nativeHeap = Debug.getNativeHeapAllocatedSize() / (1024 * 1024)
val maxNative = Debug.getNativeHeapSize() / (1024 * 1024)
Log.d("STATE DUMP", "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",
"Connected Relays: " +
Amethyst.instance.client
.relayStatusFlow()
.value.connected.size + "/" +
Amethyst.instance.client
.relayStatusFlow()
.value.available.size,
)
Log.d(
"STATE DUMP",
"Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB",
)
Log.d(
"STATE DUMP",
"Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB",
)
Log.d(
"STATE DUMP",
"Notes: " +
LocalCache.notes.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.notes.filter { _, it -> it.event != null }.size +
" / " +
LocalCache.notes.size(),
)
Log.d(
"STATE DUMP",
"Addressables: " +
LocalCache.addressables.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.addressables.filter { _, it -> it.event != null }.size +
" / " +
LocalCache.addressables.size(),
)
Log.d(
"STATE DUMP",
"Users: " +
LocalCache.users.filter { _, it -> it.flowSet != null }.size +
" / " +
LocalCache.users.filter { _, it -> it.latestMetadata != null }.size +
" / " +
LocalCache.users.size(),
)
Log.d(
"STATE DUMP",
"Deletion Events: " +
LocalCache.deletionIndex.size(),
)
Log.d(
"STATE DUMP",
"Observable Events: " +
LocalCache.observablesByKindAndETag.size +
" / " +
LocalCache.observablesByKindAndAuthor.size,
)
Log.d(
"STATE DUMP",
"Spam: " +
LocalCache.antiSpam.spamMessages.size() + " / " + LocalCache.antiSpam.recentEventIds.size() + " / " + LocalCache.antiSpam.recentAddressables.size(),
)
Log.d(
"STATE DUMP",
"Memory used by Events: " +
LocalCache.notes.sumOfLong { _, note -> note.event?.countMemory() ?: 0L } / (1024 * 1024) +
" MB",
)
val qttNotes = LocalCache.notes.countByGroup { _, it -> it.event?.kind }
val qttAddressables = LocalCache.addressables.countByGroup { _, it -> it.event?.kind }
val bytesNotes =
LocalCache.notes
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
val bytesAddressables =
LocalCache.addressables
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 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 ")
}
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 ")
}
}
inline fun <T> logTime(
debugMessage: String,
minToReportMs: Int = 1,
block: () -> T,
): T =
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage")
}
result
} else {
block()
}
inline fun <T> logTime(
debugMessage: (T) -> String,
minToReportMs: Int = 1,
block: () -> T,
): T =
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}")
}
result
} else {
block()
}
fun debug(
tag: String,
debugMessage: String,
) {
if (isDebug) {
Log.d(tag, debugMessage)
}
}
inline fun debug(
tag: String,
debugMessage: () -> String,
) {
if (isDebug) {
Log.d(tag, debugMessage())
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -29,16 +29,15 @@ class EncryptedStorage {
private const val PREFERENCES_NAME = "secret_keeper"
// returns the preferences for each account or a global file if null.
fun prefsFileName(npub: String? = null): String {
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
}
fun prefsFileName(npub: String? = null): String = if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
fun preferences(
applicationContext: Context,
npub: String? = null,
): EncryptedSharedPreferences {
val masterKey: MasterKey =
MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
MasterKey
.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

View File

@@ -0,0 +1,682 @@
/**
* 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.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.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.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
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
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.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
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 java.io.File
// Release mode (!BuildConfig.DEBUG) always uses encrypted preferences
// To use plaintext SharedPreferences for debugging, set this to true
// It will only apply in Debug builds
private const val DEBUG_PLAINTEXT_PREFERENCES = false
private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
@Immutable
data class AccountInfo(
val npub: String,
val hasPrivKey: Boolean,
val loggedInWithExternalSigner: Boolean,
val isTransient: Boolean,
)
private object PrefKeys {
const val CURRENT_ACCOUNT = "currently_logged_in_account"
const val SAVED_ACCOUNTS = "all_saved_accounts"
const val NOSTR_PRIVKEY = "nostr_privkey"
const val NOSTR_PUBKEY = "nostr_pubkey"
const val LOCAL_RELAY_SERVERS = "localRelayServers"
const val DEFAULT_FILE_SERVER = "defaultFileServer"
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList"
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
const val LATEST_USER_METADATA = "latestUserMetadata"
const val LATEST_CONTACT_LIST = "latestContactList"
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
const val LATEST_BLOCKED_RELAY_LIST = "latestBlockedRelayList"
const val LATEST_TRUSTED_RELAY_LIST = "latestTrustedRelayList"
const val LATEST_MUTE_LIST = "latestMuteList"
const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList"
const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData"
const val LATEST_CHANNEL_LIST = "latestChannelList"
const val LATEST_COMMUNITY_LIST = "latestCommunityList"
const val LATEST_HASHTAG_LIST = "latestHashtagList"
const val LATEST_GEOHASH_LIST = "latestGeohashList"
const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList"
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
const val TOR_SETTINGS = "tor_settings"
const val USE_PROXY = "use_proxy"
const val PROXY_PORT = "proxy_port"
const val LAST_READ_PER_ROUTE = "last_read_route_per_route"
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
const val SIGNER_PACKAGE_NAME = "signer_package_name"
const val HAS_DONATED_IN_VERSION = "has_donated_in_version"
const val PENDING_ATTESTATIONS = "pending_attestations"
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
const val SHARED_SETTINGS = "shared_settings"
}
object LocalPreferences {
private const val COMMA = ","
private var currentAccount: String? = null
private val savedAccounts: MutableStateFlow<List<AccountInfo>?> = MutableStateFlow(null)
private val cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf()
suspend fun currentAccount(): String? {
if (currentAccount == null) {
currentAccount =
withContext(Dispatchers.IO) {
encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
}
}
return currentAccount
}
private suspend fun updateCurrentAccount(info: AccountInfo?) {
if (info == null) {
currentAccount = null
withContext(Dispatchers.IO) {
encryptedPreferences().edit { clear() }
}
} else if (currentAccount != info.npub) {
currentAccount = info.npub
if (!info.isTransient) {
withContext(Dispatchers.IO) {
encryptedPreferences().edit { putString(PrefKeys.CURRENT_ACCOUNT, info.npub) }
}
}
}
}
private suspend fun savedAccounts(): List<AccountInfo> {
if (savedAccounts.value == null) {
withContext(Dispatchers.IO) {
with(encryptedPreferences()) {
val newSystemOfAccounts =
getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let {
JsonMapper.mapper.readValue<List<AccountInfo>>(it)
}
if (!newSystemOfAccounts.isNullOrEmpty()) {
savedAccounts.emit(newSystemOfAccounts)
} else {
val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf()
val migrated =
oldAccounts.map { npub ->
AccountInfo(
npub,
encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false),
(encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank(),
false,
)
}
savedAccounts.emit(migrated)
edit { putString(PrefKeys.ALL_ACCOUNT_INFO, JsonMapper.mapper.writeValueAsString(savedAccounts.value)) }
}
}
}
}
// it's always not null when it gets here.
return savedAccounts.value!!
}
fun accountsFlow() = savedAccounts
private suspend fun updateSavedAccounts(accounts: List<AccountInfo>) =
withContext(Dispatchers.IO) {
if (savedAccounts != accounts) {
savedAccounts.emit(accounts)
encryptedPreferences()
.edit {
putString(
PrefKeys.ALL_ACCOUNT_INFO,
JsonMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }),
)
}
}
}
private val prefsDirPath: String
get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/"
private suspend fun addAccount(accInfo: AccountInfo) {
val accounts = savedAccounts().filter { it.npub != accInfo.npub }.plus(accInfo)
updateSavedAccounts(accounts)
}
private suspend fun setCurrentAccount(accountSettings: AccountSettings) {
val npub = accountSettings.keyPair.pubKey.toNpub()
val accInfo =
AccountInfo(
npub,
accountSettings.isWriteable(),
accountSettings.externalSignerPackageName != null,
accountSettings.transientAccount,
)
updateCurrentAccount(accInfo)
addAccount(accInfo)
}
suspend fun switchToAccount(accountInfo: AccountInfo) = updateCurrentAccount(accountInfo)
/** Removes the account from the app level shared preferences */
private suspend fun removeAccount(accountInfo: AccountInfo) {
updateSavedAccounts(savedAccounts().filter { it.npub != accountInfo.npub })
}
/** Deletes the npub-specific shared preference file */
private suspend fun deleteUserPreferenceFile(npub: String) {
withContext(Dispatchers.IO) {
val prefsDir = File(prefsDirPath)
prefsDir.list()?.forEach {
if (it.contains(npub)) {
File(prefsDir, it).delete()
}
}
}
}
private fun encryptedPreferences(npub: String? = null): SharedPreferences {
checkNotInMainThread()
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)
} else {
Amethyst.instance.encryptedStorage(npub)
}
}
/**
* Clears the preferences for a given npub, deletes the preferences xml file, and switches the
* user to the first account in the list if it exists
*
* We need to use `commit()` to write changes to disk and release the file lock so that it can be
* deleted. If we use `apply()` there is a race condition and the file will probably not be
* deleted
*/
@SuppressLint("ApplySharedPref")
suspend fun deleteAccount(accountInfo: AccountInfo) {
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
withContext(Dispatchers.IO) {
encryptedPreferences(accountInfo.npub).edit(commit = true) { clear() }
removeAccount(accountInfo)
deleteUserPreferenceFile(accountInfo.npub)
if (savedAccounts().isEmpty()) {
updateCurrentAccount(null)
} else if (currentAccount() == accountInfo.npub) {
updateCurrentAccount(savedAccounts().elementAt(0))
}
}
}
suspend fun setDefaultAccount(accountSettings: AccountSettings) {
setCurrentAccount(accountSettings)
saveToEncryptedStorage(accountSettings)
}
suspend fun allSavedAccounts(): List<AccountInfo> = savedAccounts()
suspend fun saveToEncryptedStorage(settings: AccountSettings) {
Log.d("LocalPreferences", "Saving to encrypted storage")
if (!settings.transientAccount) {
withContext(Dispatchers.IO) {
val prefs = encryptedPreferences(settings.keyPair.pubKey.toNpub())
prefs.edit {
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, settings.externalSignerPackageName != null)
if (settings.externalSignerPackageName != null) {
remove(PrefKeys.NOSTR_PRIVKEY)
putString(PrefKeys.SIGNER_PACKAGE_NAME, settings.externalSignerPackageName)
} else {
remove(PrefKeys.SIGNER_PACKAGE_NAME)
settings.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
}
settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
putString(
PrefKeys.DEFAULT_FILE_SERVER,
JsonMapper.mapper.writeValueAsString(settings.defaultFileServer),
)
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value)
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value)
putString(
PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST,
settings.defaultNotificationFollowList.value,
)
putString(
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)
}
if (settings.backupDMRelayList != null) {
putString(
PrefKeys.LATEST_DM_RELAY_LIST,
JsonMapper.mapper.writeValueAsString(settings.backupDMRelayList),
)
} else {
remove(PrefKeys.LATEST_DM_RELAY_LIST)
}
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)
}
if (settings.localRelayServers.value.isNotEmpty()) {
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value)
} else {
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)
}
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)
}
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, settings.hideBlockAlertDialog)
// migrating from previous design
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
}
putString(
PrefKeys.LAST_READ_PER_ROUTE,
JsonMapper.mapper.writeValueAsString(regularMap),
)
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
putString(
PrefKeys.PENDING_ATTESTATIONS,
JsonMapper.mapper.writeValueAsString(settings.pendingAttestations.value),
)
}
}
}
Log.d("LocalPreferences", "Saved to encrypted storage")
}
suspend fun loadCurrentAccountFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
suspend fun saveSharedSettings(
sharedSettings: Settings,
prefs: SharedPreferences = encryptedPreferences(),
) {
Log.d("LocalPreferences", "Saving to shared settings")
prefs.edit {
putString(PrefKeys.SHARED_SETTINGS, JsonMapper.mapper.writeValueAsString(sharedSettings))
}
}
suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? {
Log.d("LocalPreferences", "Load shared settings")
with(prefs) {
return try {
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { JsonMapper.mapper.readValue<Settings>(it) }
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}",
e,
)
null
}
}
}
val mutex = Mutex()
suspend fun loadCurrentAccountFromEncryptedStorage(npub: String): AccountSettings? {
// if already loaded, return right away
if (cachedAccounts.containsKey(npub)) {
return cachedAccounts[npub]
}
return withContext(Dispatchers.IO) {
mutex.withLock {
if (cachedAccounts.containsKey(npub)) {
return@withContext cachedAccounts.get(npub)
}
val accountSettings = innerLoadCurrentAccountFromEncryptedStorage(npub)
cachedAccounts.put(npub, accountSettings)
return@withContext accountSettings
}
}
}
private suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? {
Log.d("LocalPreferences", "Load account from file $npub")
val result =
withContext(Dispatchers.IO) {
checkNotInMainThread()
return@withContext with(encryptedPreferences(npub)) {
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
val externalSignerPackageName =
getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
val defaultHomeFollowList =
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: ALL_FOLLOWS
val defaultStoriesFollowList =
getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val defaultNotificationFollowList =
getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val defaultDiscoveryFollowList =
getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
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 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)
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
val latestChannelList = parseEventOrNull<ChannelListEvent>(PrefKeys.LATEST_CHANNEL_LIST)
val latestCommunityList = parseEventOrNull<CommunityListEvent>(PrefKeys.LATEST_COMMUNITY_LIST)
val latestHashtagList = parseEventOrNull<HashtagListEvent>(PrefKeys.LATEST_HASHTAG_LIST)
val latestGeohashList = parseEventOrNull<GeohashListEvent>(PrefKeys.LATEST_GEOHASH_LIST)
val latestEphemeralList = parseEventOrNull<EphemeralChatListEvent>(PrefKeys.LATEST_EPHEMERAL_LIST)
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
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)
} ?: mapOf()
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
return@with AccountSettings(
keyPair = keyPair,
transientAccount = false,
externalSignerPackageName = externalSignerPackageName,
localRelayServers = MutableStateFlow(localRelayServers),
defaultFileServer = defaultFileServer,
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer?.normalize()),
hideDeleteRequestDialog = hideDeleteRequestDialog,
hideBlockAlertDialog = hideBlockAlertDialog,
hideNIP17WarningDialog = hideNIP17WarningDialog,
backupUserMetadata = latestUserMetadata,
backupContactList = latestContactList,
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
backupSearchRelayList = latestSearchRelayList,
backupBlockedRelayList = latestBlockedRelayList,
backupTrustedRelayList = latestTrustedRelayList,
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
backupMuteList = latestMuteList,
backupAppSpecificData = latestAppSpecificData,
backupChannelList = latestChannelList,
backupCommunityList = latestCommunityList,
backupHashtagList = latestHashtagList,
backupGeohashList = latestGeohashList,
backupEphemeralChatList = latestEphemeralList,
torSettings = TorSettingsFlow.build(torSettings),
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
pendingAttestations = MutableStateFlow(pendingAttestations),
)
}
}
Log.d("LocalPreferences", "Loaded account from file $npub")
return result
}
private inline fun <reified T> SharedPreferences.parseOrNull(key: String): T? {
val value = getString(key, null)
if (value.isNullOrEmpty() || value == "null") {
return null
}
return try {
if (T::class.java.isInstance(Event::class.java)) {
Event.fromJson(value) as T?
} else {
JsonMapper.mapper.readValue<T?>(value)
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
null
}
}
private inline fun <reified T> SharedPreferences.parseEventOrNull(key: String): T? {
val value = getString(key, null)
if (value.isNullOrEmpty() || value == "null") {
return null
}
return try {
Event.fromJson(value) as T?
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
null
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,610 @@
/**
* 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 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
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
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.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
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import java.util.Locale
val DefaultChannels =
listOf(
// Anigma's Nostr
ChannelTag("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos),
// Amethyst's Group
ChannelTag("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos),
)
val DefaultNIP65RelaySet = setOf(Constants.mom, Constants.nos, Constants.bitcoiner)
val DefaultNIP65List =
listOf(
AdvertisedRelayInfo(Constants.mom, AdvertisedRelayType.BOTH),
AdvertisedRelayInfo(Constants.nos, AdvertisedRelayType.BOTH),
AdvertisedRelayInfo(Constants.bitcoiner, AdvertisedRelayType.BOTH),
)
val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos)
val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday)
val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Constants.userkinds)
val DefaultSignerPermissions =
listOf(
Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND),
Permission(CommandType.SIGN_EVENT, DraftWrapEvent.KIND),
Permission(CommandType.NIP04_ENCRYPT),
Permission(CommandType.NIP04_DECRYPT),
Permission(CommandType.NIP44_DECRYPT),
Permission(CommandType.NIP44_DECRYPT),
Permission(CommandType.DECRYPT_ZAP_EVENT),
)
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
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 AROUND_ME = " Around Me "
@Stable
class AccountSettings(
val keyPair: KeyPair,
val transientAccount: Boolean = false,
var externalSignerPackageName: String? = null,
var localRelayServers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0],
val defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(ALL_FOLLOWS),
val defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
val defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
val defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
var zapPaymentRequest: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?> = MutableStateFlow(null),
var hideDeleteRequestDialog: Boolean = false,
var hideBlockAlertDialog: Boolean = false,
var hideNIP17WarningDialog: Boolean = false,
var backupUserMetadata: MetadataEvent? = null,
var backupContactList: ContactListEvent? = null,
var backupDMRelayList: ChatMessageRelayListEvent? = null,
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
var backupSearchRelayList: SearchRelayListEvent? = null,
var backupBlockedRelayList: BlockedRelayListEvent? = null,
var backupTrustedRelayList: TrustedRelayListEvent? = null,
var backupMuteList: MuteListEvent? = null,
var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null,
var backupAppSpecificData: AppSpecificDataEvent? = null,
var backupChannelList: ChannelListEvent? = null,
var backupCommunityList: CommunityListEvent? = null,
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()),
) {
val saveable = MutableStateFlow(AccountSettingsUpdater(null))
val syncedSettings: AccountSyncedSettings = AccountSyncedSettings(AccountSyncedSettingsInternal())
class AccountSettingsUpdater(
val accountSettings: AccountSettings?,
)
fun saveAccountSettings() {
saveable.update { AccountSettingsUpdater(this) }
}
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
// ---
fun changeDefaultZapType(zapType: LnZapEvent.ZapType): Boolean {
if (syncedSettings.zaps.defaultZapType.value != zapType) {
syncedSettings.zaps.defaultZapType.tryEmit(zapType)
saveAccountSettings()
return true
}
return false
}
fun changeZapAmounts(newAmounts: List<Long>): Boolean {
if (syncedSettings.zaps.zapAmountChoices.value != newAmounts) {
syncedSettings.zaps.zapAmountChoices.tryEmit(newAmounts.toImmutableList())
saveAccountSettings()
return true
}
return false
}
fun changeReactionTypes(newTypes: List<String>): Boolean {
if (syncedSettings.reactions.reactionChoices.value != newTypes) {
syncedSettings.reactions.reactionChoices.tryEmit(newTypes.toImmutableList())
saveAccountSettings()
return true
}
return false
}
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean {
if (zapPaymentRequest.value != newServer) {
zapPaymentRequest.tryEmit(newServer)
saveAccountSettings()
return true
}
return false
}
// ---
// file servers
// ---
fun changeDefaultFileServer(server: ServerName) {
if (defaultFileServer != server) {
defaultFileServer = server
saveAccountSettings()
}
}
// ---
// list names
// ---
fun changeDefaultHomeFollowList(name: FeedDefinition) {
changeDefaultHomeFollowList(name.code)
}
fun changeDefaultHomeFollowList(name: String) {
if (defaultHomeFollowList.value != name) {
defaultHomeFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultStoriesFollowList(name: FeedDefinition) {
changeDefaultStoriesFollowList(name.code)
}
fun changeDefaultStoriesFollowList(name: String) {
if (defaultStoriesFollowList.value != name) {
defaultStoriesFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultNotificationFollowList(name: FeedDefinition) {
changeDefaultNotificationFollowList(name.code)
}
fun changeDefaultNotificationFollowList(name: String) {
if (defaultNotificationFollowList.value != name) {
defaultNotificationFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultDiscoveryFollowList(name: FeedDefinition) {
changeDefaultDiscoveryFollowList(name.code)
}
fun changeDefaultDiscoveryFollowList(name: String) {
if (defaultDiscoveryFollowList.value != name) {
defaultDiscoveryFollowList.tryEmit(name)
saveAccountSettings()
}
}
// ---
// proxy settings
// ---
fun setTorSettings(newTorSettings: TorSettings): Boolean =
if (torSettings.update(newTorSettings)) {
saveAccountSettings()
true
} else {
false
}
// ---
// language services
// ---
fun toggleDontTranslateFrom(languageCode: String) {
syncedSettings.languages.toggleDontTranslateFrom(languageCode)
saveAccountSettings()
}
fun translateToContains(languageCode: Locale) = syncedSettings.languages.translateTo.contains(languageCode.language)
fun updateTranslateTo(languageCode: Locale): Boolean {
if (syncedSettings.languages.updateTranslateTo(languageCode)) {
saveAccountSettings()
return true
}
return false
}
fun prefer(
source: String,
target: String,
preference: String,
) {
syncedSettings.languages.prefer(source, target, preference)
saveAccountSettings()
}
fun preferenceBetween(
source: String,
target: String,
): String? = syncedSettings.languages.preferenceBetween(source, target)
// ----
// Backup Lists
// ----
fun updateLocalRelayServers(servers: Set<String>) {
if (localRelayServers.value != servers) {
localRelayServers.update { servers }
saveAccountSettings()
}
}
fun updateUserMetadata(newMetadata: MetadataEvent?) {
if (newMetadata == null) return
// Events might be different objects, we have to compare their ids.
if (backupUserMetadata?.id != newMetadata.id) {
backupUserMetadata = newMetadata
saveAccountSettings()
}
}
fun updateContactListTo(newContactList: ContactListEvent?) {
if (newContactList == null || newContactList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupContactList?.id != newContactList.id) {
backupContactList = newContactList
saveAccountSettings()
}
}
fun updateDMRelayList(newDMRelayList: ChatMessageRelayListEvent?) {
if (newDMRelayList == null || newDMRelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupDMRelayList?.id != newDMRelayList.id) {
backupDMRelayList = newDMRelayList
saveAccountSettings()
}
}
fun updateNIP65RelayList(newNIP65RelayList: AdvertisedRelayListEvent?) {
if (newNIP65RelayList == null || newNIP65RelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupNIP65RelayList?.id != newNIP65RelayList.id) {
backupNIP65RelayList = newNIP65RelayList
saveAccountSettings()
}
}
fun updateSearchRelayList(newSearchRelayList: SearchRelayListEvent?) {
if (newSearchRelayList == null || newSearchRelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupSearchRelayList?.id != newSearchRelayList.id) {
backupSearchRelayList = newSearchRelayList
saveAccountSettings()
}
}
fun updateBlockedRelayList(newBlockedRelayList: BlockedRelayListEvent?) {
if (newBlockedRelayList == null || newBlockedRelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupBlockedRelayList?.id != newBlockedRelayList.id) {
backupBlockedRelayList = newBlockedRelayList
saveAccountSettings()
}
}
fun updateTrustedRelayList(newTrustedRelayList: TrustedRelayListEvent?) {
if (newTrustedRelayList == null || newTrustedRelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupTrustedRelayList?.id != newTrustedRelayList.id) {
backupTrustedRelayList = newTrustedRelayList
saveAccountSettings()
}
}
fun updatePrivateHomeRelayList(newPrivateHomeRelayList: PrivateOutboxRelayListEvent?) {
if (newPrivateHomeRelayList == null || newPrivateHomeRelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupPrivateHomeRelayList?.id != newPrivateHomeRelayList.id) {
backupPrivateHomeRelayList = newPrivateHomeRelayList
saveAccountSettings()
}
}
fun updateChannelListTo(newChannelList: ChannelListEvent?) {
if (newChannelList == null || newChannelList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupChannelList?.id != newChannelList.id) {
backupChannelList = newChannelList
saveAccountSettings()
}
}
fun updateGeohashListTo(newGeohashList: GeohashListEvent?) {
if (newGeohashList == null || newGeohashList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupGeohashList?.id != newGeohashList.id) {
backupGeohashList = newGeohashList
saveAccountSettings()
}
}
fun updateHashtagListTo(newHashtagList: HashtagListEvent?) {
if (newHashtagList == null || newHashtagList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupHashtagList?.id != newHashtagList.id) {
backupHashtagList = newHashtagList
saveAccountSettings()
}
}
fun updateCommunityListTo(newCommunityList: CommunityListEvent?) {
if (newCommunityList == null || newCommunityList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupCommunityList?.id != newCommunityList.id) {
backupCommunityList = newCommunityList
saveAccountSettings()
}
}
fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) {
if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupEphemeralChatList?.id != newEphemeralChatList.id) {
backupEphemeralChatList = newEphemeralChatList
saveAccountSettings()
}
}
fun updateMuteList(newMuteList: MuteListEvent?) {
if (newMuteList == null || newMuteList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupMuteList?.id != newMuteList.id) {
backupMuteList = newMuteList
saveAccountSettings()
}
}
fun updateAppSpecificData(
appSettings: AppSpecificDataEvent?,
newSyncedSettings: AccountSyncedSettingsInternal,
) {
if (appSettings == null || appSettings.content.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupAppSpecificData?.id != appSettings.id) {
backupAppSpecificData = appSettings
syncedSettings.updateFrom(newSyncedSettings)
saveAccountSettings()
}
}
// ----
// Warning dialogs
// ----
fun setHideDeleteRequestDialog() {
if (!hideDeleteRequestDialog) {
hideDeleteRequestDialog = true
saveAccountSettings()
}
}
fun setHideNIP17WarningDialog() {
if (!hideNIP17WarningDialog) {
hideNIP17WarningDialog = true
saveAccountSettings()
}
}
fun setHideBlockAlertDialog() {
if (!hideBlockAlertDialog) {
hideBlockAlertDialog = true
saveAccountSettings()
}
}
// ---
// donations
// ---
fun hasDonatedInVersion(versionName: String) = hasDonatedInVersion.value.contains(versionName)
fun observeDonatedInVersion(versionName: String) =
hasDonatedInVersion
.map {
it.contains(versionName)
}
fun markDonatedInThisVersion(versionName: String): Boolean {
if (!hasDonatedInVersion.value.contains(versionName)) {
hasDonatedInVersion.update {
it + versionName
}
saveAccountSettings()
return true
}
return false
}
// ----
// last read flows
// ----
fun getLastReadFlow(route: String): StateFlow<Long> = lastReadPerRoute.value[route] ?: addLastRead(route, 0)
private fun addLastRead(
route: String,
timestampInSecs: Long,
): MutableStateFlow<Long> =
MutableStateFlow<Long>(timestampInSecs).also { newFlow ->
lastReadPerRoute.update { it + Pair(route, newFlow) }
saveAccountSettings()
}
fun markAsRead(
route: String,
timestampInSecs: Long,
): Boolean {
val lastTime = lastReadPerRoute.value[route]
return if (lastTime == null) {
addLastRead(route, timestampInSecs)
true
} else if (timestampInSecs > lastTime.value) {
lastTime.tryEmit(timestampInSecs)
saveAccountSettings()
true
} else {
false
}
}
// ---
// attestations
// ---
fun addPendingAttestation(
id: HexKey,
stamp: String,
) {
val current = pendingAttestations.value.get(id)
if (current == null) {
pendingAttestations.update {
it + Pair(id, stamp)
}
saveAccountSettings()
} else {
if (current != stamp) {
pendingAttestations.update {
it + Pair(id, stamp)
}
saveAccountSettings()
}
}
}
// ---
// filters
// ---
fun updateShowSensitiveContent(show: Boolean?): Boolean {
if (syncedSettings.security.updateShowSensitiveContent(show)) {
saveAccountSettings()
return true
}
return false
}
fun updateWarnReports(warnReports: Boolean): Boolean =
if (syncedSettings.security.updateWarnReports(warnReports)) {
saveAccountSettings()
true
} else {
false
}
fun updateFilterSpam(filterSpam: Boolean): Boolean =
if (syncedSettings.security.updateFilterSpam(filterSpam)) {
saveAccountSettings()
true
} else {
false
}
}

View File

@@ -0,0 +1,211 @@
/**
* 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 androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.util.Locale
@Stable
class AccountSyncedSettings(
internalSettings: AccountSyncedSettingsInternal,
) {
val reactions = AccountReactionPreferences(MutableStateFlow(internalSettings.reactions.reactionChoices.toImmutableList()))
val zaps =
AccountZapPreferences(
MutableStateFlow(internalSettings.zaps.zapAmountChoices.toImmutableList()),
MutableStateFlow(internalSettings.zaps.defaultZapType),
)
val languages =
AccountLanguagePreferences(
internalSettings.languages.dontTranslateFrom,
internalSettings.languages.languagePreferences,
internalSettings.languages.translateTo,
)
val security =
AccountSecurityPreferences(
MutableStateFlow(internalSettings.security.showSensitiveContent),
internalSettings.security.warnAboutPostsWithReports,
MutableStateFlow(internalSettings.security.filterSpamFromStrangers),
)
fun toInternal(): AccountSyncedSettingsInternal =
AccountSyncedSettingsInternal(
reactions = AccountReactionPreferencesInternal(reactions.reactionChoices.value),
zaps =
AccountZapPreferencesInternal(
zaps.zapAmountChoices.value,
zaps.defaultZapType.value,
),
languages =
AccountLanguagePreferencesInternal(
languages.dontTranslateFrom,
languages.languagePreferences,
languages.translateTo,
),
security =
AccountSecurityPreferencesInternal(
security.showSensitiveContent.value,
security.warnAboutPostsWithReports,
security.filterSpamFromStrangers.value,
),
)
fun updateFrom(syncedSettingsInternal: AccountSyncedSettingsInternal) {
val newReactionChoices = syncedSettingsInternal.reactions.reactionChoices.toImmutableList()
if (!equalImmutableLists(reactions.reactionChoices.value, newReactionChoices)) {
reactions.reactionChoices.tryEmit(newReactionChoices)
}
val newZapChoices = syncedSettingsInternal.zaps.zapAmountChoices.toImmutableList()
if (!equalImmutableLists(zaps.zapAmountChoices.value, newZapChoices)) {
zaps.zapAmountChoices.tryEmit(newZapChoices)
}
if (zaps.defaultZapType.value != syncedSettingsInternal.zaps.defaultZapType) {
zaps.defaultZapType.tryEmit(syncedSettingsInternal.zaps.defaultZapType)
}
if (languages.dontTranslateFrom != syncedSettingsInternal.languages.dontTranslateFrom) {
languages.dontTranslateFrom = syncedSettingsInternal.languages.dontTranslateFrom
}
if (languages.languagePreferences != syncedSettingsInternal.languages.languagePreferences) {
languages.languagePreferences = syncedSettingsInternal.languages.languagePreferences
}
if (languages.translateTo != syncedSettingsInternal.languages.translateTo) {
languages.translateTo = syncedSettingsInternal.languages.translateTo
}
if (security.showSensitiveContent.value != syncedSettingsInternal.security.showSensitiveContent) {
security.showSensitiveContent.tryEmit(syncedSettingsInternal.security.showSensitiveContent)
}
if (security.filterSpamFromStrangers.value != syncedSettingsInternal.security.filterSpamFromStrangers) {
security.filterSpamFromStrangers.tryEmit(syncedSettingsInternal.security.filterSpamFromStrangers)
}
if (security.warnAboutPostsWithReports != syncedSettingsInternal.security.warnAboutPostsWithReports) {
security.warnAboutPostsWithReports = syncedSettingsInternal.security.warnAboutPostsWithReports
}
}
fun dontTranslateFromFilteredBySpokenLanguages(): Set<String> = languages.dontTranslateFrom - getLanguagesSpokenByUser()
}
@Stable
class AccountReactionPreferences(
var reactionChoices: MutableStateFlow<ImmutableList<String>>,
)
@Stable
class AccountZapPreferences(
var zapAmountChoices: MutableStateFlow<ImmutableList<Long>>,
val defaultZapType: MutableStateFlow<LnZapEvent.ZapType>,
)
@Stable
class AccountLanguagePreferences(
var dontTranslateFrom: Set<String>,
var languagePreferences: Map<String, String>,
var translateTo: String,
) {
// ---
// language services
// ---
fun toggleDontTranslateFrom(languageCode: String) {
dontTranslateFrom =
if (!dontTranslateFrom.contains(languageCode)) {
dontTranslateFrom.plus(languageCode)
} else {
dontTranslateFrom.minus(languageCode)
}
}
fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language)
fun updateTranslateTo(languageCode: Locale): Boolean {
if (translateTo != languageCode.language) {
translateTo = languageCode.language
return true
}
return false
}
fun prefer(
source: String,
target: String,
preference: String,
) {
val key = "$source,$target"
if (key !in languagePreferences) {
languagePreferences = languagePreferences + Pair(key, preference)
} else {
if (languagePreferences.get(key) == preference) {
languagePreferences = languagePreferences.minus(key)
} else {
languagePreferences = languagePreferences + Pair(key, preference)
}
}
}
fun preferenceBetween(
source: String,
target: String,
): String? = languagePreferences["$source,$target"]
}
@Stable
class AccountSecurityPreferences(
val showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: MutableStateFlow<Boolean> = MutableStateFlow(true),
) {
fun updateShowSensitiveContent(show: Boolean?): Boolean {
if (showSensitiveContent.value != show) {
showSensitiveContent.update { show }
return true
}
return false
}
fun updateWarnReports(warnReports: Boolean): Boolean =
if (warnAboutPostsWithReports != warnReports) {
warnAboutPostsWithReports = warnReports
true
} else {
false
}
fun updateFilterSpam(filterSpam: Boolean): Boolean =
if (filterSpam != filterSpamFromStrangers.value) {
filterSpamFromStrangers.tryEmit(filterSpam)
true
} else {
false
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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 android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import java.util.Locale
val DefaultReactions =
listOf(
"\uD83D\uDE80",
"\uD83E\uDEC2",
"\uD83D\uDC40",
"\uD83D\uDE02",
"\uD83C\uDF89",
"\uD83E\uDD14",
"\uD83D\uDE31",
)
val DefaultZapAmounts = listOf(100L, 500L, 1000L)
fun getLanguagesSpokenByUser(): Set<String> {
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration())
val codedList = mutableSetOf<String>()
for (i in 0 until languageList.size()) {
languageList.get(i)?.let { codedList.add(it.language) }
}
return codedList
}
class AccountSyncedSettingsInternal(
val reactions: AccountReactionPreferencesInternal = AccountReactionPreferencesInternal(),
val zaps: AccountZapPreferencesInternal = AccountZapPreferencesInternal(),
val languages: AccountLanguagePreferencesInternal = AccountLanguagePreferencesInternal(),
val security: AccountSecurityPreferencesInternal = AccountSecurityPreferencesInternal(),
)
class AccountReactionPreferencesInternal(
var reactionChoices: List<String> = DefaultReactions,
)
class AccountZapPreferencesInternal(
var zapAmountChoices: List<Long> = DefaultZapAmounts,
val defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
)
class AccountLanguagePreferencesInternal(
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
var languagePreferences: Map<String, String> = mapOf(),
var translateTo: String = Locale.getDefault().language,
)
class AccountSecurityPreferencesInternal(
val showSensitiveContent: Boolean? = null,
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true,
)

View File

@@ -0,0 +1,175 @@
/**
* 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 android.util.Log
import android.util.LruCache
import com.vitorpamplona.amethyst.ui.note.njumpLink
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 kotlinx.coroutines.flow.MutableStateFlow
data class Spammer(
val pubkeyHex: HexKey,
var duplicatedEventIds: Set<HexKey>,
var duplicatedEventAddresses: Set<Address>,
) {
fun shouldHide() = duplicatedEventIds.size >= 5 || duplicatedEventAddresses.size >= 5
}
class AntiSpamFilter {
val recentEventIds = LruCache<Int, String>(2000)
val recentAddressables = LruCache<Int, Address>(2000)
val spamMessages = LruCache<Int, Spammer>(1000)
var active: Boolean = true
fun spamHashCode(event: Event): Int = 31 * event.content.hashCode() + event.tags.contentDeepHashCode()
fun isSpam(
event: Event,
relay: NormalizedRelayUrl?,
): Boolean {
if (!active) return false
// if short message, ok
// The idea here is to avoid considering repeated "GM" messages spam.
if (event.content.length < 60) return false
// if the message is actually short but because it cites a user/event, the nostr: string is
// 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
// double list strategy:
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
// Considers tags so that same replies to different people don't count.
val hash = spamHashCode(event)
// ignores multiple versions of the same addressable.
if (event is AddressableEvent) {
val address = event.address()
// normal event
if (
(recentAddressables[hash] != null && recentAddressables[hash] != address) ||
(spamMessages[hash] != null && !spamMessages[hash].duplicatedEventAddresses.contains(address))
) {
val existingAddress = recentAddressables[hash]
val link1 = njumpLink(NAddress.create(existingAddress.kind, existingAddress.pubKeyHex, existingAddress.dTag, relay))
val link2 = njumpLink(NAddress.create(event.kind, event.pubKey, event.dTag(), relay))
Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2")
// Log down offenders
val spammer = logOffender(hash, event)
if (spammer.shouldHide() && relay != null) {
RelayStats.newSpam(relay, "$link1 $link2")
}
flowSpam.tryEmit(AntiSpamState(this))
return true
}
recentAddressables.put(hash, address)
} else {
// normal event
if (
(recentEventIds[hash] != null && recentEventIds[hash] != event.id) ||
(spamMessages[hash] != null && !spamMessages[hash].duplicatedEventIds.contains(event.id))
) {
val existingEvent = recentEventIds[hash]
val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay))
val link2 = njumpLink(NEvent.create(event.id, null, null, relay))
Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2")
// Log down offenders
val spammer = logOffender(hash, event)
if (spammer.shouldHide() && relay != null) {
RelayStats.newSpam(relay, "$link1 $link2")
}
flowSpam.tryEmit(AntiSpamState(this))
return true
}
recentEventIds.put(hash, event.id)
}
return false
}
@Synchronized
private fun logOffender(
hashCode: Int,
event: Event,
): Spammer {
val spammer = spamMessages.get(hashCode)
if (spammer == null) {
val newSpammer =
if (event is AddressableEvent) {
Spammer(
pubkeyHex = event.pubKey,
duplicatedEventIds = setOf(),
duplicatedEventAddresses = setOf(recentAddressables[hashCode], event.address()),
)
} else {
Spammer(
pubkeyHex = event.pubKey,
duplicatedEventIds = setOf(recentEventIds[hashCode], event.id),
duplicatedEventAddresses = setOf(),
)
}
spamMessages.put(hashCode, newSpammer)
return newSpammer
} else {
if (event is AddressableEvent) {
spammer.duplicatedEventAddresses += event.address()
} else {
spammer.duplicatedEventIds += event.id
}
return spammer
}
}
val flowSpam = MutableStateFlow<AntiSpamState>(AntiSpamState(this))
}
class AntiSpamState(
val cache: AntiSpamFilter,
)

View File

@@ -0,0 +1,213 @@
/**
* 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 androidx.compose.runtime.Stable
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 kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import java.lang.ref.WeakReference
@Stable
abstract class Channel : NotesGatherer {
val notes = LargeCache<HexKey, Note>()
var lastNote: Note? = null
private var relays = mapOf<NormalizedRelayUrl, Counter>()
private var changesFlow: WeakReference<MutableSharedFlow<ListChange<Note>>> = WeakReference(null)
fun changesFlow(): MutableSharedFlow<ListChange<Note>> {
val current = changesFlow.get()
if (current != null) return current
val new = MutableSharedFlow<ListChange<Note>>(0, 100, BufferOverflow.DROP_OLDEST)
changesFlow = WeakReference(new)
return new
}
abstract fun toBestDisplayName(): String
open fun relays(): Set<NormalizedRelayUrl> =
relays.keys
.toSortedSet { o1, o2 ->
val o1Count = relays[o1]?.number ?: 0
val o2Count = relays[o2]?.number ?: 0
o2Count.compareTo(o1Count) // descending
}
fun updateChannelInfo() {
flowSet?.metadata?.invalidateData()
}
@Synchronized
fun addRelaySync(briefInfo: NormalizedRelayUrl) {
if (briefInfo !in relays) {
relays = relays + Pair(briefInfo, Counter(1))
}
}
fun addRelay(relay: NormalizedRelayUrl) {
val counter = relays[relay]
if (counter != null) {
counter.number++
} else {
addRelaySync(relay)
}
}
fun addNote(
note: Note,
relay: NormalizedRelayUrl? = null,
) {
if (!notes.containsKey(note.idHex)) {
notes.put(note.idHex, note)
note.addGatherer(this)
if ((note.createdAt() ?: 0) > (lastNote?.createdAt() ?: 0)) {
lastNote = note
}
if (relay != null) {
addRelay(relay)
}
changesFlow.get()?.tryEmit(ListChange.Addition(note))
flowSet?.notes?.invalidateData()
}
}
override fun removeNote(note: Note) {
if (notes.containsKey(note.idHex)) {
notes.remove(note.idHex)
note.removeGatherer(this)
if (note == lastNote) {
lastNote = notes.values().sortedWith(DefaultFeedOrder).firstOrNull()
}
changesFlow.get()?.tryEmit(ListChange.Deletion(note))
flowSet?.notes?.invalidateData()
}
}
fun pruneOldMessages(): Set<Note> {
val important =
notes
.values()
.sortedWith(DefaultFeedOrder)
.take(500)
.toSet()
val toBeRemoved = notes.filter { key, it -> it !in important }
toBeRemoved.forEach { notes.remove(it.idHex) }
changesFlow.get()?.tryEmit(ListChange.SetDeletion(toBeRemoved.toSet()))
flowSet?.notes?.invalidateData()
return toBeRemoved.toSet()
}
fun pruneHiddenMessages(account: Account): Set<Note> {
val hidden =
notes
.filter { key, it ->
it.author?.let { author -> account.isHidden(author) } == true
}.toSet()
hidden.forEach { notes.remove(it.idHex) }
changesFlow.get()?.tryEmit(ListChange.SetDeletion(hidden))
flowSet?.notes?.invalidateData()
return hidden.toSet()
}
var flowSet: ChannelFlowSet? = null
@Synchronized
fun createOrDestroyFlowSync(create: Boolean) {
if (create) {
if (flowSet == null) {
flowSet = ChannelFlowSet(this)
}
} else {
if (flowSet != null && flowSet?.isInUse() == false) {
flowSet = null
}
}
}
fun flow(): ChannelFlowSet {
if (flowSet == null) {
createOrDestroyFlowSync(true)
}
return flowSet!!
}
fun clearFlow() {
if (flowSet != null && flowSet?.isInUse() == false) {
createOrDestroyFlowSync(false)
}
}
}
data class Counter(
var number: Int = 0,
)
@Stable
class ChannelFlowSet(
u: Channel,
) {
// Observers line up here.
val metadata = ChannelFlow(u)
val notes = ChannelFlow(u)
fun isInUse(): Boolean =
metadata.hasObservers() ||
notes.hasObservers()
}
class ChannelFlow(
val channel: Channel,
) {
val stateFlow = MutableStateFlow(ChannelState(channel))
fun invalidateData() {
stateFlow.tryEmit(ChannelState(channel))
}
fun hasObservers() = stateFlow.subscriptionCount.value > 0
}
class ChannelState(
val channel: Channel,
)

View File

@@ -0,0 +1,53 @@
/**
* 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.relay.normalizer.RelayUrlNormalizer
object Constants {
val nos = RelayUrlNormalizer.normalize("wss://nos.lol")
val mom = RelayUrlNormalizer.normalize("wss://nostr.mom")
val primal = RelayUrlNormalizer.normalize("wss://relay.primal.net")
val damus = RelayUrlNormalizer.normalize("wss://relay.damus.io")
val wine = RelayUrlNormalizer.normalize("wss://nostr.wine")
val band = RelayUrlNormalizer.normalize("wss://relay.nostr.band")
val where = RelayUrlNormalizer.normalize("wss://relay.noswhere.com")
val elites = RelayUrlNormalizer.normalize("wss://nostrelites.org")
val bitcoiner = RelayUrlNormalizer.normalize("wss://nostr.bitcoiner.social")
val bg = RelayUrlNormalizer.normalize("wss://relay.nostr.bg")
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")
val auth = RelayUrlNormalizer.normalize("wss://auth.nostr1.com")
val oxchat = RelayUrlNormalizer.normalize("wss://relay.0xchat.com")
val purplepages = RelayUrlNormalizer.normalize("wss://purplepag.es")
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, bg)
val defaultSearchRelaySet = setOf(band, wine, where)
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -33,7 +33,9 @@ import com.vitorpamplona.amethyst.commons.hashtags.Btc
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
import com.vitorpamplona.amethyst.commons.hashtags.Coffee
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.hashtags.Flowerstr
import com.vitorpamplona.amethyst.commons.hashtags.Footstr
import com.vitorpamplona.amethyst.commons.hashtags.Gamestr
import com.vitorpamplona.amethyst.commons.hashtags.Grownostr
import com.vitorpamplona.amethyst.commons.hashtags.Lightning
import com.vitorpamplona.amethyst.commons.hashtags.Mate
@@ -47,29 +49,30 @@ 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.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
@Preview
@Composable
fun RenderHashTagIcons() {
val nav: (String) -> Unit = {}
fun RenderHashTagIconsPreview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
RenderRegular(
"Testing rendering of hashtags: #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate",
"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, nav)
is HashTagSegment -> HashTag(word, accountViewModel, EmptyNav)
is RegularTextSegment -> Text(word.segmentText)
}
}
}
}
fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
return when (tag.lowercase()) {
fun checkForHashtagWithIcon(tag: String): HashtagIcon? =
when (tag.lowercase()) {
"₿itcoin", "bitcoin", "btc", "timechain", "bitcoiner", "bitcoiners" -> bitcoin
"nostr", "nostrich", "nostriches", "thenostr" -> nostr
"lightning", "lightningnetwork" -> lightning
@@ -81,12 +84,13 @@ fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
"skullofsatoshi" -> skull
"grownostr", "gardening", "garden" -> growstr
"footstr" -> footstr
"flowerstr" -> flowerstr
"tunestr", "music", "nowplaying" -> tunestr
"mate", "matechain", "matestr" -> matestr
"weed", "weedstr", "420", "cannabis", "marijuana" -> weed
"gamestr", "gaming", "gamechain" -> gamestr
else -> null
}
}
val bitcoin = HashtagIcon(CustomHashTagIcons.Btc, "Bitcoin", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
val nostr = HashtagIcon(CustomHashTagIcons.Nostr, "Nostr", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
@@ -99,9 +103,11 @@ val coffee = HashtagIcon(CustomHashTagIcons.Coffee, "Coffee", Modifier.padding(s
val skull = HashtagIcon(CustomHashTagIcons.Skull, "SkullofSatoshi", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
val growstr = HashtagIcon(CustomHashTagIcons.Grownostr, "GrowNostr", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
val footstr = HashtagIcon(CustomHashTagIcons.Footstr, "Footstr", Modifier.padding(start = 2.dp, bottom = 1.dp, top = 1.dp))
val flowerstr = HashtagIcon(CustomHashTagIcons.Flowerstr, "Flowerstr", Modifier.padding(start = 2.dp, bottom = 1.dp, top = 1.dp))
val tunestr = HashtagIcon(CustomHashTagIcons.Tunestr, "Tunestr", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
val weed = HashtagIcon(CustomHashTagIcons.Weed, "Weed", Modifier.padding(start = 1.dp, bottom = 0.dp, top = 0.dp))
val matestr = HashtagIcon(CustomHashTagIcons.Mate, "Mate", Modifier.padding(start = 1.dp, bottom = 0.dp, top = 0.dp))
val gamestr = HashtagIcon(CustomHashTagIcons.Gamestr, "GameStr", Modifier.padding(start = 1.dp, bottom = 0.dp, top = 0.dp))
@Immutable
class HashtagIcon(

File diff suppressed because it is too large Load Diff

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
import android.util.LruCache
interface MutableMediaAspectRatioCache {
fun get(url: String): Float?
fun add(
url: String,
width: Int,
height: Int,
)
}
object MediaAspectRatioCache : MutableMediaAspectRatioCache {
val mediaAspectRatioCacheByUrl = LruCache<String, Float>(1000)
override fun get(url: String): Float? = mediaAspectRatioCacheByUrl.get(url)
override fun add(
url: String,
width: Int,
height: Int,
) {
if (height > 1) {
mediaAspectRatioCacheByUrl.put(url, width.toFloat() / height.toFloat())
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -21,6 +21,7 @@
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
@@ -40,11 +41,13 @@ import kotlinx.collections.immutable.toImmutableMap
JsonSubTypes.Type(value = Bundle::class, name = "Bundle"),
JsonSubTypes.Type(value = VisionPrescription::class, name = "VisionPrescription"),
)
@Stable
open class Resource(
var resourceType: String? = null,
var id: String = "",
)
@Stable
class Practitioner(
resourceType: String? = null,
id: String = "",
@@ -53,6 +56,7 @@ class Practitioner(
var gender: String? = null,
) : Resource(resourceType, id)
@Stable
class Patient(
resourceType: String? = null,
id: String = "",
@@ -61,16 +65,16 @@ class Patient(
var gender: String? = null,
) : Resource(resourceType, id)
@Stable
class HumanName(
var use: String? = null,
var family: String? = null,
var given: ArrayList<String> = arrayListOf(),
) {
fun assembleName(): String {
return given.joinToString(" ") + " " + family
}
fun assembleName(): String = given.joinToString(" ") + " " + family
}
@Stable
class Bundle(
resourceType: String? = null,
id: String = "",
@@ -79,6 +83,7 @@ class Bundle(
var entry: List<Resource> = arrayListOf(),
) : Resource(resourceType, id)
@Stable
class VisionPrescription(
resourceType: String? = null,
id: String = "",
@@ -89,15 +94,31 @@ class VisionPrescription(
var dateWritten: String? = null,
var prescriber: Reference? = Reference(),
var lensSpecification: List<LensSpecification> = arrayListOf(),
) : Resource(resourceType, id)
) : Resource(resourceType, id) {
fun glasses() = lensSpecification.filter { it.product == "lens" }
fun contacts() = lensSpecification.filter { it.product == "contacts" }
fun glassesRightEyes() = lensSpecification.filter { it.product == "lens" && it.eye == "right" }
fun glassesLeftEyes() = lensSpecification.filter { it.product == "lens" && it.eye == "left" }
fun contactsRightEyes() = lensSpecification.filter { it.product == "contacts" && it.eye == "right" }
fun contactsLeftEyes() = lensSpecification.filter { it.product == "contacts" && it.eye == "left" }
}
@Stable
class LensSpecification(
var product: String? = null,
var eye: String? = null,
var sphere: Double? = null,
var cylinder: Double? = null,
var axis: Double? = null,
var pd: Double? = null,
var interAdd: Double? = null,
var add: Double? = null,
var prism: List<Prism> = arrayListOf(),
var prism: Prism? = null,
// contact lenses
var power: Double? = null,
var backCurve: Double? = null,
@@ -107,6 +128,7 @@ class LensSpecification(
var note: String? = null,
)
@Stable
class Prism(
var amount: Double? = null,
var base: String? = null,

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
class ParticipantListBuilder {
private fun addFollowsThatDirectlyParticipateOnToSet(
@@ -96,7 +96,7 @@ class ParticipantListBuilder {
it.replyTo?.forEach { addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) }
}
LocalCache.getChannelIfExists(baseNote.idHex)?.notes?.forEach { key, it ->
LocalCache.getPublicChatChannelIfExists(baseNote.idHex)?.notes?.forEach { key, it ->
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
}

View File

@@ -0,0 +1,181 @@
/**
* 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 androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.R
@Stable
data class Settings(
val theme: ThemeType = ThemeType.SYSTEM,
val preferredLanguage: String? = null,
val automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
val automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS,
val automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS,
val automaticallyHideNavigationBars: BooleanType = BooleanType.ALWAYS,
val automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS,
val dontShowPushNotificationSelector: Boolean = false,
val dontAskForNotificationPermissions: Boolean = false,
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC,
)
enum class ThemeType(
val screenCode: Int,
val resourceId: Int,
) {
SYSTEM(0, R.string.system),
LIGHT(1, R.string.light),
DARK(2, R.string.dark),
}
fun parseThemeType(code: Int?): ThemeType =
when (code) {
ThemeType.SYSTEM.screenCode -> ThemeType.SYSTEM
ThemeType.LIGHT.screenCode -> ThemeType.LIGHT
ThemeType.DARK.screenCode -> ThemeType.DARK
else -> {
ThemeType.SYSTEM
}
}
enum class ConnectivityType(
val prefCode: Boolean?,
val screenCode: Int,
val resourceId: Int,
) {
ALWAYS(null, 0, R.string.connectivity_type_always),
WIFI_ONLY(true, 1, R.string.connectivity_type_unmetered_wifi_only),
NEVER(false, 2, R.string.connectivity_type_never),
}
enum class FeatureSetType(
val screenCode: Int,
val resourceId: Int,
) {
COMPLETE(0, R.string.ui_feature_set_type_complete),
SIMPLIFIED(1, R.string.ui_feature_set_type_simplified),
PERFORMANCE(2, R.string.ui_feature_set_type_performance),
}
enum class ProfileGalleryType(
val screenCode: Int,
val resourceId: Int,
) {
CLASSIC(0, R.string.gallery_type_classic),
MODERN(1, R.string.gallery_type_modern),
}
fun parseConnectivityType(code: Boolean?): ConnectivityType =
when (code) {
ConnectivityType.ALWAYS.prefCode -> ConnectivityType.ALWAYS
ConnectivityType.WIFI_ONLY.prefCode -> ConnectivityType.WIFI_ONLY
ConnectivityType.NEVER.prefCode -> ConnectivityType.NEVER
else -> {
ConnectivityType.ALWAYS
}
}
fun parseConnectivityType(screenCode: Int): ConnectivityType =
when (screenCode) {
ConnectivityType.ALWAYS.screenCode -> ConnectivityType.ALWAYS
ConnectivityType.WIFI_ONLY.screenCode -> ConnectivityType.WIFI_ONLY
ConnectivityType.NEVER.screenCode -> ConnectivityType.NEVER
else -> {
ConnectivityType.ALWAYS
}
}
fun parseFeatureSetType(screenCode: Int): FeatureSetType =
when (screenCode) {
FeatureSetType.COMPLETE.screenCode -> FeatureSetType.COMPLETE
FeatureSetType.SIMPLIFIED.screenCode -> FeatureSetType.SIMPLIFIED
FeatureSetType.PERFORMANCE.screenCode -> FeatureSetType.PERFORMANCE
else -> {
FeatureSetType.COMPLETE
}
}
fun parseGalleryType(screenCode: Int): ProfileGalleryType =
when (screenCode) {
ProfileGalleryType.CLASSIC.screenCode -> ProfileGalleryType.CLASSIC
ProfileGalleryType.MODERN.screenCode -> ProfileGalleryType.MODERN
else -> {
ProfileGalleryType.CLASSIC
}
}
enum class BooleanType(
val prefCode: Boolean?,
val screenCode: Int,
val reourceId: Int,
) {
ALWAYS(null, 0, R.string.connectivity_type_always),
NEVER(false, 1, R.string.connectivity_type_never),
}
fun parseBooleanType(code: Boolean?): BooleanType =
when (code) {
BooleanType.ALWAYS.prefCode -> BooleanType.ALWAYS
BooleanType.NEVER.prefCode -> BooleanType.NEVER
else -> {
BooleanType.ALWAYS
}
}
fun parseBooleanType(screenCode: Int): BooleanType =
when (screenCode) {
BooleanType.ALWAYS.screenCode -> BooleanType.ALWAYS
BooleanType.NEVER.screenCode -> BooleanType.NEVER
else -> {
BooleanType.ALWAYS
}
}
enum class WarningType(
val prefCode: Boolean?,
val screenCode: Int,
val resourceId: Int,
) {
WARN(null, 0, R.string.content_warning_see_warnings_option),
SHOW(true, 1, R.string.content_warning_show_all_sensitive_content_option),
HIDE(false, 2, R.string.content_warning_hide_all_sensitive_content_option),
}
fun parseWarningType(screenCode: Int): WarningType =
when (screenCode) {
WarningType.WARN.screenCode -> WarningType.WARN
WarningType.SHOW.screenCode -> WarningType.SHOW
WarningType.HIDE.screenCode -> WarningType.HIDE
else -> {
WarningType.WARN
}
}
fun parseWarningType(code: Boolean?): WarningType =
when (code) {
WarningType.WARN.prefCode -> WarningType.WARN
WarningType.HIDE.prefCode -> WarningType.HIDE
WarningType.SHOW.prefCode -> WarningType.SHOW
else -> {
WarningType.WARN
}
}

View File

@@ -0,0 +1,218 @@
/**
* 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 androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
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
import kotlinx.collections.immutable.toImmutableSet
class ThreadAssembler {
private fun searchRoot(
note: Note,
testedNotes: MutableSet<Note> = mutableSetOf(),
): Note? {
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
val noteEvent = note.event
if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) return note
testedNotes.add(note)
val markedAsRoot =
noteEvent
?.tags
?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }
?.getOrNull(1)
if (markedAsRoot != null) {
// Check to ssee if there is an error in the tag and the root has replies
if (LocalCache.getNoteIfExists(markedAsRoot)?.replyTo?.isEmpty() == true) {
return LocalCache.checkGetOrCreateNote(markedAsRoot)
}
}
val hasNoReplyTo =
note.replyTo?.lastOrNull {
it.replyTo?.isEmpty() == true
}
if (hasNoReplyTo != null) return hasNoReplyTo
// recursive
val roots =
note.replyTo?.mapNotNull {
if (it !in testedNotes) {
searchRoot(it, testedNotes)
} else {
null
}
}
if (roots != null && roots.isNotEmpty()) {
return roots[0]
}
return null
}
@Stable
class ThreadInfo(
val root: Note,
val allNotes: ImmutableSet<Note>,
)
fun findRoot(noteId: String): Note? {
val note = LocalCache.checkGetOrCreateNote(noteId) ?: return null
return if (note.event != null) {
val thread = OnlyLatestVersionSet()
searchRoot(note, thread) ?: note
} else {
note
}
}
fun findThreadFor(noteId: String): ThreadInfo? {
checkNotInMainThread()
val note = LocalCache.checkGetOrCreateNote(noteId) ?: return null
return if (note.event != null) {
val thread = OnlyLatestVersionSet()
val threadRoot = searchRoot(note, thread) ?: note
loadUp(note, thread)
loadDown(threadRoot, thread)
// adds the replies of the note in case the search for Root
// did not added them.
note.replies.forEach { loadDown(it, thread) }
ThreadInfo(
root = note,
allNotes = thread.toImmutableSet(),
)
} else {
ThreadInfo(
root = note,
allNotes = setOf(note).toImmutableSet(),
)
}
}
fun loadUp(
note: Note,
thread: MutableSet<Note>,
) {
if (note !in thread) {
thread.add(note)
note.replyTo?.forEach { loadUp(it, thread) }
}
}
fun loadDown(
note: Note,
thread: MutableSet<Note>,
) {
if (note !in thread) {
thread.add(note)
note.replies.forEach { loadDown(it, thread) }
}
}
}
class OnlyLatestVersionSet : MutableSet<Note> {
val map = hashMapOf<Address, Long>()
val set = hashSetOf<Note>()
override fun add(element: Note): Boolean {
val loadedCreatedAt = element.createdAt()
val noteEvent = element.event
return if (element is AddressableNote && loadedCreatedAt != null) {
innerAdd(element.address, element, loadedCreatedAt)
} else if (noteEvent is AddressableEvent && loadedCreatedAt != null) {
innerAdd(noteEvent.address(), element, loadedCreatedAt)
} else {
set.add(element)
}
}
private fun innerAdd(
address: Address,
element: Note,
loadedCreatedAt: Long,
): Boolean {
val existing = map.get(address)
return if (existing == null) {
map.put(address, loadedCreatedAt)
set.add(element)
} else {
if (loadedCreatedAt > existing) {
map.put(address, loadedCreatedAt)
set.add(element)
} else {
false
}
}
}
override fun addAll(elements: Collection<Note>): Boolean = elements.map { add(it) }.any()
override val size: Int
get() = set.size
override fun clear() {
set.clear()
map.clear()
}
override fun isEmpty(): Boolean = set.isEmpty()
override fun containsAll(elements: Collection<Note>): Boolean = set.containsAll(elements)
override fun contains(element: Note): Boolean = set.contains(element)
override fun iterator(): MutableIterator<Note> = set.iterator()
override fun retainAll(elements: Collection<Note>): Boolean = set.retainAll(elements)
override fun removeAll(elements: Collection<Note>): Boolean = elements.map { remove(it) }.any()
override fun remove(element: Note): Boolean {
element.address()?.let {
map.remove(it)
}
(element.event as? AddressableEvent)?.address()?.let {
map.remove(it)
}
return set.remove(element)
}
}

View File

@@ -0,0 +1,143 @@
/**
* 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.HexKey
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import java.lang.Long.min
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
data class LevelSignature(
val signature: String,
val createdAt: Long?,
val author: User?,
)
object ThreadLevelCalculator {
val levelFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss")
private fun formattedDateTime(timestamp: Long): String =
Instant
.ofEpochSecond(timestamp)
.atZone(ZoneId.systemDefault())
.format(levelFormatter)
/**
* This method caches signatures during each execution to avoid recalculation in longer threads
*/
fun replyLevelSignature(
note: Note,
eventsToConsider: Set<HexKey>,
cachedSignatures: MutableMap<Note, LevelSignature>,
account: User,
accountFollowingSet: Set<String>,
now: Long,
): LevelSignature {
val replyTo = note.replyTo
// estimates the min date by replies if it doesn't exist.
val createdAt =
note.createdAt() ?: min(
note.replies.minOfOrNull { it.createdAt() ?: now } ?: now,
note.reactions.values.minOfOrNull { it.minOfOrNull { it.createdAt() ?: now } ?: now } ?: now,
)
val noteAuthor = note.author
if (
note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()
) {
return LevelSignature(
signature = "/" + formattedDateTime(createdAt) + note.idHex.substring(0, 8) + ";",
createdAt = createdAt,
author = noteAuthor,
)
}
val parent =
(
replyTo
.filter {
it.idHex in eventsToConsider
} // This forces the signature to be based on a branch, avoiding two roots
.map {
cachedSignatures[it]
?: replyLevelSignature(
it,
eventsToConsider,
cachedSignatures,
account,
accountFollowingSet,
now,
).apply { cachedSignatures.put(it, this) }
}.maxByOrNull { it.signature.length }
)
val parentSignature = parent?.signature?.removeSuffix(";") ?: ""
val threadOrder =
if (noteAuthor != null && parent?.author == noteAuthor) {
// author of the thread first, in **ascending** order
"9" + formattedDateTime((parent.createdAt ?: 0) + (now - createdAt)) + note.idHex.substring(0, 8)
} else if (noteAuthor != null && noteAuthor.pubkeyHex == account.pubkeyHex) {
"8" + formattedDateTime(createdAt) + note.idHex.substring(0, 8) // my replies
} else if (noteAuthor != null && noteAuthor.pubkeyHex in accountFollowingSet) {
"7" + formattedDateTime(createdAt) + note.idHex.substring(0, 8) // my follows replies.
} else {
"0" + formattedDateTime(createdAt) + note.idHex.substring(0, 8) // everyone else.
}
val mySignature =
LevelSignature(
signature = "$parentSignature/$threadOrder;",
createdAt = createdAt,
author = note.author,
)
cachedSignatures[note] = mySignature
return mySignature
}
fun replyLevel(
note: Note,
cachedLevels: MutableMap<Note, Int> = mutableMapOf(),
): Int {
val replyTo = note.replyTo
if (
note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()
) {
cachedLevels[note] = 0
return 0
}
val thisLevel =
replyTo.maxOf {
cachedLevels[it] ?: replyLevel(it, cachedLevels)
} + 1
cachedLevels[note] = thisLevel
return thisLevel
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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 android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.previews.UrlPreview
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import okhttp3.OkHttpClient
@Stable
object UrlCachedPreviewer {
var cache = LruCache<String, UrlPreviewState>(100)
private set
suspend fun previewInfo(
url: String,
okHttpClient: (String) -> OkHttpClient,
onReady: suspend (UrlPreviewState) -> Unit,
) {
cache[url]?.let {
onReady(it)
return
}
UrlPreview().fetch(
url,
okHttpClient,
onComplete = { urlInfo ->
cache[url]?.let {
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
onReady(it)
return@fetch
}
}
val state =
if (urlInfo.fetchComplete() && urlInfo.url == url) {
UrlPreviewState.Loaded(urlInfo)
} else {
UrlPreviewState.Empty
}
cache.put(url, state)
onReady(state)
},
onFailed = { throwable ->
cache[url]?.let {
onReady(it)
return@fetch
}
val state = UrlPreviewState.Error(throwable.message ?: "Error Loading url preview")
cache.put(url, state)
onReady(state)
},
)
}
}

View File

@@ -0,0 +1,383 @@
/**
* 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 androidx.compose.runtime.Immutable
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.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash
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
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.DualCase
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.containsAny
import kotlinx.coroutines.flow.MutableStateFlow
import java.math.BigDecimal
@Stable
class User(
val pubkeyHex: String,
) {
var info: UserMetadata? = null
var latestMetadata: MetadataEvent? = null
var latestMetadataRelay: NormalizedRelayUrl? = null
var latestContactList: ContactListEvent? = null
var reports = mapOf<User, Set<Note>>()
private set
var zaps = mapOf<Note, Note?>()
private set
var relaysBeingUsed = mapOf<NormalizedRelayUrl, RelayInfo>()
private set
fun pubkey() = Hex.decode(pubkeyHex)
fun pubkeyNpub() = pubkey().toNpub()
fun pubkeyDisplayHex() = pubkeyNpub().toShortDisplay()
fun dmInboxRelayList() = (LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(pubkeyHex))?.event as? ChatMessageRelayListEvent)
fun authorRelayList() = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)
fun toNProfile() = NProfile.create(pubkeyHex, relayHints())
fun outboxRelays() = authorRelayList()?.writeRelaysNorm()
fun relayHints() = authorRelayList()?.writeRelaysNorm()?.take(3) ?: listOfNotNull(latestMetadataRelay)
fun inboxRelays() = authorRelayList()?.readRelaysNorm()
fun dmInboxRelays() = dmInboxRelayList()?.relays()?.ifEmpty { null } ?: inboxRelays()
fun bestRelayHint() = authorRelayList()?.writeRelaysNorm()?.firstOrNull() ?: latestMetadataRelay
fun toPTag() = PTag(pubkeyHex, bestRelayHint())
fun toNostrUri() = "nostr:${toNProfile()}"
fun toBestShortFirstName(): String {
val fullName = toBestDisplayName()
val names = fullName.split(' ')
val firstName =
if (names[0].length <= 3) {
// too short. Remove Dr.
"${names[0]} ${names.getOrNull(1) ?: ""}"
} else {
names[0]
}
return firstName
}
fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex()
fun nip05(): String? = info?.nip05
fun profilePicture(): String? = info?.picture
fun updateContactList(event: ContactListEvent) {
if (event.id == latestContactList?.id) return
val oldContactListEvent = latestContactList
latestContactList = event
// Update following of the current user
flowSet?.follows?.invalidateData()
// Update Followers of the past user list
// Update Followers of the new contact list
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache
.getUserIfExists(it)
?.flowSet
?.followers
?.invalidateData()
}
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
LocalCache
.getUserIfExists(it)
?.flowSet
?.followers
?.invalidateData()
}
flowSet?.relays?.invalidateData()
}
fun addReport(note: Note) {
val author = note.author ?: return
val reportsBy = reports[author]
if (reportsBy == null) {
reports = reports + Pair(author, setOf(note))
flowSet?.reports?.invalidateData()
} else if (!reportsBy.contains(note)) {
reports = reports + Pair(author, reportsBy + note)
flowSet?.reports?.invalidateData()
}
}
fun removeReport(deleteNote: Note) {
val author = deleteNote.author ?: return
if (reports[author]?.contains(deleteNote) == true) {
reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote))
flowSet?.reports?.invalidateData()
}
}
}
fun addZap(
zapRequest: Note,
zap: Note?,
) {
if (zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap)
flowSet?.zaps?.invalidateData()
}
}
fun removeZap(zapRequestOrZapEvent: Note) {
if (zaps.containsKey(zapRequestOrZapEvent)) {
zaps = zaps.minus(zapRequestOrZapEvent)
flowSet?.zaps?.invalidateData()
} else if (zaps.containsValue(zapRequestOrZapEvent)) {
zaps = zaps.filter { it.value != zapRequestOrZapEvent }
flowSet?.zaps?.invalidateData()
}
}
fun zappedAmount(): BigDecimal {
var amount = BigDecimal.ZERO
zaps.forEach {
val itemValue = (it.value?.event as? LnZapEvent)?.amount
if (itemValue != null) {
amount += itemValue
}
}
return amount
}
fun reportsBy(user: User): Set<Note> = reports[user] ?: emptySet()
fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
fun reportsBy(users: Set<HexKey>): List<Note> =
reports
.mapNotNull {
if (it.key.pubkeyHex in users) {
it.value
} else {
null
}
}.flatten()
fun addRelayBeingUsed(
relay: NormalizedRelayUrl,
eventTime: Long,
) {
val here = relaysBeingUsed[relay]
if (here == null) {
relaysBeingUsed = relaysBeingUsed + Pair(relay, RelayInfo(relay, eventTime, 1))
} else {
if (eventTime > here.lastEvent) {
here.lastEvent = eventTime
}
here.counter++
}
flowSet?.relayInfo?.invalidateData()
}
fun updateUserInfo(
newUserInfo: UserMetadata,
latestMetadata: MetadataEvent,
) {
info = newUserInfo
info?.tags = latestMetadata.tags.toImmutableListOfLists()
info?.cleanBlankNames()
if (newUserInfo.lud16.isNullOrBlank()) {
info?.lud06?.let {
if (it.lowercase().startsWith("lnurl")) {
info?.lud16 = Lud06().toLud16(it)
}
}
}
flowSet?.metadata?.invalidateData()
}
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
fun isFollowingHashtag(tag: String) = latestContactList?.isTaggedHash(tag) ?: false
fun isFollowingGeohash(geoTag: String) = latestContactList?.isTaggedGeoHash(geoTag) ?: false
fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size
suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
fun hasReport(
loggedIn: User,
type: ReportType,
): Boolean =
reports[loggedIn]?.firstOrNull {
(it.event as? ReportEvent)?.reportedAuthor()?.any { it.type == type } ?: false
} != null
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
if (hiddenWordsCase.isEmpty()) return false
if (toBestDisplayName().containsAny(hiddenWordsCase)) {
return true
}
if (profilePicture()?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.banner?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.about?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.lud06?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.lud16?.containsAny(hiddenWordsCase) == true) {
return true
}
if (info?.nip05?.containsAny(hiddenWordsCase) == true) {
return true
}
return false
}
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
var flowSet: UserFlowSet? = null
@Synchronized
fun createOrDestroyFlowSync(create: Boolean) {
if (create) {
if (flowSet == null) {
flowSet = UserFlowSet(this)
}
} else {
if (flowSet != null && flowSet?.isInUse() == false) {
flowSet = null
}
}
}
fun flow(): UserFlowSet {
if (flowSet == null) {
createOrDestroyFlowSync(true)
}
return flowSet!!
}
fun clearFlow() {
if (flowSet != null && flowSet?.isInUse() == false) {
createOrDestroyFlowSync(false)
}
}
}
@Stable
class UserFlowSet(
u: User,
) {
// 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 zaps = UserBundledRefresherFlow(u)
val statuses = UserBundledRefresherFlow(u)
fun isInUse(): Boolean =
metadata.hasObservers() ||
relays.hasObservers() ||
follows.hasObservers() ||
followers.hasObservers() ||
reports.hasObservers() ||
relayInfo.hasObservers() ||
zaps.hasObservers() ||
statuses.hasObservers()
}
@Immutable
data class RelayInfo(
val url: NormalizedRelayUrl,
var lastEvent: Long,
var counter: Long,
)
@Stable
class UserBundledRefresherFlow(
val user: User,
) {
val stateFlow = MutableStateFlow(UserState(user))
fun invalidateData() {
stateFlow.tryEmit(UserState(user))
}
fun hasObservers() = stateFlow.subscriptionCount.value > 0
}
@Immutable
class UserState(
val user: User,
)

View File

@@ -0,0 +1,29 @@
/**
* 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.edits
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
class PrivateStorageRelayListDecryptionCache(
signer: NostrSigner,
) : GenericRelayListCache<PrivateOutboxRelayListEvent>(signer)

View File

@@ -0,0 +1,112 @@
/**
* 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.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 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 PrivateStorageRelayListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: PrivateStorageRelayListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getPrivateOutboxRelayListAddress() = PrivateOutboxRelayListEvent.createAddress(signer.pubKey)
fun getPrivateOutboxRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getPrivateOutboxRelayListAddress())
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = getPrivateOutboxRelayListNote().flow().metadata.stateFlow
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
suspend fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList
return event?.let { decryptionCache.relays(it) } ?: emptySet()
}
val flow =
getPrivateOutboxRelayListFlow()
.map { normalizePrivateOutboxRelayListWithBackup(it.note) }
.onStart {
emit(normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun saveRelayList(relays: List<NormalizedRelayUrl>): PrivateOutboxRelayListEvent {
val relayListForPrivateOutbox = getPrivateOutboxRelayList()
return if (relayListForPrivateOutbox != null) {
PrivateOutboxRelayListEvent.updateRelayList(
earlierVersion = relayListForPrivateOutbox,
relays = relays,
signer = signer,
)
} else {
PrivateOutboxRelayListEvent.create(
relays = relays,
signer = signer,
)
}
}
init {
settings.backupPrivateHomeRelayList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
}
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start")
getPrivateOutboxRelayListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}")
(noteState.note.event as? PrivateOutboxRelayListEvent)?.let {
settings.updatePrivateHomeRelayList(it)
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
/**
* 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.emphChat
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
@Stable
class EphemeralChatChannel(
val roomId: RoomId,
) : Channel() {
override fun relays() = setOf(roomId.relayUrl)
override fun toBestDisplayName() = roomId.toDisplayKey()
fun anyNameStartsWith(prefix: String): Boolean = roomId.id.contains(prefix, true)
}

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.emphChat
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.experimental.ephemChat.list.roomSet
import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
class EphemeralChatListDecryptionCache(
val signer: NostrSigner,
) {
val cachedPrivateLists = PrivateTagArrayEventCache<EphemeralChatListEvent>(signer)
fun cachedRoomSet(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).roomSet()
fun cachedRooms(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).rooms()
suspend fun roomSet(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagList(event).roomSet()
suspend fun rooms(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagList(event).rooms()
}

View File

@@ -0,0 +1,130 @@
/**
* 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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
class EphemeralChatListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: EphemeralChatListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getEphemeralChatListAddress() = EphemeralChatListEvent.createAddress(signer.pubKey)
fun getEphemeralChatListNote(): AddressableNote = cache.getOrCreateAddressableNote(getEphemeralChatListAddress())
fun getEphemeralChatListFlow(): StateFlow<NoteState> = getEphemeralChatListNote().flow().metadata.stateFlow
fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent
suspend fun ephemeralChatListWithBackup(note: Note): Set<RoomId> {
val event = note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList
return event?.let { decryptionCache.roomSet(it) } ?: emptySet()
}
@OptIn(ExperimentalCoroutinesApi::class)
val liveEphemeralChatList: StateFlow<Set<RoomId>> =
getEphemeralChatListFlow()
.transformLatest { noteState ->
emit(ephemeralChatListWithBackup(noteState.note))
}.onStart {
emit(ephemeralChatListWithBackup(getEphemeralChatListNote()))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun follow(channel: EphemeralChatChannel): EphemeralChatListEvent {
val ephemeralChatList = getEphemeralChatList()
return if (ephemeralChatList == null) {
EphemeralChatListEvent.create(
room = channel.roomId,
isPrivate = true,
signer = signer,
)
} else {
EphemeralChatListEvent.add(
earlierVersion = ephemeralChatList,
room = channel.roomId,
isPrivate = true,
signer = signer,
)
}
}
suspend fun unfollow(channel: EphemeralChatChannel): EphemeralChatListEvent? {
val ephemeralChatList = getEphemeralChatList()
return if (ephemeralChatList != null) {
EphemeralChatListEvent.remove(
earlierVersion = ephemeralChatList,
room = channel.roomId,
signer = signer,
)
} else {
null
}
}
init {
settings.backupEphemeralChatList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
}
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start")
getEphemeralChatListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "EphemeralChatList List for ${signer.pubKey}")
(noteState.note.event as? EphemeralChatListEvent)?.let {
settings.updateEphemeralChatListTo(it)
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.localRelays
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class LocalRelayListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun normalizeLocalRelayListWithBackup(relayList: Set<String>): Set<NormalizedRelayUrl> = relayList.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }.toSet()
val flow =
settings.localRelayServers
.map { normalizeLocalRelayListWithBackup(it) }
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
normalizeLocalRelayListWithBackup(settings.localRelayServers.value),
)
fun saveRelayList(
relays: List<NormalizedRelayUrl>,
onDone: () -> Unit,
) {
settings.updateLocalRelayServers(relays.map { it.url }.toSet())
onDone()
}
}

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.nip01UserMetadata
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
class AccountHomeRelayState(
nip65: Nip65RelayListState,
privateStorage: PrivateStorageRelayListState,
local: LocalRelayListState,
scope: CoroutineScope,
) {
val flow =
combine(
nip65.outboxFlow,
privateStorage.flow,
local.flow,
) { nip65Outbox, privateOutBox, localRelays ->
nip65Outbox + privateOutBox + localRelays
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
nip65.outboxFlow.value +
privateStorage.flow.value +
local.flow.value,
)
}

View File

@@ -0,0 +1,58 @@
/**
* 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.nip01UserMetadata
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
class AccountOutboxRelayState(
nip65: Nip65RelayListState,
privateStorage: PrivateStorageRelayListState,
local: LocalRelayListState,
broadcast: BroadcastRelayListState,
scope: CoroutineScope,
) {
val flow =
combine(
nip65.outboxFlow,
privateStorage.flow,
local.flow,
broadcast.flow,
) { nip65Outbox, privateOutBox, localRelays, broadcastRelays ->
nip65Outbox + privateOutBox + localRelays + broadcastRelays
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
nip65.outboxFlow.value +
privateStorage.flow.value +
local.flow.value +
broadcast.flow.value,
)
}

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.nip01UserMetadata
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
class NotificationInboxRelayState(
nip65RelayList: Nip65RelayListState,
localRelayList: LocalRelayListState,
scope: CoroutineScope,
) {
val flow =
combine(
nip65RelayList.inboxFlow,
localRelayList.flow,
) { nip65Inbox, localRelays ->
nip65Inbox + localRelays
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
nip65RelayList.inboxFlow.value + localRelayList.flow.value,
)
}

View File

@@ -0,0 +1,123 @@
/**
* 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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class UserMetadataState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
fun getUserMetadataUser(): User = cache.getOrCreateUser(signer.pubKey)
fun getUserMetadataFlow(): StateFlow<UserState> = getUserMetadataUser().flow().metadata.stateFlow
fun getUserMetadataEvent(): MetadataEvent? = getUserMetadataUser().latestMetadata
suspend fun sendNewUserMetadata(
name: String? = null,
picture: String? = null,
banner: String? = null,
website: String? = null,
pronouns: String? = null,
about: String? = null,
nip05: String? = null,
lnAddress: String? = null,
lnURL: String? = null,
twitter: String? = null,
mastodon: String? = null,
github: String? = null,
): MetadataEvent {
val latest = getUserMetadataEvent()
val template =
if (latest != null) {
MetadataEvent.updateFromPast(
latest = latest,
name = name,
displayName = name,
picture = picture,
banner = banner,
website = website,
pronouns = pronouns,
about = about,
nip05 = nip05,
lnAddress = lnAddress,
lnURL = lnURL,
twitter = twitter,
mastodon = mastodon,
github = github,
)
} else {
MetadataEvent.createNew(
name = name,
displayName = name,
picture = picture,
banner = banner,
website = website,
pronouns = pronouns,
about = about,
nip05 = nip05,
lnAddress = lnAddress,
lnURL = lnURL,
twitter = twitter,
mastodon = mastodon,
github = github,
)
}
return signer.sign(template)
}
init {
settings.backupUserMetadata?.let {
Log.d("AccountRegisterObservers", "Loading saved user metadata ${it.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
// saves contact list for the next time.
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
getUserMetadataFlow().collect {
Log.d("AccountRegisterObservers", "Updating Kind 0 ${it.user.toBestDisplayName()}")
settings.updateUserMetadata(it.user.latestMetadata)
}
}
}
}

View File

@@ -0,0 +1,143 @@
/**
* 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.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.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
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
import kotlin.collections.flatten
class FollowListOutboxOrProxyRelays(
kind3Follows: FollowListState,
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)))
}.onStart {
emit(
kind3Follows.flow.value.authors
.mapNotNull {
getNIP65RelayList(it)?.writeRelaysNorm()
}.flatten()
.toSet(),
)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.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.Default)
.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.Default)
.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.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
}

View File

@@ -0,0 +1,161 @@
/**
* 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 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.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 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.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
class FollowListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
fun getFollowListUser(): User = cache.getOrCreateUser(signer.pubKey)
fun getFollowListFlow(): StateFlow<UserState> = getFollowListUser().flow().follows.stateFlow
fun getFollowListEvent(): ContactListEvent? = getFollowListUser().latestContactList
@OptIn(ExperimentalCoroutinesApi::class)
private val innerFlow: Flow<Kind3Follows> =
getFollowListFlow().transformLatest {
emit(buildKind3Follows(it.user.latestContactList ?: settings.backupContactList))
}
val flow =
innerFlow
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
// this has priority.
buildKind3Follows(getFollowListEvent() ?: settings.backupContactList),
)
/**
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) }
}
fun buildKind3Follows(latestContactList: ContactListEvent?): Kind3Follows {
// makes sure the output include only valid p tags
val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
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(),
)
}
suspend fun follow(user: User): ContactListEvent {
val contactList = getFollowListEvent()
return if (contactList != null) {
ContactListEvent.followUser(contactList, user.pubkeyHex, signer)
} else {
ContactListEvent.createFromScratch(
followUsers = listOf(ContactTag(user.pubkeyHex, user.bestRelayHint(), null)),
relayUse = emptyMap(),
signer = signer,
)
}
}
suspend fun unfollow(user: User): ContactListEvent? {
val contactList = getFollowListEvent()
return if (contactList != null && contactList.tags.isNotEmpty()) {
ContactListEvent.unfollowUser(
contactList,
user.pubkeyHex,
signer,
)
} else {
null
}
}
init {
settings.backupContactList?.let {
Log.d("AccountRegisterObservers", "Loading saved contacts ${it.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
// saves contact list for the next time.
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
getFollowListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}")
settings.updateContactListTo(it.user.latestContactList)
}
}
}
}

View File

@@ -0,0 +1,147 @@
/**
* 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.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.ui.navigation.navs.EmptyNav.scope
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
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
import kotlin.collections.ifEmpty
class FollowsPerOutboxRelay(
kind3Follows: FollowListState,
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)))
}.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)
}
}
},
)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyMap(),
)
val outboxPerRelayMinusBlockedFlow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
combine(outboxPerRelayFlow, blockedRelayList.flow) { followList, blockedRelays ->
followList.minus(blockedRelays)
}.onStart {
emit(outboxPerRelayFlow.value.minus(blockedRelayList.flow.value.toSet()))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyMap(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
proxyRelayList.flow
.flatMapLatest { proxyRelays ->
if (proxyRelays.isEmpty()) {
outboxPerRelayMinusBlockedFlow
} else {
kind3Follows.flow.map { follows ->
proxyRelays.associateWith { follows.authors }
}
}
}.onStart {
if (proxyRelayList.flow.value.isEmpty()) {
emit(outboxPerRelayMinusBlockedFlow.value)
} else {
emit(proxyRelayList.flow.value.associateWith { kind3Follows.flow.value.authors })
}
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyMap(),
)
}

View File

@@ -0,0 +1,89 @@
/**
* 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 android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.update
import java.util.Base64
class OtsState(
val signer: NostrSigner,
val cache: LocalCache,
val otsResolver: OkHttpOtsResolverBuilder,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
suspend fun updateAttestations(): List<OtsEvent> {
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
return settings.pendingAttestations.value.toList().mapNotNull { (key, value) ->
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver.build())
if (otsState != null) {
val hint = cache.getNoteIfExists(key)?.toEventHint<Event>()
val template =
if (hint != null) {
OtsEvent.build(hint, otsState)
} else {
OtsEvent.build(key, otsState)
}
val otsEvent = signer.sign(template)
settings.pendingAttestations.update { it - key }
otsEvent
} else {
null
}
}
}
fun hasPendingAttestations(note: Note): Boolean {
val id = note.event?.id ?: note.idHex
return settings.pendingAttestations.value[id] != null
}
fun timestamp(note: Note) {
if (note.isDraft()) return
val id = note.event?.id ?: note.idHex
settings.addPendingAttestation(
id = id,
stamp =
Base64.getEncoder().encodeToString(
OtsEvent.stamp(
id,
otsResolver.build(),
),
),
)
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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.nip17Dms
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
class DmInboxRelayState(
// main relay
dmRelayList: DmRelayListState,
// backup relays
nip65RelayList: Nip65RelayListState,
privateOutbox: PrivateStorageRelayListState,
localRelayList: LocalRelayListState,
scope: CoroutineScope,
) {
val flow =
combine(
nip65RelayList.inboxFlow,
dmRelayList.flow,
privateOutbox.flow,
localRelayList.flow,
) { nip65Inbox, dmRelayList, privateOutBox, localRelays ->
nip65Inbox + dmRelayList + privateOutBox + localRelays
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
nip65RelayList.inboxFlow.value +
dmRelayList.flow.value +
privateOutbox.flow.value +
localRelayList.flow.value,
)
}

View File

@@ -0,0 +1,109 @@
/**
* 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.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 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 DmRelayListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getDMRelayListAddress() = ChatMessageRelayListEvent.createAddress(signer.pubKey)
fun getDMRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getDMRelayListAddress())
fun getDMRelayListFlow(): StateFlow<NoteState> = getDMRelayListNote().flow().metadata.stateFlow
fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent
fun normalizeDMRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList
return event?.relays()?.toSet() ?: emptySet()
}
val flow =
getDMRelayListFlow()
.map { normalizeDMRelayListWithBackup(it.note) }
.onStart { emit(normalizeDMRelayListWithBackup(getDMRelayListNote())) }
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun saveRelayList(dmRelays: List<NormalizedRelayUrl>): ChatMessageRelayListEvent {
val relayListForDMs = getDMRelayList()
return if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) {
ChatMessageRelayListEvent.updateRelayList(
earlierVersion = relayListForDMs,
relays = dmRelays,
signer = signer,
)
} else {
ChatMessageRelayListEvent.create(
relays = dmRelays,
signer = signer,
)
}
}
init {
settings.backupDMRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved DM Relay List ${it.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(it)
}
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start")
getDMRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating DM Relay List for ${signer.pubKey}")
(it.note.event as? ChatMessageRelayListEvent)?.let {
settings.updateDMRelayList(it)
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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.nip18Reposts
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
class RepostAction {
companion object {
suspend fun repost(
note: Note,
signer: NostrSigner,
): Event? {
val noteEvent = note.event ?: return null
if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) {
// has already bosted in the past 5mins
return null
}
val noteHint = note.relayHintUrl()
val authorHint = note.author?.bestRelayHint()
val template =
if (noteEvent.kind == 1) {
RepostEvent.build(noteEvent, noteHint, authorHint)
} else {
GenericRepostEvent.build(noteEvent, noteHint, authorHint)
}
return signer.sign(template)
}
}
}

View File

@@ -0,0 +1,103 @@
/**
* 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.nip25Reactions
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
class ReactionAction {
companion object {
suspend fun reactTo(
note: Note,
reaction: String,
by: User,
signer: NostrSigner,
onPublic: (ReactionEvent) -> Unit,
onPrivate: suspend (NIP17Factory.Result) -> Unit,
) {
if (!signer.isWriteable()) return
if (note.hasReacted(by, reaction)) {
// has already liked this note
return
}
val noteEvent = note.event
if (noteEvent is NIP17Group) {
val users = noteEvent.groupMembers().toList()
if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrlTag.decode(reaction)
if (emojiUrl != null) {
note.toEventHint<Event>()?.let {
onPrivate(
NIP17Factory().createReactionWithinGroup(
emojiUrl = emojiUrl,
originalNote = it,
to = users,
signer = signer,
),
)
}
return
}
}
note.toEventHint<Event>()?.let {
onPrivate(
NIP17Factory().createReactionWithinGroup(
content = reaction,
originalNote = it,
to = users,
signer = signer,
),
)
}
return
} else {
if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrlTag.decode(reaction)
if (emojiUrl != null) {
note.event?.let {
val template = ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl()))
onPublic(signer.sign(template))
}
return
}
}
note.toEventHint<Event>()?.let {
onPublic(signer.sign(ReactionEvent.build(reaction, it)))
}
}
}
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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.nip28PublicChats
import androidx.compose.runtime.Stable
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.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
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm
@Stable
class PublicChatChannel(
val idHex: String,
) : Channel() {
var creator: User? = null
var event: ChannelCreateEvent? = null
var eventNote: Note? = null
var info = ChannelDataNorm(null, null, null, null)
var infoTags = EmptyTagList
var updatedMetadataAt: Long = 0
override fun relays() = info.relays?.toSet() ?: super.relays()
fun relayHintUrls() = relays().take(3)
fun relayHintUrl() = relays().firstOrNull()
fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, relayHintUrls())
fun toNostrUri() = "nostr:${toNEvent()}"
fun toEventHint() = event?.let { EventHintBundle(it, relayHintUrl(), null) }
fun toEventId() = EventIdHintOptional(idHex, relayHintUrl())
fun updateChannelInfo(
creator: User,
event: ChannelCreateEvent,
eventNote: Note? = null,
) {
this.creator = creator
this.event = event
this.info = event.channelInfo()
this.infoTags = event.tags.toImmutableListOfLists()
this.updatedMetadataAt = event.createdAt
this.eventNote = eventNote
updateChannelInfo()
}
fun updateChannelInfo(
creator: User,
event: ChannelMetadataEvent,
) {
this.creator = creator
this.info = event.channelInfo()
this.infoTags = event.tags.toImmutableListOfLists()
this.updatedMetadataAt = event.createdAt
super.updateChannelInfo()
}
override fun toBestDisplayName(): String = info.name ?: toNEvent().toShortDisplay()
fun summary(): String? = info.about
fun profilePicture(): String? {
if (info.picture.isNullOrBlank()) return creator?.info?.banner
return info.picture ?: creator?.info?.banner
}
fun anyNameStartsWith(prefix: String): Boolean =
idHex.startsWith(prefix) ||
info.name?.contains(prefix, true) == true ||
info.about?.contains(prefix, true) == true
}

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.nip28PublicChats
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.channelSet
import com.vitorpamplona.quartz.nip28PublicChat.list.channels
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
class PublicChatListDecryptionCache(
val signer: NostrSigner,
) {
val cachedPrivateLists = PrivateTagArrayEventCache<ChannelListEvent>(signer)
fun cachedChannelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channelSet()
fun cachedChannels(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channels()
suspend fun channelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channelSet()
suspend fun channels(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channels()
}

View File

@@ -0,0 +1,145 @@
/**
* 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.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
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.flow.transformLatest
import kotlinx.coroutines.launch
class PublicChatListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: PublicChatListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey)
fun getChannelListNote(): AddressableNote = cache.getOrCreateAddressableNote(getChannelListAddress())
fun getChannelListFlow(): StateFlow<NoteState> = getChannelListNote().flow().metadata.stateFlow
fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent
suspend fun publicChatListWithBackup(note: Note): Set<ChannelTag> {
val event = note.event as? ChannelListEvent ?: settings.backupChannelList
return event?.let { decryptionCache.channelSet(it) } ?: emptySet()
}
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<Set<ChannelTag>> =
getChannelListFlow()
.transformLatest { noteState ->
emit(publicChatListWithBackup(noteState.note))
}.onStart {
emit(publicChatListWithBackup(getChannelListNote()))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val flowSet: StateFlow<Set<HexKey>> =
flow
.map {
it.mapTo(mutableSetOf()) { it.eventId }
}.onStart {
emit(flow.value.mapTo(mutableSetOf()) { it.eventId })
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun follow(channel: PublicChatChannel): ChannelListEvent {
val publicChatList = getChannelList()
return if (publicChatList == null) {
ChannelListEvent.create(ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer)
} else {
ChannelListEvent.add(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer)
}
}
suspend fun follow(channels: List<PublicChatChannel>): ChannelListEvent {
val publicChatList = getChannelList()
val channelTags = channels.map { ChannelTag(it.idHex, it.relayHintUrl()) }
return if (publicChatList == null) {
ChannelListEvent.create(channelTags, true, signer)
} else {
ChannelListEvent.add(publicChatList, channelTags, true, signer)
}
}
suspend fun unfollow(channel: PublicChatChannel): ChannelListEvent? {
val publicChatList = getChannelList()
return if (publicChatList != null) {
ChannelListEvent.remove(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), signer)
} else {
null
}
}
init {
settings.backupChannelList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
}
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "Channel List Collector Start")
getChannelListFlow().collect {
Log.d("AccountRegisterObservers", "Channel List for ${signer.pubKey}")
(it.note.event as? ChannelListEvent)?.let {
settings.updateChannelListTo(it)
}
}
}
}
}

View File

@@ -0,0 +1,155 @@
/**
* 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.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.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis
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.combineTransform
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 EmojiPackState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
class EmojiMedia(
val code: String,
val link: MediaUrlImage,
)
fun getEmojiPackSelectionAddress() = EmojiPackSelectionEvent.createAddress(signer.pubKey)
fun getEmojiPackSelection(): EmojiPackSelectionEvent? = getEmojiPackSelectionNote().event as? EmojiPackSelectionEvent
fun getEmojiPackSelectionFlow(): StateFlow<NoteState> = getEmojiPackSelectionNote().flow().metadata.stateFlow
fun getEmojiPackSelectionNote(): AddressableNote = cache.getOrCreateAddressableNote(getEmojiPackSelectionAddress())
fun convertEmojiSelectionPack(selection: EmojiPackSelectionEvent?): List<StateFlow<NoteState>>? =
selection?.taggedAddresses()?.map {
cache
.getOrCreateAddressableNote(it)
.flow()
.metadata.stateFlow
}
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<List<StateFlow<NoteState>>?> =
getEmojiPackSelectionFlow()
.transformLatest {
emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent))
}.onStart {
emit(convertEmojiSelectionPack(getEmojiPackSelection()))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
fun convertEmojiPack(pack: EmojiPackEvent): List<EmojiMedia> =
pack.taggedEmojis().map {
EmojiMedia(it.code, MediaUrlImage(it.url))
}
fun mergePack(list: Array<NoteState>): List<EmojiMedia> =
list
.mapNotNull {
val ev = it.note.event as? EmojiPackEvent
if (ev != null) {
convertEmojiPack(ev)
} else {
null
}
}.flatten()
.distinctBy { it.link }
@OptIn(ExperimentalCoroutinesApi::class)
val myEmojis =
flow
.transformLatest { emojiList ->
if (emojiList != null) {
emitAll(
combineTransform(emojiList) {
emit(mergePack(it))
},
)
} else {
emit(emptyList())
}
}.onStart {
emit(
mergePack(
convertEmojiSelectionPack(
getEmojiPackSelection(),
)?.map { it.value }?.toTypedArray() ?: emptyArray(),
),
)
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
suspend fun addEmojiPack(emojiPack: Note): EmojiPackSelectionEvent {
val emojiPackEvent = emojiPack.event
if (emojiPackEvent !is EmojiPackEvent) throw IllegalArgumentException("Cannot add an emoji pack to this kind of event.")
val eventHint = emojiPack.toEventHint<EmojiPackEvent>() ?: throw IllegalArgumentException("Cannot add an emoji pack to this kind of event.")
val usersEmojiList = getEmojiPackSelection()
return if (usersEmojiList == null) {
val template = EmojiPackSelectionEvent.build(listOf(eventHint))
signer.sign(template)
} else {
val template = EmojiPackSelectionEvent.add(usersEmojiList, eventHint)
signer.sign(template)
}
}
suspend fun removeEmojiPack(emojiPack: Note): EmojiPackSelectionEvent? {
val usersEmojiList = getEmojiPackSelection() ?: throw IllegalArgumentException("Cannot remove an emoji pack to this kind of event.")
val emojiPackEvent = emojiPack.event
if (emojiPackEvent !is EmojiPackEvent) return null
val template = EmojiPackSelectionEvent.remove(usersEmojiList, emojiPackEvent)
return signer.sign(template)
}
}

View File

@@ -0,0 +1,62 @@
/**
* 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.nip38UserStatuses
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
class UserStatusAction {
companion object {
suspend fun create(
newStatus: String,
signer: NostrSigner,
): StatusEvent = StatusEvent.create(newStatus, "general", expiration = null, signer)
suspend fun update(
oldStatus: AddressableNote,
newStatus: String,
signer: NostrSigner,
): StatusEvent {
val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event")
return StatusEvent.update(oldEvent, newStatus, signer)
}
suspend fun delete(
oldStatus: AddressableNote,
signer: NostrSigner,
): List<Event> {
val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event")
val event = StatusEvent.clear(oldEvent, signer)
val deletion =
signer.sign(
DeletionEvent.buildForVersionOnly(listOf(event)),
)
return listOf(event, deletion)
}
}
}

View File

@@ -0,0 +1,181 @@
/**
* 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.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.NWCPaymentQueryState
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectRequestCache
import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectResponseCache
import com.vitorpamplona.quartz.nip47WalletConnect.Request
import com.vitorpamplona.quartz.nip47WalletConnect.Response
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
* Manages NIP-47 (Nostr Wallet Connect) related signing operations and decryption cache for a given account.
*
* Key Responsibilities:
*
* - Dynamically creates a NIP-47 signer if the wallet setup changes in the account settings.
* - Provides decryption caches to manage decrypted NIP-47 requests and responses efficiently.
* - Handles creating of zap payment requests and waits for responses.
*
* @property signer the main Nostr signer used for general Nostr operations
* @property cache the local cache for handling notes and events
* @property scope the coroutine scope used for async operations
* @property settings the account settings containing NIP-47 configuration
*/
class NwcSignerState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
/**
* Derives a NIP-47 signer from the zap payment request in settings.
* If there's no valid configuration, it defaults to the main signer.
* Flows updates whenever settings change.
*/
val nip47Signer =
settings.zapPaymentRequest
.map {
buildSigner(it) ?: signer
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
buildSigner(settings.zapPaymentRequest.value) ?: signer,
)
/**
* Creates a dedicated request decryption cache for the NIP-47 signer.
* Flows updates whenever the signer changes.
*/
val zapPaymentRequestDecryptionCache =
nip47Signer
.map {
NostrWalletConnectRequestCache(it)
}.flowOn(Dispatchers.Default)
.stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectRequestCache(nip47Signer.value))
/**
* Creates a dedicated response decryption cache for the NIP-47 signer.
* Flows updates whenever the signer changes.
*/
val zapPaymentResponseDecryptionCache =
nip47Signer
.map {
NostrWalletConnectResponseCache(it)
}.flowOn(Dispatchers.Default)
.stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectResponseCache(nip47Signer.value))
fun buildSigner(uri: Nip47WalletConnect.Nip47URINorm?) =
uri?.secret?.hexToByteArray()?.let {
NostrSignerInternal(KeyPair(it))
}
fun hasWalletConnectSetup(): Boolean = settings.zapPaymentRequest.value != null
fun isNIP47Author(pubkeyHex: String?): Boolean = nip47Signer.value.pubKey == pubkeyHex
/**
* Decrypts a NIP-47 payment request using the current signer.
*
* @param nwcRequest the NIP-47 payment request event to decrypt
* @return the decrypted request or null if not set up or decryption fails
*/
suspend fun decryptRequest(nwcRequest: LnZapPaymentRequestEvent): Request? {
if (!hasWalletConnectSetup()) return null
return zapPaymentRequestDecryptionCache.value.decryptRequest(nwcRequest)
}
/**
* Decrypts a NIP-47 payment response using the current signer.
*
* @param nwsResponse the NIP-47 payment response event to decrypt
* @return the decrypted response or null if not set up or decryption fails
*/
suspend fun decryptResponse(nwsResponse: LnZapPaymentResponseEvent): Response? {
if (!hasWalletConnectSetup()) return null
return zapPaymentResponseDecryptionCache.value.decryptResponse(nwsResponse)
}
/**
* Sends a zap payment request to a connected Lightning wallet.
* Subscribes to responses and waits up to 60s for a reply.
*
* @param bolt11 the BOLT-11 invoice to pay
* @param zappedNote the note being zapped (if any)
* @param onResponse callback to handle the response from the wallet
* @return a pair containing the payment request event and target relay URL
* @throws IllegalArgumentException if no NIP-47 wallet is set up
*/
suspend fun sendZapPaymentRequestFor(
bolt11: String,
zappedNote: Note?,
onResponse: (Response?) -> Unit,
): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> {
val walletService = settings.zapPaymentRequest.value
if (walletService == null) throw IllegalArgumentException("No NIP47 setup")
val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, nip47Signer.value)
val filter =
NWCPaymentQueryState(
fromServiceHex = walletService.pubKeyHex,
toUserHex = event.pubKey,
replyingToHex = event.id,
relay = walletService.relayUri,
)
Amethyst.instance.sources.nwc
.subscribe(filter)
scope.launch(Dispatchers.IO) {
delay(60000) // waits 1 minute to complete payment.
Amethyst.instance.sources.nwc
.unsubscribe(filter)
}
cache.consume(event, zappedNote, true, walletService.relayUri) {
onResponse(decryptResponse(it))
}
return Pair(event, walletService.relayUri)
}
}

View File

@@ -0,0 +1,276 @@
/**
* 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
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.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
class BookmarkListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
class BookmarkList(
val public: List<Note> = emptyList(),
val private: List<Note> = emptyList(),
)
fun getBookmarkListAddress() = BookmarkListEvent.createBookmarkAddress(signer.pubKey)
fun getBookmarkListNote() = cache.getOrCreateAddressableNote(getBookmarkListAddress())
fun getBookmarkListFlow(): StateFlow<NoteState> = getBookmarkListNote().flow().metadata.stateFlow
fun getBookmarkList(): BookmarkListEvent? = getBookmarkListNote().event as? BookmarkListEvent
suspend fun publicBookmarks(note: Note): List<BookmarkIdTag> {
val noteEvent = note.event as? BookmarkListEvent
return noteEvent?.publicBookmarks() ?: emptyList()
}
suspend fun privateBookmarks(note: Note): List<BookmarkIdTag> {
val noteEvent = note.event as? BookmarkListEvent
return noteEvent?.privateBookmarks(signer) ?: emptyList()
}
@OptIn(FlowPreview::class)
val publicBookmarks: StateFlow<List<BookmarkIdTag>> =
getBookmarkListFlow()
.map { noteState ->
publicBookmarks(noteState.note)
}.onStart {
emit(publicBookmarks(getBookmarkListNote()))
}.debounce(100)
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
@OptIn(FlowPreview::class)
val privateBookmarks: StateFlow<List<BookmarkIdTag>> =
getBookmarkListFlow()
.map { noteState ->
privateBookmarks(noteState.note)
}.onStart {
emit(privateBookmarks(getBookmarkListNote()))
}.debounce(100)
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val publicBookmarkEventIdSet =
publicBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is EventBookmark) it.eventId else null
}.toSet()
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val publicBookmarkAddressIdSet =
publicBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is AddressBookmark) it.address else null
}.toSet()
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val privateBookmarkEventIdSet =
privateBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is EventBookmark) it.eventId else null
}.toSet()
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val privateBookmarkAddressIdSet =
privateBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is AddressBookmark) it.address else null
}.toSet()
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
fun bookmarkList(
privateBookmarks: List<BookmarkIdTag>,
publicBookmarks: List<BookmarkIdTag>,
): BookmarkList =
BookmarkList(
public =
publicBookmarks
.mapNotNull {
when (it) {
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
}
}.reversed(),
private =
privateBookmarks
.mapNotNull {
when (it) {
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
}
}.reversed(),
)
@OptIn(FlowPreview::class)
val bookmarks: StateFlow<BookmarkList> =
combineTransform(privateBookmarks, publicBookmarks) { private, public ->
emit(bookmarkList(private, public))
}.onStart {
emit(bookmarkList(privateBookmarks.value, publicBookmarks.value))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
BookmarkList(),
)
fun isInPrivateBookmarks(note: Note): Boolean {
if (!signer.isWriteable()) return false
return if (note is AddressableNote) {
privateBookmarkAddressIdSet.value.contains(note.address)
} else {
privateBookmarkEventIdSet.value.contains(note.idHex)
}
}
fun isInPublicBookmarks(note: Note): Boolean =
if (note is AddressableNote) {
publicBookmarkAddressIdSet.value.contains(note.address)
} else {
publicBookmarkEventIdSet.value.contains(note.idHex)
}
suspend fun addBookmark(
note: Note,
isPrivate: Boolean,
): BookmarkListEvent {
val bookmarkList = getBookmarkList()
return if (bookmarkList == null) {
if (note is AddressableNote) {
BookmarkListEvent.create(
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.create(
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
} else {
if (note is AddressableNote) {
BookmarkListEvent.add(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.add(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
}
}
suspend fun removeBookmark(
note: Note,
isPrivate: Boolean,
): BookmarkListEvent? {
val bookmarkList = getBookmarkList()
return if (bookmarkList != null) {
if (note is AddressableNote) {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
} else {
null
}
}
}

View File

@@ -0,0 +1,118 @@
/**
* 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
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.nip01Core.core.HexKey
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.DualCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class HiddenUsersState(
val muteList: StateFlow<List<MuteTag>>,
val blockList: StateFlow<List<MuteTag>>,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
@Immutable
class LiveHiddenUsers(
val hiddenUsers: Set<String>,
val spammers: Set<String>,
val hiddenWords: Set<String>,
val showSensitiveContent: Boolean?,
) {
// speeds up isHidden calculations
val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() }
val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() }
val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) }
fun isUserHidden(userHex: HexKey) = hiddenUsers.contains(userHex) || spammers.contains(userHex)
}
suspend fun assembleLiveHiddenUsers(
blockList: List<MuteTag>,
muteList: List<MuteTag>,
transientHiddenUsers: Set<String>,
showSensitiveContent: Boolean?,
): LiveHiddenUsers =
LiveHiddenUsers(
hiddenUsers = blockList.mapNotNullTo(mutableSetOf()) { if (it is UserTag) it.pubKey else null } + muteList.mapNotNull { if (it is UserTag) it.pubKey else null },
hiddenWords = blockList.mapNotNullTo(mutableSetOf()) { if (it is WordTag) it.word else null } + muteList.mapNotNull { if (it is WordTag) it.word else null },
spammers = transientHiddenUsers,
showSensitiveContent = showSensitiveContent,
)
val flow: StateFlow<LiveHiddenUsers> =
combineTransform(
blockList,
muteList,
transientHiddenUsers,
settings.syncedSettings.security.showSensitiveContent,
) { blockList, muteList, transientHiddenUsers, showSensitiveContent ->
checkNotInMainThread()
emit(assembleLiveHiddenUsers(blockList, muteList, transientHiddenUsers, showSensitiveContent))
}.onStart {
emit(
assembleLiveHiddenUsers(
blockList.value,
muteList.value,
transientHiddenUsers.value,
settings.syncedSettings.security.showSensitiveContent.value,
),
)
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
LiveHiddenUsers(emptySet(), emptySet(), emptySet(), null),
)
fun resetTransientUsers() {
transientHiddenUsers.update {
emptySet()
}
}
fun showUser(pubkeyHex: HexKey) {
transientHiddenUsers.update { it - pubkeyHex }
}
fun hideUser(pubkeyHex: HexKey) {
transientHiddenUsers.update { it + pubkeyHex }
}
fun isHidden(pubkeyHex: HexKey) = pubkeyHex in transientHiddenUsers.value
}

View File

@@ -0,0 +1,118 @@
/**
* 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.blockPeopleList
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.muteList.tags.MuteTag
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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
class BlockPeopleListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: PeopleListDecryptionCache,
val scope: CoroutineScope,
) {
fun getBlockListAddress() = PeopleListEvent.createBlockAddress(signer.pubKey)
fun getBlockListNote() = LocalCache.getOrCreateAddressableNote(getBlockListAddress())
fun getBlockListFlow(): StateFlow<NoteState> = getBlockListNote().flow().metadata.stateFlow
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
suspend fun blockListWithBackup(note: Note): List<MuteTag> {
val event = note.event as? PeopleListEvent
return event?.let { decryptionCache.usersAndWords(it) } ?: emptyList()
}
val flow =
getBlockListFlow()
.map { blockListWithBackup(it.note) }
.onStart { emit(blockListWithBackup(getBlockListNote())) }
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
suspend fun hideUser(pubkeyHex: String): PeopleListEvent {
val blockList = getBlockList()
return if (blockList != null) {
PeopleListEvent.add(
earlierVersion = blockList,
person = UserTag(pubkeyHex),
isPrivate = true,
signer = signer,
)
} else {
PeopleListEvent.create(
name = PeopleListEvent.BLOCK_LIST_D_TAG,
person = UserTag(pubkeyHex),
isPrivate = true,
signer = signer,
dTag = PeopleListEvent.BLOCK_LIST_D_TAG,
)
}
}
suspend fun showUser(pubkeyHex: String): PeopleListEvent? {
val blockList = getBlockList()
return if (blockList != null) {
PeopleListEvent.remove(
earlierVersion = blockList,
person = UserTag(pubkeyHex),
signer = signer,
)
} else {
null
}
}
suspend fun showWord(word: String): PeopleListEvent? {
val blockList = getBlockList()
return if (blockList != null) {
PeopleListEvent.remove(
earlierVersion = blockList,
person = WordTag(word),
signer = signer,
)
} else {
null
}
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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.blockPeopleList
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsers
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsersAndWords
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWordSet
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWords
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
class PeopleListDecryptionCache(
val signer: NostrSigner,
) {
val cachedPrivateLists = PrivateTagArrayEventCache<PeopleListEvent>(signer)
fun cachedUsersAndWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsersAndWords()
fun cachedUsers(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsers()
fun cachedUserIdSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUserIdSet()
fun cachedWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWords()
fun cachedWordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWordSet()
suspend fun usersAndWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsersAndWords()
suspend fun users(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsers()
suspend fun userIdSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUserIdSet()
suspend fun words(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords()
suspend fun wordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet()
}

View File

@@ -0,0 +1,29 @@
/**
* 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.blockedRelays
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
class BlockedRelayListDecryptionCache(
signer: NostrSigner,
) : GenericRelayListCache<BlockedRelayListEvent>(signer)

View File

@@ -0,0 +1,112 @@
/**
* 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.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
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 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 BlockedRelayListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: BlockedRelayListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getBlockedRelayListAddress() = BlockedRelayListEvent.createAddress(signer.pubKey)
fun getBlockedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlockedRelayListAddress())
fun getBlockedRelayListFlow(): StateFlow<NoteState> = getBlockedRelayListNote().flow().metadata.stateFlow
fun getBlockedRelayList(): BlockedRelayListEvent? = getBlockedRelayListNote().event as? BlockedRelayListEvent
suspend fun normalizeBlockedRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? BlockedRelayListEvent ?: settings.backupBlockedRelayList
return event?.let { decryptionCache.relays(it) } ?: emptySet()
}
val flow =
getBlockedRelayListFlow()
.map {
normalizeBlockedRelayListWithBackup(it.note)
}.onStart { emit(normalizeBlockedRelayListWithBackup(getBlockedRelayListNote())) }
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun saveRelayList(blockedRelays: List<NormalizedRelayUrl>): BlockedRelayListEvent {
if (!signer.isWriteable()) throw SignerExceptions.ReadOnlyException()
val relayListForBlocked = getBlockedRelayList()
return if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) {
BlockedRelayListEvent.updateRelayList(
earlierVersion = relayListForBlocked,
relays = blockedRelays,
signer = signer,
)
} else {
BlockedRelayListEvent.create(
relays = blockedRelays,
signer = signer,
)
}
}
init {
settings.backupBlockedRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved Blocked relay list ${it.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "Blocked Relay List Collector Start")
getBlockedRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Blocked Relay List for ${signer.pubKey}")
(it.note.event as? BlockedRelayListEvent)?.let {
settings.updateBlockedRelayList(it)
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.broadcastRelays
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
class BroadcastRelayListDecryptionCache(
signer: NostrSigner,
) : GenericRelayListCache<BroadcastRelayListEvent>(signer)

View File

@@ -0,0 +1,87 @@
/**
* 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.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
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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
class BroadcastRelayListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: BroadcastRelayListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getBroadcastRelayListAddress() = BroadcastRelayListEvent.createAddress(signer.pubKey)
fun getBroadcastRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getBroadcastRelayListAddress())
fun getBroadcastRelayListFlow(): StateFlow<NoteState> = getBroadcastRelayListNote().flow().metadata.stateFlow
fun getBroadcastRelayList(): BroadcastRelayListEvent? = getBroadcastRelayListNote().event as? BroadcastRelayListEvent
suspend fun normalizeBroadcastRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? BroadcastRelayListEvent
return event?.let { decryptionCache.relays(it) } ?: emptySet()
}
val flow =
getBroadcastRelayListFlow()
.map { normalizeBroadcastRelayListWithBackup(it.note) }
.onStart { emit(normalizeBroadcastRelayListWithBackup(getBroadcastRelayListNote())) }
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun saveRelayList(broadcastRelays: List<NormalizedRelayUrl>): BroadcastRelayListEvent {
val relayListForBroadcast = getBroadcastRelayList()
return if (relayListForBroadcast != null && relayListForBroadcast.tags.isNotEmpty()) {
BroadcastRelayListEvent.updateRelayList(
earlierVersion = relayListForBroadcast,
relays = broadcastRelays,
signer = signer,
)
} else {
BroadcastRelayListEvent.create(
relays = broadcastRelays,
signer = signer,
)
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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.geohashLists
data class GeohashListCard(
val relays: List<String>,
)
val EmptyGeohashListCard = GeohashListCard(emptyList())

View File

@@ -0,0 +1,76 @@
/**
* 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.geohashLists
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
import com.vitorpamplona.quartz.nip51Lists.geohashList.geohashSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
class GeohashListDecryptionCache(
val signer: NostrSigner,
) {
val cachedPrivateLists = PrivateTagArrayEventCache<GeohashListEvent>(signer)
fun cachedGeohashes(event: GeohashListEvent) = cachedPrivateLists.mergeTagListPrecached(event).geohashSet()
suspend fun geohashes(event: GeohashListEvent) = cachedPrivateLists.mergeTagList(event).geohashSet()
fun fastStartValueForGeohashList(note: Note): GeohashListCard {
val noteEvent = note.event as? GeohashListEvent
return if (noteEvent != null) {
GeohashListCard(cachedGeohashes(noteEvent).toList())
} else {
EmptyGeohashListCard
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun observeDecryptedGeohashList(note: Note): Flow<GeohashListCard> =
note
.flow()
.metadata.stateFlow
.mapLatest { noteState ->
val event = noteState.note.event as? GeohashListEvent
GeohashListCard(event?.let { geohashes(it).toList() } ?: emptyList())
}.onStart {
val event = note.event as? GeohashListEvent
if (event != null) {
val list = geohashes(event)
if (list.isNotEmpty()) {
emit(GeohashListCard(list.toList()))
} else {
emit(EmptyGeohashListCard)
}
} else {
emit(EmptyGeohashListCard)
}
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}

View File

@@ -0,0 +1,127 @@
/**
* 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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
class GeohashListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: GeohashListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getGeohashListAddress() = GeohashListEvent.createAddress(signer.pubKey)
fun getGeohashListNote(): AddressableNote = cache.getOrCreateAddressableNote(getGeohashListAddress())
fun getGeohashListFlow(): StateFlow<NoteState> = getGeohashListNote().flow().metadata.stateFlow
fun getGeohashList(): GeohashListEvent? = getGeohashListNote().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()
}
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<Set<String>> =
getGeohashListFlow()
.transformLatest { noteState ->
emit(geohashListWithBackup(noteState.note))
}.onStart {
emit(geohashListWithBackup(getGeohashListNote()))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun follow(geohashes: List<String>): GeohashListEvent {
val geohashList = getGeohashList()
return if (geohashList == null) {
GeohashListEvent.create(geohashes, true, signer)
} else {
GeohashListEvent.add(geohashList, geohashes, true, signer)
}
}
suspend fun follow(geohash: String): GeohashListEvent {
val geohashList = getGeohashList()
return if (geohashList == null) {
GeohashListEvent.create(geohash, true, signer)
} else {
GeohashListEvent.add(geohashList, geohash, true, signer)
}
}
suspend fun unfollow(geohash: String): GeohashListEvent? {
val geohashList = getGeohashList()
return if (geohashList != null) {
GeohashListEvent.remove(geohashList, geohash, signer)
} else {
null
}
}
init {
settings.backupGeohashList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
}
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "Geohash List Collector Start")
getGeohashListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}")
(noteState.note.event as? GeohashListEvent)?.let {
settings.updateGeohashListTo(it)
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
/**
* 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.hashtagLists
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
import com.vitorpamplona.quartz.nip51Lists.hashtagList.hashtagSet
class HashtagListDecryptionCache(
val signer: NostrSigner,
) {
val cachedPrivateLists = PrivateTagArrayEventCache<HashtagListEvent>(signer)
fun cachedHashtags(event: HashtagListEvent) = cachedPrivateLists.mergeTagListPrecached(event).hashtagSet()
suspend fun hashtags(event: HashtagListEvent) = cachedPrivateLists.mergeTagList(event).hashtagSet()
}

View File

@@ -0,0 +1,127 @@
/**
* 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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
class HashtagListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: HashtagListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey)
fun getHashtagListNote(): AddressableNote = cache.getOrCreateAddressableNote(getHashtagListAddress())
fun getHashtagListFlow(): StateFlow<NoteState> = getHashtagListNote().flow().metadata.stateFlow
fun getHashtagList(): HashtagListEvent? = getHashtagListNote().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()
}
@OptIn(ExperimentalCoroutinesApi::class)
val flow: StateFlow<Set<String>> =
getHashtagListFlow()
.transformLatest { noteState ->
emit(hashtagListWithBackup(noteState.note))
}.onStart {
emit(hashtagListWithBackup(getHashtagListNote()))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Companion.Eagerly,
emptySet(),
)
suspend fun follow(hashtags: List<String>): HashtagListEvent {
val hashtagList = getHashtagList()
return if (hashtagList == null) {
HashtagListEvent.Companion.create(hashtags, true, signer)
} else {
HashtagListEvent.Companion.add(hashtagList, hashtags, true, signer)
}
}
suspend fun follow(hashtag: String): HashtagListEvent {
val hashtagList = getHashtagList()
return if (hashtagList == null) {
HashtagListEvent.Companion.create(hashtag, true, signer)
} else {
HashtagListEvent.Companion.add(hashtagList, hashtag, true, signer)
}
}
suspend fun unfollow(hashtag: String): HashtagListEvent? {
val hashtagList = getHashtagList()
return if (hashtagList != null) {
HashtagListEvent.Companion.remove(hashtagList, hashtag, signer)
} else {
null
}
}
init {
settings.backupHashtagList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved Hashtag list ${event.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
}
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "Hashtag List Collector Start")
getHashtagListFlow().collect {
Log.d("AccountRegisterObservers", "Hashtag List for ${signer.pubKey}")
(it.note.event as? HashtagListEvent)?.let {
settings.updateHashtagListTo(it)
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.indexerRelays
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
class IndexerRelayListDecryptionCache(
signer: NostrSigner,
) : GenericRelayListCache<IndexerRelayListEvent>(signer)

View File

@@ -0,0 +1,88 @@
/**
* 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.indexerRelays
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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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
class IndexerRelayListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: IndexerRelayListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getIndexerRelayListAddress() = IndexerRelayListEvent.createAddress(signer.pubKey)
fun getIndexerRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getIndexerRelayListAddress())
fun getIndexerRelayListFlow(): StateFlow<NoteState> = getIndexerRelayListNote().flow().metadata.stateFlow
fun getIndexerRelayList(): IndexerRelayListEvent? = getIndexerRelayListNote().event as? IndexerRelayListEvent
suspend fun normalizeIndexerRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> {
val event = note.event as? IndexerRelayListEvent
return event?.let { decryptionCache.relays(it) } ?: emptySet()
}
val flow =
getIndexerRelayListFlow()
.map { normalizeIndexerRelayListWithBackup(it.note) }
.onStart { emit(normalizeIndexerRelayListWithBackup(getIndexerRelayListNote())) }
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
suspend fun saveRelayList(indexerRelays: List<NormalizedRelayUrl>): IndexerRelayListEvent {
val relayListForIndexer = getIndexerRelayList()
return if (relayListForIndexer != null && relayListForIndexer.tags.isNotEmpty()) {
IndexerRelayListEvent.updateRelayList(
earlierVersion = relayListForIndexer,
relays = indexerRelays,
signer = signer,
)
} else {
IndexerRelayListEvent.create(
relays = indexerRelays,
signer = signer,
)
}
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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.muteList
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsers
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsersAndWords
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWordSet
import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWords
class MuteListDecryptionCache(
val signer: NostrSigner,
) {
val cachedPrivateLists = PrivateTagArrayEventCache<MuteListEvent>(signer)
fun cachedUsersAndWords(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsersAndWords()
fun cachedUsers(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsers()
fun cachedUserIdSet(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUserIdSet()
fun cachedWords(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWords()
fun cachedWordSet(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWordSet()
suspend fun mutedUsersAndWords(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsersAndWords()
suspend fun mutedUsers(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsers()
suspend fun mutedUserIdSet(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUserIdSet()
suspend fun mutedWords(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords()
suspend fun mutedWordSet(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet()
}

View File

@@ -0,0 +1,161 @@
/**
* 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.muteList
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.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
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 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 MuteListState(
val signer: NostrSigner,
val cache: LocalCache,
val decryptionCache: MuteListDecryptionCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
fun getMuteListAddress() = MuteListEvent.createAddress(signer.pubKey)
fun getMuteListNote() = cache.getOrCreateAddressableNote(getMuteListAddress())
fun getMuteListFlow(): StateFlow<NoteState> = getMuteListNote().flow().metadata.stateFlow
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
suspend fun muteListWithBackup(note: Note): List<MuteTag> {
val event = note.event as? MuteListEvent ?: settings.backupMuteList
return event?.let { decryptionCache.mutedUsersAndWords(it) } ?: emptyList()
}
val flow =
getMuteListFlow()
.map { muteListWithBackup(it.note) }
.onStart { emit(muteListWithBackup(getMuteListNote())) }
.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
suspend fun hideUser(pubkeyHex: String): MuteListEvent {
val muteList = getMuteList()
return if (muteList != null) {
MuteListEvent.add(
earlierVersion = muteList,
mute = UserTag(pubkeyHex),
isPrivate = true,
signer = signer,
)
} else {
MuteListEvent.create(
mute = UserTag(pubkeyHex),
isPrivate = true,
signer = signer,
)
}
}
suspend fun showUser(pubkeyHex: String): MuteListEvent? {
val muteList = getMuteList()
return if (muteList != null) {
MuteListEvent.remove(
earlierVersion = muteList,
mute = UserTag(pubkeyHex),
signer = signer,
)
} else {
null
}
}
suspend fun hideWord(word: String): MuteListEvent {
val muteList = getMuteList()
return if (muteList != null) {
MuteListEvent.add(
earlierVersion = muteList,
mute = WordTag(word),
isPrivate = true,
signer = signer,
)
} else {
MuteListEvent.create(
mute = WordTag(word),
isPrivate = true,
signer = signer,
)
}
}
suspend fun showWord(word: String): MuteListEvent? {
val muteList = getMuteList()
return if (muteList != null) {
MuteListEvent.remove(
earlierVersion = muteList,
mute = WordTag(word),
signer = signer,
)
} else {
null
}
}
init {
settings.backupMuteList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}")
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
}
}
scope.launch(Dispatchers.Default) {
Log.d("AccountRegisterObservers", "Mute List Collector Start")
getMuteListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Mute List for ${signer.pubKey}")
(it.note.event as? MuteListEvent)?.let {
settings.updateMuteList(it)
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.proxyRelays
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent
class ProxyRelayListDecryptionCache(
signer: NostrSigner,
) : GenericRelayListCache<ProxyRelayListEvent>(signer)

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