Compare commits

...

1829 Commits

Author SHA1 Message Date
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
Vitor Pamplona
29b8f6916b v0.87.4 2024-06-03 17:29:43 -04:00
Vitor Pamplona
a6519d57c6 Passes the video note uri so that when the user clicks in the video popup the app brings it back to the playing screen without breaking the video. 2024-06-03 17:26:40 -04:00
Vitor Pamplona
2b2ee5bbfc Avoids using Video Cache for live streams. 2024-06-03 17:18:38 -04:00
Vitor Pamplona
62a6b7b7e0 Fixes video stream position for live streams. 2024-06-03 17:18:17 -04:00
Vitor Pamplona
5e0a1f9de1 Moves SimpleCache creation to the IO thread. 2024-06-03 17:17:26 -04:00
Vitor Pamplona
571b1bc9cf Merge pull request #900 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 14:27:47 -04:00
Crowdin Bot
a3e76225e6 New Crowdin translations by GitHub Action 2024-06-03 18:26:14 +00:00
Vitor Pamplona
4388ceb529 v0.87.3 2024-06-03 14:24:28 -04:00
Vitor Pamplona
5d4fa0d732 Adds the incoming relays to the request. 2024-06-03 14:15:12 -04:00
Vitor Pamplona
ed6825de97 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 14:04:46 -04:00
Vitor Pamplona
10f623b6b3 Forces URLs to be single line. 2024-06-03 14:04:38 -04:00
Vitor Pamplona
467e5c0e4a Fixes bug that every socket is null 2024-06-03 14:04:29 -04:00
Vitor Pamplona
09d2e9593e Adds error messages to relay information dialog 2024-06-03 14:03:58 -04:00
Vitor Pamplona
cd20c0567b Merge pull request #899 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 13:27:05 -04:00
Vitor Pamplona
db15f5e2a6 Uses the user to avoid Java removing the variable due to lack of usage. 2024-06-03 13:25:56 -04:00
Crowdin Bot
18d5c7e3f4 New Crowdin translations by GitHub Action 2024-06-03 16:08:05 +00:00
Vitor Pamplona
d42ae7c9f7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 12:06:38 -04:00
Vitor Pamplona
2b741b62e8 Merge pull request #898 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 12:06:14 -04:00
Vitor Pamplona
4e1c63d36a allows more replies to filter when using DVMs 2024-06-03 12:05:58 -04:00
Vitor Pamplona
95797171d7 Fixes observer of authors 2024-06-03 12:05:46 -04:00
Crowdin Bot
395ef9695e New Crowdin translations by GitHub Action 2024-06-03 15:49:13 +00:00
Vitor Pamplona
ad1c7088c7 Loads the DVM's NIP65 records. 2024-06-03 11:47:39 -04:00
Vitor Pamplona
eda4569b64 Only shows debug relay writes in DEBUG 2024-06-03 11:47:20 -04:00
Vitor Pamplona
fb5f1e738b Adds a user and max result params to the DVM request. 2024-06-03 11:47:00 -04:00
Vitor Pamplona
e3cca6c691 Pulls NIP-65 relays even in the first lookup for the user. 2024-06-03 11:45:57 -04:00
Vitor Pamplona
ce2b8dfa11 Sends the DVM request to the NIP65 Inbox relays. 2024-06-03 11:15:36 -04:00
Vitor Pamplona
b5faf6ffc5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 11:15:07 -04:00
Vitor Pamplona
4ee157f27f Merge pull request #897 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 11:14:50 -04:00
Vitor Pamplona
ad8c04b79a Loads user's relays on scrolling. 2024-06-03 11:14:33 -04:00
Crowdin Bot
c0e6273954 New Crowdin translations by GitHub Action 2024-06-03 15:04:13 +00:00
Vitor Pamplona
aebade21a6 Don't display zap splits when the post is short. 2024-06-03 11:02:23 -04:00
Vitor Pamplona
bda24c0101 Separates the Account's relay info to the UI component 2024-06-03 11:01:34 -04:00
Vitor Pamplona
3d0b461550 Moves Relay Stats to it's own cache.
Refactors the isLocalRelay function
Removes unnecessary onError methods from the Relay class
Moves byte counts from Int to Long
2024-06-03 10:42:00 -04:00
Vitor Pamplona
5900e6c49f Merge pull request #896 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 08:45:33 -04:00
Crowdin Bot
f29fe3ced5 New Crowdin translations by GitHub Action 2024-06-03 12:38:46 +00:00
Vitor Pamplona
bf17cc8f03 Updating Firebase and Androidx JUnit 2024-06-03 08:36:45 -04:00
Vitor Pamplona
a8d10c22d4 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-01 16:14:47 -04:00
Vitor Pamplona
5492ebf43f Merge pull request #893 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-01 16:13:05 -04:00
Vitor Pamplona
0c48366d56 v0.80.2 2024-06-01 16:12:49 -04:00
Crowdin Bot
d66a027270 New Crowdin translations by GitHub Action 2024-06-01 19:33:36 +00:00
Vitor Pamplona
f1834af506 Moves the filter for no subscriptions to the DAL 2024-06-01 15:31:43 -04:00
Vitor Pamplona
f26fa71e49 improves the speed of the kind filter for DVMs 2024-06-01 15:20:26 -04:00
Vitor Pamplona
4faee68174 Filters by supported kinds in the DVM view. 2024-06-01 15:16:35 -04:00
Vitor Pamplona
2326e66382 Solves race conditions when connecting to relays. 2024-06-01 15:16:18 -04:00
Vitor Pamplona
bf71365f51 Merge pull request #892 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-01 13:18:30 -04:00
Crowdin Bot
f45e8d491c New Crowdin translations by GitHub Action 2024-06-01 17:17:09 +00:00
Vitor Pamplona
5a476cab32 v0.87.0 2024-06-01 13:15:27 -04:00
Vitor Pamplona
b5b0a19df5 Fixes the disappearance of the notification icon 2024-06-01 13:14:30 -04:00
Vitor Pamplona
85d26dee08 Merge pull request #891 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-01 10:58:27 -04:00
Crowdin Bot
476da07d3d New Crowdin translations by GitHub Action 2024-06-01 14:55:45 +00:00
Vitor Pamplona
3b7344e894 v0.87.0 2024-06-01 10:53:39 -04:00
Vitor Pamplona
d307bb0e49 Fixes missing relays certain internal lists 2024-05-31 19:47:40 -04:00
Vitor Pamplona
1bbcb97d54 Fixes read status between others and the logged in user 2024-05-31 16:54:18 -04:00
Vitor Pamplona
3a9637ccb9 Optimizes Blurhash generation 2024-05-31 14:24:55 -04:00
Vitor Pamplona
071da53a6a Waits 500ms before restarting all connections. 2024-05-30 17:11:11 -04:00
Vitor Pamplona
9d190f93bb deactivate write for search relays (they are read only) 2024-05-30 17:10:56 -04:00
Vitor Pamplona
7c65a93896 - Only updates lists if they were changed.
- Refactoring RelayLists
2024-05-30 17:10:38 -04:00
Vitor Pamplona
d9b96d2fd5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-30 16:20:00 -04:00
Vitor Pamplona
3333fdc237 Layout improvements for the relay screen 2024-05-30 16:19:52 -04:00
Vitor Pamplona
e178ff988d Adds NIP-50 requirements to the Search explainer. 2024-05-30 16:19:39 -04:00
Vitor Pamplona
df649445d0 Merge pull request #890 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-30 16:02:37 -04:00
Vitor Pamplona
a38346c7d3 Adds private outbox relays to save Draft events. 2024-05-30 16:02:21 -04:00
Crowdin Bot
71da587426 New Crowdin translations by GitHub Action 2024-05-30 19:58:38 +00:00
Vitor Pamplona
6ecb5ecc47 Merge pull request #889 from believethehype/main
show error messages if dvm status returns error
2024-05-30 15:57:23 -04:00
Believethehype
b2a19efcd0 show error messages, if dvm status returns error 2024-05-30 20:12:06 +02:00
Vitor Pamplona
1123586ef6 Uses DM Relay List to activate nip17 messages in chats. 2024-05-30 14:00:20 -04:00
Vitor Pamplona
ab037e61aa adds local relays to the connecting set 2024-05-30 13:51:31 -04:00
Vitor Pamplona
4a43b57f79 Uses NIP-65 preferably for zap requests. 2024-05-30 13:51:14 -04:00
Vitor Pamplona
f81c83326c Relay screen padding adjustments 2024-05-30 13:33:11 -04:00
Vitor Pamplona
fdd1dd1855 Adds a title to the relay settings screen 2024-05-30 12:44:29 -04:00
Vitor Pamplona
9f3d4db9dd Adds Local Relays to the relay list screen, saving locally in the device. 2024-05-30 12:35:24 -04:00
Vitor Pamplona
c23a9f8ad6 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-30 11:21:47 -04:00
Vitor Pamplona
36b0c8acb5 Fixing crash when calling localcache on the main thread. 2024-05-30 11:21:39 -04:00
Vitor Pamplona
dc12789166 Merge pull request #888 from KotlinGeekDev/unifiedpush-updates
UnifiedPush library updates
2024-05-30 11:20:38 -04:00
KotlinGeekDev
942b0fa531 Merge branch 'vitorpamplona:main' into unifiedpush-updates 2024-05-30 15:07:48 +00:00
KotlinGeekDev
8ae7f31c3d Refactor getSavedDistributor(). It uses UnifiedPush.getSavedDistributor underneath to preserve pre-API change behaviour. 2024-05-30 16:04:30 +01:00
KotlinGeekDev
bc1bbc2f70 Update to up-connector to v2.3.1. The JVM toolchain and Kotlin downgrade in 2.4.0 may not be good for Amethyst now. Will upgrade when everything has checked out. 2024-05-30 16:01:22 +01:00
Vitor Pamplona
632c78792a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-30 09:55:00 -04:00
Vitor Pamplona
e2a719164f Migrates Kotlin and dependencies to the latest version 2024-05-30 09:48:45 -04:00
Vitor Pamplona
ff94f45d04 Enabling download and creation of NIP-96 file servers. 2024-05-30 09:38:33 -04:00
Vitor Pamplona
7e1b1c7d2c No need for experimental tag anymore. 2024-05-30 09:37:15 -04:00
Vitor Pamplona
f6f9d82665 Merge pull request #887 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-30 08:41:51 -04:00
Crowdin Bot
3638059c23 New Crowdin translations by GitHub Action 2024-05-30 12:41:41 +00:00
Vitor Pamplona
a781e733f2 improving the filter for the request id of the cached response. 2024-05-30 08:39:23 -04:00
Vitor Pamplona
56d6376efc Merge pull request #886 from believethehype/main
Check if NIP90 Result exists before relying on Cache
2024-05-30 08:34:59 -04:00
Vitor Pamplona
3ac9d3a4f7 Merge pull request #885 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-30 08:33:49 -04:00
Believethehype
521da9a774 check if NIP90 Result list exists before relying on Cache 2024-05-30 14:23:24 +02:00
Crowdin Bot
18ebdaf96d New Crowdin translations by GitHub Action 2024-05-30 10:50:40 +00:00
Vitor Pamplona
19d394a383 Merge pull request #884 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-30 06:49:20 -04:00
Crowdin Bot
b198b4c081 New Crowdin translations by GitHub Action 2024-05-30 10:47:55 +00:00
Vitor Pamplona
28bc46f120 Merge pull request #883 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-30 06:46:21 -04:00
Crowdin Bot
7a6de4ea49 New Crowdin translations by GitHub Action 2024-05-30 10:33:52 +00:00
Vitor Pamplona
bc108875e6 Merge pull request #882 from davotoula/update_tr
Update SE, CZ, DE, PT translations
2024-05-30 06:32:33 -04:00
David Kaspar
aa38b41c58 Update SE, CZ, DE, PT translations 2024-05-30 10:01:33 +02:00
Vitor Pamplona
a4c26b1fd8 Reducing the use of backgrounds to improve rendering speeds. 2024-05-29 18:53:26 -04:00
Vitor Pamplona
550cfe8bb4 Fixes relay list assembling 2024-05-29 15:58:47 -04:00
Vitor Pamplona
56664afa4e Show missing relays for DMs when the DMRelayList event is empty. 2024-05-29 15:22:19 -04:00
Vitor Pamplona
90c9deecd4 Removes the forced search relay selection and helps users setup their relay list for search. 2024-05-29 15:21:54 -04:00
Vitor Pamplona
add65b094d updates junit 2024-05-29 14:28:07 -04:00
Vitor Pamplona
c7a4161799 Fixing tests 2024-05-29 12:50:46 -04:00
Vitor Pamplona
f6a16e4394 Fixes date format's year 2024-05-29 12:28:22 -04:00
Vitor Pamplona
44a615020d Improves wording of the relay list 2024-05-29 12:28:10 -04:00
Vitor Pamplona
288aafc3a2 Adds support for search relay lists 2024-05-29 12:06:45 -04:00
Vitor Pamplona
aa97c7ee9b Adds NIP-65 relay settings 2024-05-29 10:39:08 -04:00
Vitor Pamplona
51a6eb4974 - Adds a new observer by kind and author
- Uses NIP-17 relays for DMs
- Changes default connection to account from NIP17 relays
- Only sends wraps to the appropriate relays.
2024-05-28 19:13:03 -04:00
Vitor Pamplona
f8dfc06a06 Saves DMs and All Relays on the relay list 2024-05-28 19:11:23 -04:00
Vitor Pamplona
2d4db9fea8 Refactoring on latestbykindandetag 2024-05-28 19:09:53 -04:00
Vitor Pamplona
34b743146e Making sure the relay list has different keys for different sections. 2024-05-28 18:48:41 -04:00
Vitor Pamplona
13eb8b1abc Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-28 11:49:20 -04:00
Vitor Pamplona
e98a77b37f Adds DM relays to the relay list. 2024-05-28 11:49:13 -04:00
Vitor Pamplona
6a17a8a871 Refactors GiftWrap caching to delete encrypted text and reload the wrap if necessary.
- Changes host to a host stub to reduce memory use
- Only download GiftWraps form 2 days past the last EOSE
2024-05-28 11:48:20 -04:00
Vitor Pamplona
3ce726bbb7 Merge pull request #881 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-28 08:27:01 -04:00
Crowdin Bot
593bf24f5b New Crowdin translations by GitHub Action 2024-05-28 12:22:25 +00:00
Vitor Pamplona
7a857711eb Merge pull request #880 from davotoula/update_tr
Update SE, CZ, DE, PT translations
2024-05-28 08:20:52 -04:00
David Kaspar
420e325d1f Update SE, CZ, DE, PT translations 2024-05-28 09:04:49 +02:00
Vitor Pamplona
610657a9b6 Merge pull request #877 from greenart7c3/main
add signString method
2024-05-27 08:15:34 -04:00
greenart7c3
ea891367e9 add signString method 2024-05-27 07:56:02 -03:00
Vitor Pamplona
0c22e66e8f Overrides pubkey to avoid impersonators on seals. 2024-05-24 19:27:14 -04:00
Vitor Pamplona
cd008d78a0 Improves the debug of the memory use per event kind 2024-05-24 19:26:39 -04:00
Vitor Pamplona
2c2e4a1694 Clears flows 2024-05-24 19:26:13 -04:00
Vitor Pamplona
d7790cd31e Moves the thread formatter and calculator out of Note 2024-05-24 18:11:57 -04:00
Vitor Pamplona
0bb571b52e Fixes position of the edit field after the relay list 2024-05-24 17:42:42 -04:00
Vitor Pamplona
c65a4b8fe7 Merge pull request #876 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-24 14:59:55 -04:00
Crowdin Bot
abb0c73c71 New Crowdin translations by GitHub Action 2024-05-24 18:56:33 +00:00
Vitor Pamplona
224bcf97ec Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-24 14:54:48 -04:00
Vitor Pamplona
04c449072a Adds support for NIP-06 seed word key derivation (bip32 and bip39) 2024-05-24 14:54:38 -04:00
Vitor Pamplona
b26aba92a1 Merge pull request #875 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-24 09:12:25 -04:00
Crowdin Bot
5ce9df9dce New Crowdin translations by GitHub Action 2024-05-24 12:20:21 +00:00
Vitor Pamplona
7426600dc6 deleting orangepill since it might not come back 2024-05-24 08:18:32 -04:00
Vitor Pamplona
9d4a88d26f Clear up some old unstable api tags 2024-05-23 18:18:46 -04:00
Vitor Pamplona
85aaf25561 Remove unnecessary logs 2024-05-23 17:11:18 -04:00
Vitor Pamplona
1cd5845960 Fixes the rendering of replies on wikipages. 2024-05-23 17:09:16 -04:00
Vitor Pamplona
e38f6be20c Refactoring the composables for the relay list screen 2024-05-23 16:40:24 -04:00
Vitor Pamplona
c78c00acd9 Adds method to create the NIP-65 list 2024-05-22 18:23:52 -04:00
Vitor Pamplona
11fa762028 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-22 17:46:45 -04:00
Vitor Pamplona
599fddb369 Adds a card to setup the DM relay list if the user doesn't have one. 2024-05-22 17:46:39 -04:00
Vitor Pamplona
76103ac057 Fixes the padding of the x button on the relay list screen 2024-05-22 17:45:55 -04:00
Vitor Pamplona
6538cbd9da Fixes padding of the chat list elements. 2024-05-22 17:45:18 -04:00
Vitor Pamplona
566ca0e390 Moves Relay URL formatter to Quartz 2024-05-22 17:44:18 -04:00
Vitor Pamplona
c5f7ea801d Update README.md
Adding the most recent feature sets
2024-05-22 14:54:21 -04:00
Vitor Pamplona
469e7abd99 Clears imeta tags that might duplicate for the same post. 2024-05-22 13:43:07 -04:00
Vitor Pamplona
e540954125 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-21 15:56:58 -04:00
Vitor Pamplona
f73052ed48 Checks cache before making a new request to DVM 2024-05-21 15:56:51 -04:00
Vitor Pamplona
a9437d9001 Refactors CreatedAt Comparator 2024-05-21 15:56:33 -04:00
Vitor Pamplona
02f6422bab Merge pull request #871 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-21 14:53:09 -04:00
Crowdin Bot
da2b59af49 New Crowdin translations by GitHub Action 2024-05-21 18:52:37 +00:00
Vitor Pamplona
7acdf56e68 Slightly improving the threading for badges. 2024-05-21 14:50:36 -04:00
Vitor Pamplona
24be6cd90d No need to launch, just use IO 2024-05-21 12:26:39 -04:00
Vitor Pamplona
afee0ddc53 Merge pull request #869 from believethehype/NIP90-ContentDiscovery
update paid dvms with processing status after payment
2024-05-21 08:41:38 -04:00
Believethehype
c85b8a1d83 update paid dvms with processing status after payment 2024-05-21 12:11:44 +02:00
Vitor Pamplona
cfeedfa4e2 Fixes the use of cache for video files. 2024-05-20 17:42:21 -04:00
Vitor Pamplona
dd112d28ae updating some libraries (others are not ready for the app) 2024-05-20 17:11:03 -04:00
Vitor Pamplona
fc27526113 Removes reposts from the Dot Notification in the home's bottom bar icon 2024-05-20 16:45:08 -04:00
Vitor Pamplona
666635811b Migrating to AGP 8.4.1 2024-05-20 16:42:11 -04:00
Vitor Pamplona
4e43938f96 Can zap DVMs directly on the regular zap button. 2024-05-20 16:22:27 -04:00
Vitor Pamplona
2a744205f0 Merge pull request #868 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-20 15:20:33 -04:00
Crowdin Bot
7e6ca34d2a New Crowdin translations by GitHub Action 2024-05-20 19:16:35 +00:00
Vitor Pamplona
24f7991116 Fixes a white space after a new line 2024-05-20 15:15:06 -04:00
Vitor Pamplona
beb901120e Fixes call from main thread 2024-05-20 13:25:53 -04:00
Vitor Pamplona
0936df9851 Fixes sat division and the state changing text. 2024-05-20 13:19:04 -04:00
Vitor Pamplona
9ceb8866ed Refactoring the DVM payment code. 2024-05-20 12:37:55 -04:00
Vitor Pamplona
c88b21b547 Merge pull request #867 from believethehype/NIP90-ContentDiscovery
Nip90 content discovery - Consider status and amount tags
2024-05-20 11:38:13 -04:00
Vitor Pamplona
5c366d5cfc Merge branch 'main' into NIP90-ContentDiscovery 2024-05-20 11:37:07 -04:00
Vitor Pamplona
ea70d44ac7 liniting 2024-05-20 11:24:03 -04:00
Believethehype
67f10920f6 Update NIP90ContentDiscoveryScreen.kt 2024-05-20 00:35:39 +02:00
Believethehype
654632a585 Update NIP90ContentDiscoveryScreen.kt 2024-05-20 00:32:26 +02:00
Believethehype
794b05106b add possibility to zap/pay invoices for dvms 2024-05-20 00:15:30 +02:00
Vitor Pamplona
5812e290c9 Merge pull request #866 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-17 18:32:35 -04:00
Crowdin Bot
c279c04858 New Crowdin translations by GitHub Action 2024-05-17 22:32:08 +00:00
Vitor Pamplona
01016525d3 Fixes missing parameter 2024-05-17 18:30:48 -04:00
Vitor Pamplona
72c6e93524 Migrates DVM route from pubkey to AppDefinition eventId and makes sure NFC reading will load the AppDefinition itself before rendering the rest of the screen. 2024-05-17 18:06:20 -04:00
Vitor Pamplona
d6988ad4e1 fixes redirection of routeFor when passing an App Definition Event. 2024-05-17 18:05:38 -04:00
Vitor Pamplona
4c1cd1c9ab adds DVM picture and name to the top bar and the status feed. 2024-05-17 17:18:31 -04:00
Vitor Pamplona
9fb8d4821e Refactoring the DVM codebase
Allows pull to refresh to request the job again.
2024-05-17 17:02:04 -04:00
Vitor Pamplona
8b052567c4 Adds a new observable system to local cache. 2024-05-17 17:01:57 -04:00
Vitor Pamplona
4a6ea550d6 Fixes the spacing in the Like button 2024-05-17 17:00:19 -04:00
Vitor Pamplona
5c88e7993f Fixes some of the missing classes due to R8 2024-05-16 15:14:51 -04:00
Vitor Pamplona
1bd1493bf4 Merge pull request #864 from greenart7c3/main
add paste from clipboard button to nwc
2024-05-16 13:47:19 -04:00
greenart7c3
404278a4e3 add paste from clipboard button to nwc 2024-05-16 14:13:40 -03:00
Vitor Pamplona
edca55b0b6 must not use crypto tools on the benchmark 2024-05-16 13:10:38 -04:00
Vitor Pamplona
738187d32b Making generateID public for testing 2024-05-16 13:05:15 -04:00
Vitor Pamplona
d9de0d2798 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-16 13:04:17 -04:00
Vitor Pamplona
b20515b1a0 Merge pull request #862 from believethehype/NIP90-ContentDiscovery
NIP90: cleanup, potential fix endless loop
2024-05-16 13:04:02 -04:00
Vitor Pamplona
e6d8291f07 Trying to unify NIP01 Serialization with SHA-256 procedures to reduce creation of several bytearray at every verification. 2024-05-16 13:03:30 -04:00
Vitor Pamplona
18d08bf6e0 Don't obfuscate data classes. 2024-05-16 13:01:15 -04:00
Believethehype
b2193f48d5 Update NIP90ContentDiscoveryScreen.kt 2024-05-16 16:48:19 +02:00
Vitor Pamplona
de391f03b1 Merge pull request #858 from VASHvic/auto_focus_search_screen
auto focus when entering the search screen
2024-05-16 09:29:21 -04:00
Believethehype
c494cf8ac1 Update NIP90ContentDiscoveryScreen.kt 2024-05-16 15:20:04 +02:00
Believethehype
193e9a5adb add more info on current dvm state 2024-05-16 15:19:19 +02:00
Believethehype
4fb9c93cf0 Merge branch 'NIP90-ContentDiscovery' of https://github.com/believethehype/amethyst into NIP90-ContentDiscovery 2024-05-16 15:16:33 +02:00
Believethehype
6bd98201f8 add DVM not replying message 2024-05-16 15:16:30 +02:00
believethehype
cbc6697631 Merge branch 'main' into NIP90-ContentDiscovery 2024-05-16 15:08:42 +02:00
Vitor Pamplona
d48634ac0e Merge pull request #861 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-16 09:03:48 -04:00
believethehype
5e2c8de15e Merge branch 'main' into NIP90-ContentDiscovery 2024-05-16 15:03:34 +02:00
Believethehype
926a721c53 cleanup, potential fix endless loop 2024-05-16 15:02:44 +02:00
Crowdin Bot
5bc6da3bfc New Crowdin translations by GitHub Action 2024-05-16 13:00:51 +00:00
Vitor Pamplona
eae066b003 Merge pull request #860 from believethehype/NIP90-ContentDiscovery
Fix order of events, use proper event content parsing
2024-05-16 08:58:54 -04:00
Believethehype
b74fa975ba Show DVM Status
Limitation: This will only show the first reply from the DVM, if mutliple status updates follow they will be ignored.
2024-05-16 14:24:13 +02:00
Believethehype
7f2b8519f3 improve parsing 2024-05-16 14:07:11 +02:00
Believethehype
51335e06f1 Fix order of Events in DVM feeds 2024-05-16 13:52:22 +02:00
VASH
0b6cd08c4a auto focus when entering the search screen 2024-05-15 21:59:21 +02:00
Vitor Pamplona
1bfe57da63 Fixes benchmark test for the new isDraft parameter 2024-05-15 10:28:01 -04:00
Vitor Pamplona
283d52ac2f Merge pull request #856 from believethehype/NIP90-ContentDiscovery
Nip90 Content Discovery
2024-05-15 10:17:10 -04:00
Vitor Pamplona
d1646761d2 Starts the process of adding a DM Relay list card to setup relays for GiftWraps 2024-05-15 10:00:54 -04:00
Believethehype
dc8209c90d Update NIP90ContentDiscoveryScreen.kt 2024-05-15 15:13:16 +02:00
Believethehype
420323bcb0 adjustments and cleanups 2024-05-15 15:11:36 +02:00
Believethehype
142aca40ce add NIP90 status events, update NIP90 feeds based on request id 2024-05-15 12:06:12 +02:00
Vitor Pamplona
4fa3d60638 Speeding up the url finder in the new post edit text. 2024-05-14 20:32:48 -04:00
Vitor Pamplona
a9eeb04014 Fixes alignment of the like icons after the like event. 2024-05-14 19:26:47 -04:00
Believethehype
f09b00ff01 adding 5301 events to be handled 2024-05-15 00:14:15 +02:00
Believethehype
ceaae398c2 Merge remote-tracking branch 'upstream/main' into NIP90-ContentDiscovery 2024-05-14 23:16:49 +02:00
Believethehype
39f87af072 remove notifications for NIP90 Content events 2024-05-14 23:09:43 +02:00
Vitor Pamplona
a34a3cbc83 Fixes GirtWrap test 2024-05-14 16:34:43 -04:00
Vitor Pamplona
f8afb4b783 Refactor the relay list for chat message event kind 2024-05-14 16:34:32 -04:00
Vitor Pamplona
fad4248539 Moves Zap the Devs card to a better position 2024-05-14 16:33:55 -04:00
Believethehype
13a53876d0 fix sorting 2024-05-14 22:21:03 +02:00
Believethehype
ea8affaebf general (unpolished) workflow works 2024-05-14 22:01:03 +02:00
Believethehype
df44e172ab Update NIP90ContentDiscoveryFilter.kt 2024-05-14 21:48:59 +02:00
Believethehype
8a50b3938d send request, get results (still needs parsing) 2024-05-14 21:41:36 +02:00
Believethehype
0245c907ff add nip90 events to factory 2024-05-14 18:51:38 +02:00
Vitor Pamplona
3bdc75b1be Improves the rendering of Channels and Communities when quoted 2024-05-14 12:40:53 -04:00
Vitor Pamplona
9d02361d01 Fixes wrong display of original and forked notes 2024-05-14 12:40:29 -04:00
Vitor Pamplona
6e6a13c5bf Moves video compressing quality to medium. 2024-05-14 12:12:48 -04:00
Vitor Pamplona
1f45a63081 Makes NIP-17 the default messaging mode
Migrates code from old NIP-24 to NIP-17
2024-05-14 11:34:36 -04:00
Believethehype
11a5f4a67e Merge remote-tracking branch 'upstream/main' into NIP90-ContentDiscovery 2024-05-14 17:06:00 +02:00
Believethehype
52e79580bf attempt to add own filters and event kinds for NIP90 content related events 2024-05-14 17:03:13 +02:00
Vitor Pamplona
561b19c447 Better rendering drafts in the thread 2024-05-14 10:16:41 -04:00
believethehype
9b21c3c964 Merge branch 'vitorpamplona:main' into NIP90-ContentDiscovery 2024-05-14 06:29:19 +02:00
Believethehype
baaa984d0d add nip90 content discovery request 2024-05-13 22:43:38 +02:00
Believethehype
2b7ef79d21 send dvm id to render list 2024-05-13 21:57:33 +02:00
Vitor Pamplona
22c96d2489 Use the default comparator instead of creating a new object every time 2024-05-13 15:40:24 -04:00
Vitor Pamplona
0c1187e4f5 Reducing the creation of new buttons when modifier objects change 2024-05-13 15:39:29 -04:00
Vitor Pamplona
7310ef175f DecimalFormats are not thread safe 2024-05-13 15:39:06 -04:00
Vitor Pamplona
53ba65cac2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-13 13:27:09 -04:00
Vitor Pamplona
58ed27dc75 Adds recommended amounts for the Zap the Devs
Block error messages from closing the Zap split payment screen.
2024-05-13 13:27:03 -04:00
Vitor Pamplona
065ba1c165 Better format zap amounts (don't show .0 if the previous numbers are large) 2024-05-13 13:25:50 -04:00
Believethehype
896d227fea Update DiscoverScreen.kt 2024-05-13 19:25:43 +02:00
Vitor Pamplona
6bac18c5df Fixes the offset position of the payment amounts on the Zap the Devs message 2024-05-13 13:24:53 -04:00
Vitor Pamplona
9ad62ef263 Reduces default zap amounts due to the change of bitcoin price 2024-05-13 13:22:58 -04:00
Believethehype
fe45e188bd hide subscription dvms for now 2024-05-13 18:17:36 +02:00
Believethehype
334b948900 app meta data, show content dvms 2024-05-13 17:27:34 +02:00
Vitor Pamplona
fb9ad2b457 Merge pull request #854 from jeremyd/main
allow relay selection dialog to pick any relays
2024-05-13 10:47:17 -04:00
Vitor Pamplona
921bb41596 Merge pull request #855 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-13 10:47:08 -04:00
Crowdin Bot
ab4d01583b New Crowdin translations by GitHub Action 2024-05-13 14:44:00 +00:00
Vitor Pamplona
314531e938 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-13 10:42:05 -04:00
Vitor Pamplona
991eed9bdf Merge pull request #845 from davotoula/843-URL-previews-mishandle-HTML-entities
843 url previews mishandle html entities
2024-05-13 10:40:33 -04:00
Believethehype
357981f266 add nip89 filter 2024-05-13 16:12:19 +02:00
Believethehype
119e9b7281 Structure for Content Discovery 2024-05-13 15:42:52 +02:00
Vitor Pamplona
8ca53e9707 Updates jackson 2024-05-10 19:47:40 -04:00
jeremyd
326e38f293 allow relay selection dialog to pick any relays (instead of just write relays) 2024-05-10 15:14:37 -07:00
Vitor Pamplona
6232e2682f Updates firebase 2024-05-09 19:10:19 -04:00
Vitor Pamplona
a2363221c6 Fixes Copy Text function of DraftEvents 2024-05-07 09:58:29 -04:00
Vitor Pamplona
1eef457b4e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-05-06 11:32:21 -04:00
Vitor Pamplona
fbd88bdeab Fixes top bar lists not updating when following communities and hashtags. 2024-05-06 11:32:14 -04:00
Vitor Pamplona
5d25bec1c9 Merge pull request #851 from VASHvic/hide_words_check
show toast error if unable to hide words
2024-05-04 15:59:36 -04:00
VASH
201a6d4462 show toast error if unable to hide words 2024-05-04 19:38:19 +02:00
Vitor Pamplona
1345ad3745 Merge pull request #847 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-01 13:17:55 -04:00
Crowdin Bot
b5f2a2b428 New Crowdin translations by GitHub Action 2024-05-01 17:16:56 +00:00
Vitor Pamplona
fe7b9b7930 Updates AGP 2024-05-01 13:09:29 -04:00
Vitor Pamplona
b24d3d863d ignores new idea file 2024-05-01 13:08:02 -04:00
Vitor Pamplona
3b7252616b Updates dependencies 2024-05-01 13:07:26 -04:00
Vitor Pamplona
c76ed3bb53 Merge pull request #846 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-24 17:06:42 -04:00
Crowdin Bot
cf775eebfb New Crowdin translations by GitHub Action 2024-04-24 21:05:58 +00:00
Vitor Pamplona
72018dc208 Base support for Relay lists for DMs. 2024-04-24 17:04:13 -04:00
Vitor Pamplona
fef635ab39 Fixes the use of Global-active relays in the Global Feed. 2024-04-24 13:54:13 -04:00
Vitor Pamplona
7a243af45c Updates compose, kotlin and okhttp 2024-04-24 09:53:17 -04:00
David Kaspar
94af0eb220 incremented matching group due to added matching group
added nbsp entity
2024-04-23 19:59:12 +02:00
David Kaspar
c0aea75c16 added test for html entity numbers to regular expression
added a few common html entities and entity numbers
2024-04-23 17:16:53 +02:00
Vitor Pamplona
175b79b291 Merge pull request #844 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-22 16:38:35 -04:00
Crowdin Bot
e5b8523bee New Crowdin translations by GitHub Action 2024-04-22 20:32:36 +00:00
Vitor Pamplona
1aecd9cf45 Minimizes costs of keeping track of the number of events received per subscription 2024-04-22 11:16:17 -04:00
Vitor Pamplona
6600a49564 Fixes the lack of refresh when adding hidden words in the Security filters 2024-04-18 19:34:15 -04:00
Vitor Pamplona
cdb65640ba Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-18 17:18:29 -04:00
Vitor Pamplona
0c9e76eeaf adds content type on CardFeedView 2024-04-18 17:17:17 -04:00
Vitor Pamplona
cfdbd0a9b6 Merge pull request #842 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-18 16:35:05 -04:00
Crowdin Bot
09b8178a7c New Crowdin translations by GitHub Action 2024-04-18 20:21:04 +00:00
Vitor Pamplona
5ea793eb51 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-18 16:19:21 -04:00
Vitor Pamplona
8cf04967c3 Makes sure only one pepare is run for each video view. 2024-04-18 14:46:01 -04:00
Vitor Pamplona
ef363457e8 Moves ClientController executor to a thread 2024-04-18 14:45:28 -04:00
Vitor Pamplona
e87394f3f7 Merge pull request #841 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-18 10:49:50 -04:00
Crowdin Bot
86ebfe8564 New Crowdin translations by GitHub Action 2024-04-18 14:24:22 +00:00
Vitor Pamplona
a2b3cfb991 Increases timeout to Tor connections 2024-04-18 10:22:19 -04:00
Vitor Pamplona
1b6aa621cd Merge pull request #840 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-17 17:55:02 -04:00
Crowdin Bot
b7f73c6eab New Crowdin translations by GitHub Action 2024-04-17 21:50:53 +00:00
Vitor Pamplona
e35fb88ff1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-17 17:49:00 -04:00
Vitor Pamplona
6ecb3c8e1f - Improves Zap efficiency for large zap splits
- Adds a queue of commands while the relay connects.
- Makes sure only one connection per URL is made when doing splits
- Removes unecessary Amber calls when decrypting private zaps
2024-04-17 17:48:53 -04:00
Vitor Pamplona
ff20f0a266 Deleting unecessary logs 2024-04-17 17:47:17 -04:00
Vitor Pamplona
a4cc6337f9 Don't use DM relays to find metadata 2024-04-17 17:43:40 -04:00
Vitor Pamplona
202b897029 Refactors AppScreen out of Activity. 2024-04-17 16:25:13 -04:00
Vitor Pamplona
79c174b92e Merge pull request #839 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-17 12:23:15 -04:00
Crowdin Bot
bdd3f19b2c New Crowdin translations by GitHub Action 2024-04-17 15:16:18 +00:00
Vitor Pamplona
afcc775d1b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-17 11:14:10 -04:00
Vitor Pamplona
8bbf308619 refactors the use of lud16 and lud06 2024-04-17 11:14:03 -04:00
Vitor Pamplona
df378937fe Adds a preview for UserNames with emojis 2024-04-17 11:12:51 -04:00
Vitor Pamplona
02ab7a3f3f Merge pull request #838 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-16 17:16:22 -04:00
Crowdin Bot
68ba9b3b91 New Crowdin translations by GitHub Action 2024-04-16 21:14:51 +00:00
Vitor Pamplona
f62833d1be Updates Firebase and Media3 dependencies 2024-04-16 17:11:52 -04:00
Vitor Pamplona
3be246c9cc Merge pull request #835 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-12 17:07:41 -04:00
Crowdin Bot
b28546b172 New Crowdin translations by GitHub Action 2024-04-12 21:05:53 +00:00
Vitor Pamplona
0fccfd7f80 clean up 2024-04-12 11:02:25 -04:00
Vitor Pamplona
3f35b57571 Improves Zap error messages to include the lnaddress of the error 2024-04-12 11:02:17 -04:00
Vitor Pamplona
32b9b6c37a Speeds up id calculations for Amber's Intent call.
Only assembles an id if necessary.
2024-04-12 11:01:32 -04:00
Vitor Pamplona
2342da114d Increases Amber signing cache. 2024-04-12 11:00:16 -04:00
Vitor Pamplona
ef0d77f8eb Removes hardcoded amber packages as default 2024-04-12 10:59:59 -04:00
Vitor Pamplona
5559b69bdb Displays Zap Split error messages in sequence instead of in multiple popups. 2024-04-12 10:58:32 -04:00
Vitor Pamplona
eda25b4cfe Callback uri is not necessary 2024-04-12 10:57:33 -04:00
Vitor Pamplona
9ce14e08fd Fixes a bug with signature-null in the sig of events. 2024-04-12 10:56:38 -04:00
Vitor Pamplona
b046fd91cb Only logs external signer calls on debug 2024-04-11 18:30:28 -04:00
Vitor Pamplona
8c9800664f v0.86.5 2024-04-11 18:20:48 -04:00
Vitor Pamplona
b90a57220d Enables Mutiny Wallet NWC 2024-04-11 18:18:54 -04:00
Vitor Pamplona
d16b0f58bb Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-11 18:04:53 -04:00
Vitor Pamplona
a538b66db3 Removes the use of DM relays to find events due to private inbox settings 2024-04-11 18:04:02 -04:00
Vitor Pamplona
f04631b0dd Adds vertical scrolling on the Zap page for collaborators. 2024-04-11 18:01:37 -04:00
Vitor Pamplona
6e31cff99c Avoids decrypting existing Nostr events just to add the relay into the relay list. 2024-04-11 18:01:22 -04:00
Vitor Pamplona
1553640c18 Calculates hash in the IO thread from Compose's scope. 2024-04-11 16:34:34 -04:00
Vitor Pamplona
68b8f9c82a Merge pull request #834 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-11 13:49:46 -04:00
Crowdin Bot
f6cce42028 New Crowdin translations by GitHub Action 2024-04-11 17:47:21 +00:00
Vitor Pamplona
0cbddad9c0 v0.86.4 2024-04-11 13:45:27 -04:00
Vitor Pamplona
b14154e2b5 Merge pull request #832 from greenart7c3/main
Do not use tor proxy when localhost, fix proxy not being used inside ImageDownloader.kt
2024-04-10 17:30:51 -04:00
greenart7c3
c4250ccd35 fix error when not using proxy 2024-04-10 14:11:11 -03:00
greenart7c3
31516964c8 Do not use tor proxy when localhost, fix proxy not being used inside ImageDownloader.kt 2024-04-10 13:51:48 -03:00
Vitor Pamplona
4722b2a617 Fixes missing Zaps and some DMs on startup 2024-04-10 11:05:21 -04:00
Vitor Pamplona
eca5b47ab0 Updates Kotlin version 2024-04-10 10:20:33 -04:00
Vitor Pamplona
d38b57025c Updates compose BOM 2024-04-10 09:00:57 -04:00
Vitor Pamplona
fa7aa3cf24 Updates AGP 2024-04-10 08:47:05 -04:00
Vitor Pamplona
d8e9b4773b v0.86.3 2024-04-09 19:41:22 -04:00
Vitor Pamplona
f9a7b13ba1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-09 19:36:49 -04:00
Vitor Pamplona
ecbf0e404d Fixes missing notifications 2024-04-09 19:36:40 -04:00
Vitor Pamplona
c2f8df963a Merge pull request #831 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-09 12:23:35 -04:00
Crowdin Bot
ff20960bb5 New Crowdin translations by GitHub Action 2024-04-09 16:20:30 +00:00
Vitor Pamplona
8dd1fc2077 Fixing F-droid's Markdown use 2024-04-09 12:18:14 -04:00
Vitor Pamplona
bb2fb2b103 Fixes max width of hidden notes 2024-04-09 12:15:27 -04:00
Vitor Pamplona
00a9c49915 - Migrates to the new Markdown Parser.
- Adds Note previews on Markdown
- Adds Custom hashtag icons to markdown.
- Adds URL preview boxes to markdown
- Performance improvements.
2024-04-08 18:53:55 -04:00
Vitor Pamplona
d2872cc8bb Refactors the url preview state 2024-04-08 18:34:10 -04:00
Vitor Pamplona
d33a1ce14f Fixes clickable route not showing the user 's npub before loading the name. 2024-04-08 18:33:50 -04:00
Vitor Pamplona
31958215be Moves loading of an embed event to thread. 2024-04-08 18:33:31 -04:00
Vitor Pamplona
bbbb614718 Moves added charts after NIP19 uris to being nullable. 2024-04-08 18:32:37 -04:00
Vitor Pamplona
776a23c256 Clearing the secondary button design or the encrypted nsec backup 2024-04-08 11:34:33 -04:00
Vitor Pamplona
0854bd34ff v0.86.2 2024-04-05 14:08:27 -04:00
Vitor Pamplona
1738a775ef Fixes the lack of the Comparator interface for the Deletion Index. 2024-04-05 14:05:04 -04:00
Vitor Pamplona
a6953872ea Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-05 11:39:34 -04:00
Vitor Pamplona
d48714456c Fixes check for Deleted Addressable events. 2024-04-05 11:34:50 -04:00
Vitor Pamplona
c25aad482b Merge pull request #829 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-05 11:23:19 -04:00
Crowdin Bot
cbebfd263b New Crowdin translations by GitHub Action 2024-04-05 15:21:28 +00:00
Vitor Pamplona
89dbe82191 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-05 11:19:23 -04:00
Vitor Pamplona
7bb72d0c2d Adds a Deletion event cache. 2024-04-05 11:19:15 -04:00
Vitor Pamplona
9be4895080 Merge pull request #828 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-05 08:19:17 -04:00
Crowdin Bot
6250db01d1 New Crowdin translations by GitHub Action 2024-04-05 12:05:47 +00:00
Vitor Pamplona
48f9045f1b Merge pull request #825 from greenart7c3/onion_url
add ws:// if not present in .onion urls
2024-04-05 08:03:52 -04:00
Vitor Pamplona
818ca7e39e Merge pull request #826 from greenart7c3/draft_decryption_error
fix draft decryption error
2024-04-05 08:03:39 -04:00
Vitor Pamplona
e8675b8e45 Merge pull request #827 from davotoula/update_translations
added  translations for CS, DE, SV
2024-04-05 08:03:11 -04:00
David Kaspar
cef7e17447 added translations for CS, DE, SV 2024-04-05 11:38:51 +01:00
greenart7c3
6b15a0db8e fix draft decryption error 2024-04-05 05:41:03 -03:00
greenart7c3
50c5845a11 add ws:// if not present in .onion urls 2024-04-05 05:30:51 -03:00
Vitor Pamplona
1b7ba3de01 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-04 23:06:08 -04:00
Vitor Pamplona
712063f5d2 Avoids double notifications. 2024-04-04 23:05:58 -04:00
Vitor Pamplona
d92f23e274 LargeCache inner map should be private 2024-04-04 23:05:35 -04:00
Vitor Pamplona
3b7f530c0b Merge pull request #824 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-04 18:15:14 -04:00
Crowdin Bot
623a8d377c New Crowdin translations by GitHub Action 2024-04-04 22:08:10 +00:00
Vitor Pamplona
79489d0b07 v0.86.1 2024-04-04 18:06:36 -04:00
Vitor Pamplona
827512b225 Avoids circular rendering of Drafts. 2024-04-04 18:06:27 -04:00
Vitor Pamplona
6acfadeb9b Reduces cache for expandable texts. 2024-04-04 18:06:09 -04:00
Vitor Pamplona
e159af2cd7 v0.86.0 2024-04-04 17:21:03 -04:00
Vitor Pamplona
89c2e9d2e0 Changes precision of Zap Splits to 1% steps 2024-04-04 17:19:03 -04:00
Vitor Pamplona
06f6ab6719 Adds button to load Zap Splits from the cited users in the text 2024-04-04 17:18:42 -04:00
Vitor Pamplona
98c48e8b6b Fixes contract violation when sorting users. 2024-04-04 16:54:14 -04:00
Vitor Pamplona
25cde455d8 removes chat bubble animation when size changes 2024-04-04 15:58:19 -04:00
Vitor Pamplona
ef0fdf553c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-04 15:24:59 -04:00
Vitor Pamplona
719b950272 Fixing the use of filters that didn't discriminate the relay type setup 2024-04-04 15:24:04 -04:00
Vitor Pamplona
2d02fad6b9 Merge pull request #823 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-04 15:16:49 -04:00
Crowdin Bot
a39db5bf7b New Crowdin translations by GitHub Action 2024-04-04 19:01:46 +00:00
Vitor Pamplona
7fd37367fc refactoring of cache methods in GiftWraps 2024-04-04 14:59:49 -04:00
Vitor Pamplona
e1c134830e Avoiding feed jitter when pressing the notification button twice. 2024-04-04 10:03:28 -04:00
Vitor Pamplona
621d1c7731 Migrating Refreshable feeds to the reusable box 2024-04-04 09:41:51 -04:00
Vitor Pamplona
7475143506 Continues the migration from LiveData to Flow 2024-04-04 08:56:15 -04:00
Vitor Pamplona
2509d639bd Merge pull request #822 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-03 13:07:08 -04:00
Crowdin Bot
85dd5cf698 New Crowdin translations by GitHub Action 2024-04-03 17:02:53 +00:00
Vitor Pamplona
274ce6ad77 Fixing the spacing for channels 2024-04-03 12:26:57 -04:00
Vitor Pamplona
0e8d2fc33a adds save when closing the screen on new posts. 2024-04-03 10:20:19 -04:00
Vitor Pamplona
b88723b68b Fixes wrong refactoring 2024-04-03 10:20:05 -04:00
Vitor Pamplona
638dba770d Moves the creation of Draft Notes to the IO Thread 2024-04-02 17:49:52 -04:00
Vitor Pamplona
4d7de6bc19 No need to switch to IO this early in the process 2024-04-02 16:08:59 -04:00
Vitor Pamplona
fbf676bdb2 Reduces recomposition of the hash verification 2024-04-02 16:08:38 -04:00
Vitor Pamplona
793780f02c Merge pull request #821 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-02 10:02:21 -04:00
Crowdin Bot
adf31ed115 New Crowdin translations by GitHub Action 2024-04-02 13:54:47 +00:00
Vitor Pamplona
96e434bfcf Fixes the lack of following mark on chats 2024-04-02 09:52:52 -04:00
Vitor Pamplona
c7563c938d Minor refactoring 2024-04-02 09:42:01 -04:00
Vitor Pamplona
4380393c5b No need to update UUID anymore. After deletion the draft can just be updated. 2024-04-02 09:23:08 -04:00
Vitor Pamplona
8125a7dabb Correctly moving the Reply line out of the renderer. 2024-04-01 18:32:36 -04:00
Vitor Pamplona
e898d58239 Finishing the rendering of card notes for DMs and live chats 2024-04-01 17:07:27 -04:00
Vitor Pamplona
29a43f82e6 Fixes lack of blurhash in some videos 2024-04-01 15:26:02 -04:00
Vitor Pamplona
469b9c6acb Only changes the keep playing status if different 2024-04-01 15:25:49 -04:00
Vitor Pamplona
38d1bf9aec Updates firebase 2024-04-01 15:01:39 -04:00
Vitor Pamplona
18b57b8ac8 Reactivating hold to edit draft. 2024-04-01 14:14:58 -04:00
Vitor Pamplona
7fc43c96d6 Merge pull request #819 from jiftechnify/meta-parser-benchmark
Add benchmark for MetaTagsParser
2024-03-29 17:46:35 -04:00
Vitor Pamplona
fc7d3a9519 Merge pull request #820 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-29 17:46:12 -04:00
Crowdin Bot
1667a78bb9 New Crowdin translations by GitHub Action 2024-03-29 21:44:07 +00:00
Vitor Pamplona
5fbd6c25d0 Fixes layout of the reply row in chats. 2024-03-29 17:41:13 -04:00
Vitor Pamplona
d079d511e8 Fixes markers for DMs 2024-03-29 17:40:51 -04:00
Vitor Pamplona
6e1418cd54 - Adds a Draft Screen
- Migrating drafts to new architecture where the Draft Event is sent to the screen instead of the inner event.
- Fixes lots of deletion and indexing bugs
2024-03-29 17:38:31 -04:00
Vitor Pamplona
cd84c07fcc Adds k-tag to the Deletion events. 2024-03-28 19:02:23 -04:00
jiftechnify
6eb2fbfa2f reduce creation of StringBuffers in meta tags parsing 2024-03-27 22:54:06 +09:00
jiftechnify
fc6f460063 fix contact link in relay information dialog 2024-03-27 22:52:16 +09:00
jiftechnify
442cdfdf2a move MetaTagsParser to common module 2024-03-27 21:19:52 +09:00
Vitor Pamplona
539433014e Fixes Notification for Follows now showing Zaps 2024-03-26 21:05:24 -04:00
Vitor Pamplona
d3f54a7082 Removes the Draft dependency for signer implementations 2024-03-26 16:56:58 -04:00
Vitor Pamplona
d3a0ae743a Merge pull request #749 from greenart7c3/main
save a draft while you are typing the post
2024-03-26 15:24:59 -04:00
Vitor Pamplona
6690d5391c Fixes controller comparison for keep playing 2024-03-26 15:06:55 -04:00
Vitor Pamplona
d61d684a27 Merge pull request #818 from jiftechnify/url-preview
Fix garbled URL preview for non UTF-8 HTML, with optimization
2024-03-26 13:40:15 -04:00
jiftechnify
4f84fad0cd remove jsoup version 2024-03-27 01:03:29 +09:00
Vitor Pamplona
cd32c4db72 Makes a cache for Media Items 2024-03-26 11:56:10 -04:00
jiftechnify
a71ce69cab support tags in quoted attribute value 2024-03-27 00:47:35 +09:00
Vitor Pamplona
b45f9bd460 Avoids launching coroutines that were just launched. 2024-03-26 09:09:35 -04:00
Vitor Pamplona
75ac17b57d increases the time to notify to 15 minutes 2024-03-26 08:35:15 -04:00
Vitor Pamplona
fa4745038f Uses the cached pool instead of the scheduled pool for translation services. 2024-03-26 08:34:45 -04:00
Vitor Pamplona
0182011487 Fixes NPE with the cached state. 2024-03-25 20:11:23 -04:00
Vitor Pamplona
37fdb8b2aa Removes unecessary parameters in the UserDisplay function 2024-03-25 17:58:54 -04:00
Vitor Pamplona
9ade18e1c1 Adds a bot field to the user info 2024-03-25 17:57:27 -04:00
Vitor Pamplona
aff6588791 Removing unnecessary layouts from the top bar 2024-03-25 17:57:11 -04:00
Vitor Pamplona
4274d2ddbd Blocks previews from being too big 2024-03-25 17:54:41 -04:00
Vitor Pamplona
63600f4782 Removes time measuring log 2024-03-25 16:45:21 -04:00
Vitor Pamplona
5deb9af477 Avoids publishing with two equal hashtags. 2024-03-25 16:41:52 -04:00
Vitor Pamplona
38e701a363 Moves the Following Vector to a native composable. 2024-03-25 16:41:38 -04:00
greenart7c3
6e6fa66c53 Merge branch 'main' into main 2024-03-25 17:34:22 -03:00
Vitor Pamplona
1f60d39cbf turning hashtag icons into programmable vectors 2024-03-25 16:02:36 -04:00
Vitor Pamplona
8ba474e79a Adjusting some of the icon sizes on the galleries 2024-03-25 14:23:36 -04:00
Vitor Pamplona
1123b3b3c1 Removes the need to observe author changes to event. 2024-03-25 13:51:08 -04:00
jiftechnify
bffb9f3778 remove jsoup from dependencies 2024-03-26 02:42:15 +09:00
jiftechnify
e11961695f parse HTML as little as possible 2024-03-26 02:31:06 +09:00
jiftechnify
042579ddfb remove unnecessary items from mata-tag canditates for getting URL info 2024-03-25 23:53:47 +09:00
jiftechnify
d0aa7430ca optimize HTML charset detection 2024-03-25 23:53:47 +09:00
jiftechnify
3434c31487 fix garbled URL preview for non-UTF-8 HTML 2024-03-25 23:53:43 +09:00
Vitor Pamplona
7eefbee0e3 Realigning the reaction icons and texts between main feed and video feed. 2024-03-25 10:44:10 -04:00
greenart7c3
ed4d867622 save draft when toogling nip4 and nip 44 2024-03-25 07:48:30 -03:00
greenart7c3
27db2b91ab save draft when changing options from polls and classifieds 2024-03-25 07:32:31 -03:00
greenart7c3
a2316b6ed0 Merge branch 'main' into main 2024-03-25 07:08:08 -03:00
Vitor Pamplona
a26b5490b2 updates compose 2024-03-24 18:53:11 -04:00
Vitor Pamplona
9990f458bf Moves Channel to LargeCache
Fixes filter for Chat Messages in discovery
2024-03-23 17:21:02 -04:00
Vitor Pamplona
0b40b6d1d8 Sorts chats by the latest message in Discovery 2024-03-23 17:19:54 -04:00
Vitor Pamplona
5a1c9f5a4a Ranks communities by the last post in each community in the discovery screen 2024-03-23 17:18:27 -04:00
Vitor Pamplona
eb6d31cf2b Better filter to bring public chat messages and avoid public chat creation and metadata updates 2024-03-23 17:16:43 -04:00
Vitor Pamplona
2c5e07de87 removes the use of data classes to speed up comparisons. 2024-03-23 17:15:38 -04:00
Vitor Pamplona
8d70664cc1 Fixes the list selection on the Discovery page. 2024-03-23 17:15:19 -04:00
Vitor Pamplona
102a34afca Merge pull request #814 from tyiu/fix-nip44-encrypt-decrypt-test
Fix NIP-44 encryptDecryptTest to decrypt with swapped private and public keys to follow NIP-44 documentation
2024-03-23 08:20:24 -04:00
Terry Yiu
c843e07709 Fix Kotlin encryptDecryptTest to decrypt with swapped private and public keys to follow NIP-44 documentation 2024-03-22 23:51:57 -04:00
Vitor Pamplona
3faa4983f0 Merge pull request #813 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-22 14:43:15 -04:00
Crowdin Bot
6f9dbbb8c3 New Crowdin translations by GitHub Action 2024-03-22 18:30:49 +00:00
greenart7c3
62a114b981 Merge branch 'main' into main 2024-03-22 08:48:47 -03:00
Vitor Pamplona
ff5612203d Fixes the width of muted messages on chat feeds. 2024-03-21 17:32:29 -04:00
Vitor Pamplona
6f0e4f1f19 Makes hidden cards full width on the discovery feed 2024-03-21 16:55:11 -04:00
Vitor Pamplona
8f5820f46d Fixes the reply finder for the reply row on compose 2024-03-21 16:54:50 -04:00
Vitor Pamplona
3086d3957d Moves from habla to njump 2024-03-21 09:27:57 -04:00
Vitor Pamplona
c00319812a Reverts to make sure the logged in user's post do not appear in notifications. 2024-03-21 09:25:26 -04:00
Vitor Pamplona
67202c32d4 Migrates channel notes to LargeCache 2024-03-21 08:51:23 -04:00
Vitor Pamplona
64909bfb32 Fixes Chat preview images when no image has been set. 2024-03-21 08:50:37 -04:00
Vitor Pamplona
c1756b75a7 Fixes double mention to Community headers when seeing a reply to a community post. 2024-03-21 08:42:14 -04:00
Vitor Pamplona
bdf41f53fb Removes logs 2024-03-21 08:41:29 -04:00
Vitor Pamplona
3b982b8962 Fixes the centralizing of the counter after the list of participants in a live event. 2024-03-20 17:02:13 -04:00
Vitor Pamplona
fd39ff24e1 Adds a space after the Channel header in the reply rows 2024-03-20 16:48:24 -04:00
Vitor Pamplona
e659153730 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-20 16:48:04 -04:00
Vitor Pamplona
f3c4b3255b Fixes missing context in some replies to blog posts. 2024-03-20 16:47:53 -04:00
Vitor Pamplona
f35f54166d Merge pull request #812 from greenart7c3/fix-amber-deep-sleep
add a lifeCycleOwner to register external signer on resume
2024-03-20 16:44:30 -04:00
greenart7c3
538c0493ed add a lifeCycleOwner to register external signer on resume 2024-03-20 17:20:35 -03:00
Vitor Pamplona
ceca149eb7 Doesn't show edits of blog posts in the User's Profile 2024-03-20 16:03:13 -04:00
Vitor Pamplona
97cdc0bc7a Fixes layout issues of LongForm content when the image is not present. 2024-03-20 16:02:49 -04:00
Vitor Pamplona
f2a8e51b20 Refactors horizontal dividers 2024-03-20 15:18:00 -04:00
greenart7c3
8d7a3f4d5e Merge branch 'main' into main 2024-03-20 15:13:33 -03:00
greenart7c3
c087042f7d Merge branch 'main' into main 2024-03-20 15:10:32 -03:00
greenart7c3
644d2fc2bb add generic draft event to default permissions 2024-03-20 14:56:29 -03:00
Vitor Pamplona
b819f24790 Reduces the space between chat bubbles. 2024-03-20 13:55:47 -04:00
greenart7c3
ea33cc77ed add edit draft in the dropdown menu and the long press popup 2024-03-20 14:50:53 -03:00
Vitor Pamplona
27fbf1c1ed Activates the chat simplified mode for the simplified setting. 2024-03-20 13:48:04 -04:00
Vitor Pamplona
3226e4e024 1. Refactors the use of dividers out of components
2. Refactors composables to load events, check hidden and check report
2024-03-20 13:47:47 -04:00
greenart7c3
090b643f43 add edit draft in the dropdown menu and the long press popup 2024-03-20 14:45:45 -03:00
Vitor Pamplona
943a4260ff Testing reduced line height 2024-03-20 11:49:12 -04:00
Vitor Pamplona
9867ac1689 Fixes videos not being able to seekTo the zero position. 2024-03-20 11:35:22 -04:00
Vitor Pamplona
d26de39749 Fixes lack of zap amount refresh after zapping a note. 2024-03-20 11:35:03 -04:00
Vitor Pamplona
1072b7a5c5 Fixes following by geotags 2024-03-20 11:34:40 -04:00
greenart7c3
220ce75f19 add validations to draft notes 2024-03-20 10:25:41 -03:00
greenart7c3
bc180ae210 fix draft delete not working 2024-03-20 09:31:31 -03:00
greenart7c3
5910ef199f fix build after merge 2024-03-20 07:22:55 -03:00
greenart7c3
499939ed68 Merge branch 'main' into main 2024-03-20 07:06:24 -03:00
Vitor Pamplona
d15beb2ae5 Fixes too strict timing constraints for new posts. 2024-03-19 19:55:41 -04:00
Vitor Pamplona
87fafd9451 Reducing the amount of co-routines being launched in each LiveData update. 2024-03-19 19:30:49 -04:00
Vitor Pamplona
bfbfcb6ed2 Avoids triggering an error when decoding invalid hexes. 2024-03-19 19:29:50 -04:00
Vitor Pamplona
ffc0a7c6ed Avoids showing error machine-level details when paying for zaps on external wallets 2024-03-19 17:44:51 -04:00
Vitor Pamplona
77789379c0 Minimizes memory use to calculate zaps. 2024-03-19 17:44:30 -04:00
Vitor Pamplona
a0a10b2cd0 Fixes some warnings 2024-03-19 17:34:26 -04:00
Vitor Pamplona
d59b98089a Aligns the BOM between implementation and tests. 2024-03-19 17:33:54 -04:00
Vitor Pamplona
160e52bd91 Fixes a bug with the latest version of jackson 2024-03-19 17:33:34 -04:00
Vitor Pamplona
d7e1a80465 Might be needed for those importing the AAR. 2024-03-19 17:33:09 -04:00
Vitor Pamplona
a5496445d2 Simple refactoring. 2024-03-19 14:49:56 -04:00
Vitor Pamplona
b75c3e3031 Refactoring caching systems for the Compose layer. 2024-03-19 14:17:15 -04:00
Vitor Pamplona
e9830c61aa Removes unused addMargin param. 2024-03-19 11:50:01 -04:00
Vitor Pamplona
25f28d38d5 Only shows OTS to the respective Edit 2024-03-19 11:49:50 -04:00
Vitor Pamplona
410d6bd690 Fixes binary payloads in the search results. 2024-03-19 11:49:03 -04:00
Vitor Pamplona
99270662ee Optimizing the performance of Highlight rendering 2024-03-19 10:30:21 -04:00
Vitor Pamplona
e40cd8d932 After clearing memory, only trigger liveData when it actually changed. 2024-03-19 10:07:21 -04:00
Vitor Pamplona
9858d98722 Avoids double request to show anyway when the user has hidden a post and ALSO the user's followers have reported the post. 2024-03-19 10:06:53 -04:00
Vitor Pamplona
6ab5488852 Starts with complete to avoid user shock 2024-03-19 10:06:16 -04:00
Vitor Pamplona
9651563b16 Fixing the test cases for nip96 2024-03-18 19:21:16 -04:00
Vitor Pamplona
9012bdad27 Fixes some of the testing cases 2024-03-18 18:19:05 -04:00
Vitor Pamplona
ef859c85a4 Merge pull request #810 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-18 18:09:18 -04:00
Crowdin Bot
959b2ac4e4 New Crowdin translations by GitHub Action 2024-03-18 22:08:29 +00:00
Vitor Pamplona
55a6f8829e Adds a large benchmark test for duplicate events. 2024-03-18 18:06:37 -04:00
greenart7c3
940fa2ee8d add debouncer 2024-03-18 17:47:15 -03:00
Vitor Pamplona
92d9c682f8 This should be better than launching a new thread after LaunchedEffect already lauched hers. 2024-03-18 16:38:40 -04:00
Vitor Pamplona
f5a1007f88 Seeing if the performance improves when the factory becomes just another lambda 2024-03-18 16:23:00 -04:00
Vitor Pamplona
d83acab84b - Solves infinite Quotation issue.
- Restructures NoteCompose for performance
- Restructures markAsRead to minimize threading cost.
2024-03-18 15:26:49 -04:00
greenart7c3
f6e5af3e98 support for dms, streams and communities 2024-03-18 16:18:12 -03:00
Vitor Pamplona
ac56d02b9d Updates AGP to 8.3.1 2024-03-18 15:10:39 -04:00
Vitor Pamplona
46a120450e Merge pull request #809 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-18 15:10:16 -04:00
Crowdin Bot
33e0107d3b New Crowdin translations by GitHub Action 2024-03-18 19:08:49 +00:00
greenart7c3
8b3e3e7af8 support Classifieds 2024-03-18 14:51:50 -03:00
greenart7c3
84faa7557e add support for polls 2024-03-18 14:37:51 -03:00
greenart7c3
f7ab925b1d fix delete on close 2024-03-18 14:21:58 -03:00
greenart7c3
204eaa4606 fix crash when loading draft from channels or lives 2024-03-18 11:16:13 -03:00
greenart7c3
1c249eed20 load post from draft 2024-03-18 10:56:36 -03:00
greenart7c3
3cc32ecd9a create a loadFromDraft method 2024-03-18 09:09:00 -03:00
greenart7c3
0a20d5484b show drafts as soon as its created 2024-03-18 08:39:55 -03:00
greenart7c3
6e4f1269dd add todo 2024-03-18 07:43:19 -03:00
greenart7c3
eba0837e52 fix draftnotes filter 2024-03-18 07:20:30 -03:00
greenart7c3
d682518ddb Merge branch 'main' into main 2024-03-18 07:07:32 -03:00
Vitor Pamplona
8c43f3492b Fixes some imports for benchmarks 2024-03-16 11:08:31 -04:00
Vitor Pamplona
688abee205 Merge pull request #808 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-16 10:35:35 -04:00
Crowdin Bot
d96ea0f8a2 New Crowdin translations by GitHub Action 2024-03-16 14:32:46 +00:00
Vitor Pamplona
b19b60128f Updates to compose and jackson 2024-03-16 10:29:56 -04:00
Vitor Pamplona
78be5a9ecc Reorganizing classes in the commons lib 2024-03-15 21:28:13 -04:00
Vitor Pamplona
36d2a3e42c Isolating the LargeCache forEach method to allow quick time measurements on filters. 2024-03-15 21:14:06 -04:00
Vitor Pamplona
fa7c2efaab Fixing size and alignment of the text when the live stream video is not present. 2024-03-15 21:13:15 -04:00
Vitor Pamplona
6edc634b82 Only download video, image and audio files in NIP-94 2024-03-15 19:55:14 -04:00
Vitor Pamplona
6bdf3e2625 Restructures Data Access filters and LocalCache to use a ConcurrentSkipList instead of ConcurrentHashMap 2024-03-15 19:32:43 -04:00
Vitor Pamplona
2d17200f03 Adds two additional helper methods to ATags and Classifieds. 2024-03-15 19:29:27 -04:00
Vitor Pamplona
c74176684f Preparing to refactor the Relay classes. 2024-03-15 19:28:59 -04:00
Vitor Pamplona
1b9742597a Keeps cache size in 10 to account for recompositions 2024-03-15 19:26:34 -04:00
greenart7c3
f949d5624e add draft support for public chat screen 2024-03-15 13:35:54 -03:00
greenart7c3
2bc2890d08 add draft support for other event kinds 2024-03-15 12:35:14 -03:00
greenart7c3
f3f8bc1b65 show draft in the simplified view 2024-03-15 10:45:53 -03:00
greenart7c3
99e9514d6c remove drafts from shared prefs 2024-03-15 10:38:22 -03:00
greenart7c3
8ade5b7e5f Merge branch 'main' into main 2024-03-15 09:18:38 -03:00
greenart7c3
e292affbe6 add draft in the home feed 2024-03-15 09:08:35 -03:00
Vitor Pamplona
fbf4f6dd08 Small Refactoring 2024-03-14 14:09:13 -04:00
Vitor Pamplona
b2508b3db5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-14 11:55:32 -04:00
Vitor Pamplona
1abdb42552 Sets default to the simplified view. 2024-03-14 11:55:24 -04:00
Vitor Pamplona
2c54ba1a92 Corrects the starting point to show reactions on messages. 2024-03-14 11:55:13 -04:00
Vitor Pamplona
f941397cc4 Fixes tickmarks on dropdowns 2024-03-14 11:54:53 -04:00
Vitor Pamplona
69e31be37f Merge pull request #807 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-14 11:14:42 -04:00
Crowdin Bot
86c328c8a7 New Crowdin translations by GitHub Action 2024-03-14 15:13:23 +00:00
Vitor Pamplona
837865a699 Adds simplified views to the video and chat feeds. 2024-03-14 11:11:38 -04:00
Vitor Pamplona
1014e29289 Fixes some contract issues when follow and names are the same. 2024-03-14 09:54:43 -04:00
Vitor Pamplona
c1c5bc2039 Merge pull request #806 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-13 18:03:06 -04:00
Crowdin Bot
2d6aab954f New Crowdin translations by GitHub Action 2024-03-13 22:00:20 +00:00
Vitor Pamplona
eadb28321c Adds missing classes to support WebServer conections in the Video Playback 2024-03-13 17:58:26 -04:00
Vitor Pamplona
99850573f7 Reduces video cache from 10 to 4 videos. 2024-03-13 17:58:01 -04:00
Vitor Pamplona
45974fb09b change the isOnlineCheck to prepare for nostr nests websocket-based streaming. 2024-03-13 17:57:31 -04:00
Vitor Pamplona
f2dc2ef0d0 Uses the new default factory instead of the 3 separate methods. 2024-03-13 17:57:08 -04:00
Vitor Pamplona
8641bd36c3 Fixes Scheduled Tag in LiveStreams 2024-03-13 17:28:30 -04:00
Vitor Pamplona
a034d2f96e Updating Jackson 2024-03-13 14:02:50 -04:00
greenart7c3
fa5d992010 add draftevent class 2024-03-13 13:24:22 -03:00
greenart7c3
0d47e8b823 Merge branch 'main' into main 2024-03-13 11:24:12 -03:00
Vitor Pamplona
e289730be5 Fixes the cut off when having massive uncut string chars 2024-03-13 09:42:38 -04:00
greenart7c3
91b0d5b7fc Merge branch 'main' into main 2024-03-13 07:17:52 -03:00
Vitor Pamplona
0dbd58d8d7 Fixing some of the old display name structure 2024-03-12 17:51:27 -04:00
Vitor Pamplona
e094b56b72 Deleting old Settings local db 2024-03-12 17:18:06 -04:00
Vitor Pamplona
efb9d9268b Avoids parsing metadata events several times due to the json parser stopping coroutines. 2024-03-12 17:16:50 -04:00
Vitor Pamplona
82c4bf89df Avoids serializing temporary fields. 2024-03-12 16:32:43 -04:00
Vitor Pamplona
4a069fa73b Simplifies if a chatroom sender intersects with the follow list. 2024-03-12 16:21:18 -04:00
Vitor Pamplona
1cda191035 Moves the play button to the profile page. 2024-03-12 15:51:02 -04:00
Vitor Pamplona
e5d4b2a145 Adds a simplified version to the UI. 2024-03-12 15:50:15 -04:00
Vitor Pamplona
6ff5787e60 Merge pull request #805 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-12 11:23:20 -04:00
Crowdin Bot
ffb73b1780 New Crowdin translations by GitHub Action 2024-03-12 15:14:40 +00:00
Vitor Pamplona
9a6a857a11 Brings the ZapForwarding icon to Compose 2024-03-12 11:11:46 -04:00
Vitor Pamplona
abb9edc46b Refreshing the boostrap relay list. 2024-03-12 10:29:52 -04:00
Vitor Pamplona
5d78d9b046 updates dependencies 2024-03-12 10:20:09 -04:00
Vitor Pamplona
f372a256c5 formatting 2024-03-11 19:46:00 -04:00
Vitor Pamplona
e2fe1538b0 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-11 19:12:16 -04:00
Vitor Pamplona
7560a737cd Updated hashtag icons for performance 2024-03-11 19:10:22 -04:00
Vitor Pamplona
24a7812432 Merge pull request #804 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-11 15:30:34 -04:00
Crowdin Bot
df99c56c4a New Crowdin translations by GitHub Action 2024-03-11 18:38:52 +00:00
Vitor Pamplona
e984487d9e avoids escaping errors from events inside strings 2024-03-11 13:35:56 -04:00
Vitor Pamplona
afb5169ede Fitlers too many reposts out of the main feed. 2024-03-11 13:10:29 -04:00
Vitor Pamplona
64d6d6753c Centers Blank Note when post was hidden by the user. 2024-03-09 11:32:28 -05:00
Vitor Pamplona
0f69030cdf Improves Preview Utilities 2024-03-09 11:32:08 -05:00
Vitor Pamplona
dd81c51fab Removes dependency of the Robohash from CryptoUtils 2024-03-09 11:30:55 -05:00
Vitor Pamplona
6cd04a7617 Fixes space after clickable user display 2024-03-08 21:51:56 -05:00
Vitor Pamplona
632bd77db3 Switches Robohash to Precompiled SVGs in order to reduce the memory burned of creating Strings with SVGs on the fly. 2024-03-08 18:41:07 -05:00
greenart7c3
4938ba03a6 Merge branch 'main' into main 2024-03-08 12:35:23 -03:00
Vitor Pamplona
e6da340879 resets the show full text flag after 20 views. 2024-03-06 15:02:59 -05:00
Vitor Pamplona
c796cbd7be Fixes the fdroid flavor 2024-03-06 14:27:22 -05:00
Vitor Pamplona
2038994613 Holds the state of expanded text button between edits and translations. 2024-03-06 14:25:19 -05:00
Vitor Pamplona
5f76cdf721 Center the text of notes that couldn't be found. 2024-03-06 11:31:03 -05:00
Vitor Pamplona
b8619e3b61 - User Metadata clean up upon receipt instead of in every rendering.
- Simpler/Faster UserDisplay renderings
- Removes co-routing use for Hashtags.
2024-03-06 10:32:08 -05:00
Vitor Pamplona
da41fbb4c9 Updates secp256k1KmpJniAndroid 2024-03-05 19:05:03 -05:00
Vitor Pamplona
0dd553ae55 Fixes search on binary content. 2024-03-05 16:45:41 -05:00
Vitor Pamplona
563663c131 Merge pull request #800 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-05 16:20:23 -05:00
Crowdin Bot
dc3574bb6e New Crowdin translations by GitHub Action 2024-03-05 21:12:16 +00:00
Vitor Pamplona
f6ffb87e20 Avoids allocating Strings for the JSON conversion when checking if a filter has changed. 2024-03-05 16:09:50 -05:00
Vitor Pamplona
3b3ca06c1c Massive refactoring to reduce the size of NoteCompose 2024-03-05 09:30:47 -05:00
Vitor Pamplona
f73c9b5773 v0.85.3 2024-03-04 18:09:03 -05:00
Vitor Pamplona
5848255e72 Displaying issues and Patches in the Notification 2024-03-04 18:01:12 -05:00
Vitor Pamplona
1cf828b165 v0.85.2 2024-03-04 17:52:08 -05:00
Vitor Pamplona
2e1f7d6587 Merge pull request #799 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-04 17:00:26 -05:00
Crowdin Bot
c6977d97d3 New Crowdin translations by GitHub Action 2024-03-04 21:54:12 +00:00
Vitor Pamplona
d12e886e9e Bringing GitIssues in the Notification 2024-03-04 16:52:38 -05:00
Vitor Pamplona
8299f4cfca Cleans up the fork information on new posts has been canceled or posted. 2024-03-04 16:20:59 -05:00
Vitor Pamplona
432b1e4902 Fixes forking information kept from a previous post. 2024-03-04 16:15:09 -05:00
Vitor Pamplona
e9fd62dc26 Displaying correct edits on the new edit proposal 2024-03-04 15:37:29 -05:00
Vitor Pamplona
f42ec3c149 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-04 15:25:43 -05:00
Vitor Pamplona
43e1e0f23e Merge pull request #798 from greenart7c3/multiple-results-crash
fix crash parsing multiple results from amber
2024-03-04 15:17:52 -05:00
Vitor Pamplona
7bc393143c Adds link to the version notes from the drawer 2024-03-04 15:12:17 -05:00
greenart7c3
23718f51dd fix crash parsing multiple results from amber 2024-03-04 17:11:01 -03:00
Vitor Pamplona
8b2efecfbe Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-04 14:13:32 -05:00
Vitor Pamplona
9081d5a54b Adds nostr git for issue management software. 2024-03-04 14:13:23 -05:00
Vitor Pamplona
709e3bdd3a Merge pull request #797 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-04 13:22:09 -05:00
Crowdin Bot
1f66ef717b New Crowdin translations by GitHub Action 2024-03-04 18:18:38 +00:00
Vitor Pamplona
cd2b5d78a1 Don't show button to edit the post if the author of the original post is not the logged in user 2024-03-04 13:16:34 -05:00
Vitor Pamplona
2f45067949 v0.85.1 2024-03-04 12:00:02 -05:00
Vitor Pamplona
5a680beabb Fixes missing Fhir Classes on Release 2024-03-04 11:58:24 -05:00
Vitor Pamplona
779ce110ba Adding the most recent release features to the Readme. 2024-03-04 11:26:22 -05:00
Vitor Pamplona
43bcee5642 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-04 11:03:55 -05:00
Vitor Pamplona
64c4f7dc95 Removes extra new line 2024-03-04 10:29:19 -05:00
Vitor Pamplona
2b6cd07805 Merge pull request #796 from davotoula/accessibility_updates
Accessibility updates
2024-03-04 10:29:05 -05:00
David Kaspar
875d538581 added accessibility translations for CS, DE, SV, PT 2024-03-04 15:03:36 +00:00
David Kaspar
f0c53efebd app logo 2024-03-04 14:56:59 +00:00
David Kaspar
8c5aea46f1 refactor incognito button icon 2024-03-04 14:56:59 +00:00
David Kaspar
e507e0bb6a update Play text 2024-03-04 14:56:59 +00:00
David Kaspar
2f3df616f3 QR code icon 2024-03-04 14:56:59 +00:00
David Kaspar
75934b5b98 Relay list screeen 2024-03-04 14:56:59 +00:00
David Kaspar
4cbdf11ecb Send message button 2024-03-04 14:56:59 +00:00
David Kaspar
bbf0d36cdf Sealed message button on send message 2024-03-04 14:56:59 +00:00
David Kaspar
2bef5f015d change ArrowForwardIos to AutoMirrored 2024-03-04 14:56:59 +00:00
David Kaspar
708bd00d0d updated vertical dots with content descriptions 2024-03-04 14:56:59 +00:00
David Kaspar
2158e36032 Correct spelling 2024-03-04 14:56:59 +00:00
David Kaspar
08a103d332 updated some icons with content descriptions 2024-03-04 14:56:59 +00:00
Vitor Pamplona
57bcae6a0d Merge pull request #795 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-04 09:43:33 -05:00
Crowdin Bot
4897c28ed3 New Crowdin translations by GitHub Action 2024-03-04 14:38:48 +00:00
Vitor Pamplona
b3211cbb35 Removes the release drafter plugin on actions. Too buggy 2024-03-04 09:37:32 -05:00
Vitor Pamplona
fa6adcde75 v0.85.0 2024-03-04 09:10:11 -05:00
Vitor Pamplona
ad65e02fc9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-04 09:09:21 -05:00
Vitor Pamplona
249ac4e039 Revert "v0.85.0"
This reverts commit a92ef7a375.
2024-03-04 09:08:20 -05:00
Vitor Pamplona
a92ef7a375 v0.85.0 2024-03-04 09:08:06 -05:00
Vitor Pamplona
e65faa2076 Merge pull request #794 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-03 11:14:10 -05:00
Crowdin Bot
5f53fc6aa9 New Crowdin translations by GitHub Action 2024-03-03 16:07:20 +00:00
Vitor Pamplona
218e133857 Merge pull request #793 from davotoula/update_translations
Updated translations. DE, SV, PT
2024-03-03 11:06:06 -05:00
David Kaspar
fcd61d87be Updated translations. DE, SV, PT 2024-03-03 09:28:10 +00:00
Vitor Pamplona
fb280f4880 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-02 16:18:33 -05:00
Vitor Pamplona
5342c4cfba updates mockk 2024-03-02 16:18:11 -05:00
Vitor Pamplona
dd106f315f Merge pull request #792 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-02 16:14:02 -05:00
Crowdin Bot
3ddd06fafc New Crowdin translations by GitHub Action 2024-03-02 21:13:28 +00:00
Vitor Pamplona
4fb68dd014 Fixes content title for the video playback notification 2024-03-02 16:11:01 -05:00
Vitor Pamplona
a301421915 Adds Home-only empty feed screen. 2024-03-02 15:57:49 -05:00
Vitor Pamplona
16c171ec40 Fixes quote-notes and reposted notes partially disappearing when they contain hidden users or words. 2024-03-02 12:37:02 -05:00
Vitor Pamplona
6725114b48 Avoids changing the showhidden value all the time, triggering recompositions. 2024-03-02 12:35:30 -05:00
Vitor Pamplona
861ad218c8 Fixes bug with NIP-11s with null kinds 2024-03-02 12:03:16 -05:00
Vitor Pamplona
b694ac7259 Visualizing proposed edits from other people 2024-03-01 21:15:43 -05:00
Vitor Pamplona
81cc985e3b Adds the ability to see and reply to Git Issues and Patches. 2024-03-01 18:09:15 -05:00
Vitor Pamplona
786802b708 Presents notifications when a person forks your content. 2024-03-01 17:07:19 -05:00
Vitor Pamplona
c53fc03157 Cache must update before updating the screen. 2024-03-01 16:52:18 -05:00
Vitor Pamplona
48a6923537 Forces edits to be from the same user. 2024-03-01 16:15:18 -05:00
Vitor Pamplona
c34811e05b Tries to highlight that there are two states here for Compose to observe. 2024-03-01 16:15:06 -05:00
Vitor Pamplona
3f500ef5b3 Make sure to add the forked author to the p-tag of a note. 2024-03-01 16:04:26 -05:00
Vitor Pamplona
cbce22c994 Fixes the alt tag 2024-03-01 15:46:58 -05:00
Vitor Pamplona
bc7a578cfe Making forks work with revisions. 2024-03-01 15:39:24 -05:00
Vitor Pamplona
af784a5bda Editting the current version of the post instead of only the original post. 2024-03-01 14:50:28 -05:00
Vitor Pamplona
b1a355691a Fixes another caching issue when multiple posts include the same poll. 2024-03-01 13:41:43 -05:00
Vitor Pamplona
4f2b2992e0 Merge pull request #791 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-01 12:41:09 -05:00
Crowdin Bot
77d30b6e7c New Crowdin translations by GitHub Action 2024-03-01 17:30:38 +00:00
Vitor Pamplona
228de99ccf Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-01 12:28:48 -05:00
Vitor Pamplona
dfec7ae7f5 Better aligns header elements 2024-03-01 12:28:42 -05:00
Vitor Pamplona
f8c4242843 Merge pull request #790 from davotoula/adding_translations
Updated translations. DE, SV, PT, CS
2024-03-01 12:17:18 -05:00
Vitor Pamplona
34150b6725 Merge branch 'main' into adding_translations 2024-03-01 12:17:12 -05:00
Vitor Pamplona
21a18cfa38 Rotates all versions of a note 2024-03-01 12:15:48 -05:00
David Kaspar
8fa6d1d490 Updated translations. DE, SV, PT, CS 2024-03-01 17:12:06 +00:00
Vitor Pamplona
c23510e05e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-03-01 10:41:57 -05:00
Vitor Pamplona
1cc4fd5183 Closes popup when finished editing. 2024-03-01 10:40:43 -05:00
Vitor Pamplona
7468352795 Removes warnings from the quartz library. 2024-03-01 10:40:12 -05:00
Vitor Pamplona
238e577b93 avoids changing and reading the state at the same time 2024-03-01 10:39:49 -05:00
Vitor Pamplona
0653a69bbd Replaces deprecated put method 2024-03-01 10:39:32 -05:00
Vitor Pamplona
87fcba02b3 Move project to version catalogs 2024-03-01 10:38:03 -05:00
Vitor Pamplona
3fb8a5938f Updates to Android Studio Iguana 2024-03-01 08:03:58 -05:00
Vitor Pamplona
f2a9c2b34b Merge pull request #789 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-03-01 07:59:24 -05:00
Crowdin Bot
cf1e5f952d New Crowdin translations by GitHub Action 2024-03-01 12:57:52 +00:00
Vitor Pamplona
b6e096ec96 Only display edit option when looking at a text note. 2024-02-29 20:00:19 -05:00
Vitor Pamplona
ee4e20bac8 Fixing alt of git reply 2024-02-29 19:58:18 -05:00
Vitor Pamplona
4d2a7bbc32 Fixes Poll Caching problems. 2024-02-29 18:51:39 -05:00
Vitor Pamplona
600aab5e8d Reverts the ReactionRow change due to misalignments 2024-02-29 18:51:27 -05:00
Vitor Pamplona
1efef5887a Reduces the cache size for parsed posts. 2024-02-29 18:50:55 -05:00
Vitor Pamplona
3495866017 Improving cache of cashu tokens 2024-02-29 18:03:07 -05:00
Vitor Pamplona
0755bd2474 Improving the cache of LnInvoices. 2024-02-29 18:02:52 -05:00
Vitor Pamplona
d257f7e253 Fixes not being able to create an invoice because of an old cache of the lud16
Fixes inserting invoice in the cursor.
2024-02-29 17:35:44 -05:00
Vitor Pamplona
7497237763 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-29 17:06:22 -05:00
Vitor Pamplona
d506cf13a9 Adjustments to padding of the last element and the size of the relay box in kind 1 2024-02-29 17:05:41 -05:00
Vitor Pamplona
05454f5420 Merge pull request #788 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-29 15:10:18 -05:00
Crowdin Bot
8d8990899b New Crowdin translations by GitHub Action 2024-02-29 19:55:30 +00:00
Vitor Pamplona
fd37826663 Refactoring to improve legibility and compose stability of FhirResources 2024-02-29 14:52:56 -05:00
Vitor Pamplona
0084c2b532 Improves the speed of Location services. 2024-02-29 14:19:50 -05:00
Vitor Pamplona
c686e775bf Deletes unused code 2024-02-29 10:09:19 -05:00
Vitor Pamplona
6e0701b65d Removes unused kotlin serialization 2024-02-29 09:22:15 -05:00
Vitor Pamplona
417c29bd72 Updates Compose UI Version 2024-02-29 09:18:56 -05:00
Vitor Pamplona
3eb6983a94 Support for Kind 1 Edits 2024-02-28 17:42:06 -05:00
Vitor Pamplona
d4ba135368 Second embed bundle tests with nsecs 2024-02-28 16:54:28 -05:00
Vitor Pamplona
7f35d7d416 Improves rendering of prescriptions 2024-02-28 16:52:33 -05:00
Vitor Pamplona
9f4a30657b Fixes Ots Observation check 2024-02-28 16:51:14 -05:00
Vitor Pamplona
86aab2e842 Fixes DM chatroom edit button 2024-02-28 16:50:58 -05:00
Vitor Pamplona
73e3b60721 Fixes missing nsec processing 2024-02-28 16:50:23 -05:00
Vitor Pamplona
fdb0830d1b Refactors extra chars for Bech Links 2024-02-28 11:23:56 -05:00
Vitor Pamplona
bc833fadcf Fixes event loading for nembeds 2024-02-28 11:23:05 -05:00
greenart7c3
4d2c17cd1c fix draft on nip94 2024-02-28 11:16:19 -03:00
greenart7c3
53987336c0 Merge branch 'main' into main 2024-02-28 10:59:39 -03:00
Vitor Pamplona
d36efd6a2c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-27 20:56:31 -05:00
Vitor Pamplona
0d92384d8c Fixes OTS test cases 2024-02-27 19:16:06 -05:00
Vitor Pamplona
466929a0f7 Merge pull request #787 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-27 18:56:30 -05:00
Crowdin Bot
85059d775b New Crowdin translations by GitHub Action 2024-02-27 23:52:02 +00:00
Vitor Pamplona
328ed226c6 Basic support for FHIR payloads.
Support for nembed
2024-02-27 18:50:20 -05:00
Vitor Pamplona
e305f3c198 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-27 15:40:49 -05:00
Vitor Pamplona
f78a252c8f - Adds nembed
- Redesigns NIP-19 returns to allow nembeds and give more nuance to the UI.
2024-02-27 15:39:15 -05:00
Vitor Pamplona
6e65bdbb91 Merge pull request #786 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-27 10:40:49 -05:00
Crowdin Bot
9639514a82 New Crowdin translations by GitHub Action 2024-02-27 15:37:25 +00:00
Vitor Pamplona
87fabd54de Completed OTS Support 2024-02-27 10:34:49 -05:00
Vitor Pamplona
529403d92f Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-26 20:52:24 -05:00
Vitor Pamplona
e2d86c1727 Adds a new report option as other 2024-02-26 20:52:15 -05:00
Vitor Pamplona
11ff0736e4 Speeding up the loop through local cache. 2024-02-26 20:51:59 -05:00
Vitor Pamplona
fbc63a6df1 Merge pull request #784 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-26 19:04:10 -05:00
Crowdin Bot
7dccac41e9 New Crowdin translations by GitHub Action 2024-02-26 23:53:45 +00:00
Vitor Pamplona
8ebad524ff Adds local cache to wait for OTS confirmations
Presents OTS pending and confirmations on the screen
Adds Menu item to OTS any post.
2024-02-26 18:51:23 -05:00
Vitor Pamplona
bf67ff64b4 Moves the image preview on new posts to the top of additional options for the post. 2024-02-26 15:47:44 -05:00
Vitor Pamplona
85ecaf18cc Sets split zaps for all quotes and forks 2024-02-26 15:47:22 -05:00
Vitor Pamplona
3c36f52baf Visual support for Kind1 forks. 2024-02-26 15:17:17 -05:00
Vitor Pamplona
782926fbea Separates the q filter from the e filter to avoid and moves q to the last position since it can be cut off and won't affect the app. 2024-02-25 18:24:50 -05:00
Vitor Pamplona
21d141c5c2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-25 17:53:23 -05:00
Vitor Pamplona
de17aab7ab Fixing stack overflow with more than 200 zaps in a single note. 2024-02-25 17:53:15 -05:00
Vitor Pamplona
d1b0707b54 Merge pull request #783 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-25 17:13:49 -05:00
Crowdin Bot
2ea80272e3 New Crowdin translations by GitHub Action 2024-02-25 22:06:59 +00:00
Vitor Pamplona
6a7d33e58f - Generalizing OTS Calendar access
- Developing OTS access points with OkHttp
2024-02-25 17:04:51 -05:00
Vitor Pamplona
880915e702 Better explains why the fork of javaOTS was needed. 2024-02-25 15:43:59 -05:00
Vitor Pamplona
d6fd407632 Moves external signer from org.json to jackson parser 2024-02-25 15:43:42 -05:00
Vitor Pamplona
1c0854eeaa Moves from org.json to jackson in OTS replies 2024-02-25 15:43:18 -05:00
Vitor Pamplona
9eefb65047 Generalizes the interface for block explorers in OTS 2024-02-25 15:43:01 -05:00
Vitor Pamplona
31c9041461 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-25 11:50:01 -05:00
Vitor Pamplona
c4d6b1a5d1 Adds support for OpenTimestamps in Quartz 2024-02-25 11:49:51 -05:00
Vitor Pamplona
ad03d355b0 Adds internet for testing of OTS attestations 2024-02-25 11:45:59 -05:00
Vitor Pamplona
5ad86e5604 Adds support for the q tag together with the support for the mention marker. 2024-02-25 11:40:39 -05:00
Vitor Pamplona
6afd9a0191 Adds note on why the libscrypt was removed. 2024-02-25 11:39:48 -05:00
Vitor Pamplona
58ff0b5500 Merge pull request #782 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-24 09:18:08 -05:00
Crowdin Bot
3f33871c9c New Crowdin translations by GitHub Action 2024-02-24 14:16:45 +00:00
Vitor Pamplona
ca4cc93b31 Fix Quartz use 2024-02-24 09:15:41 -05:00
Vitor Pamplona
49c2d4f486 Adding Basic Git Rendering support. 2024-02-23 22:09:06 -05:00
Vitor Pamplona
b9c3123f18 Faster Longform tag filters 2024-02-23 18:08:55 -05:00
Vitor Pamplona
452911bf27 Removes the need to specify nulls for new TextNote creations 2024-02-23 18:08:30 -05:00
Vitor Pamplona
173e82d236 Adds Wikipage Kind 2024-02-23 17:55:39 -05:00
Vitor Pamplona
2819c71e85 Updates Coil and Google Service dependencies 2024-02-23 16:59:47 -05:00
Vitor Pamplona
3eac579638 Fixing Format 2024-02-23 14:29:14 -05:00
Vitor Pamplona
d606cef90e Adds Jitpack instructions for Quartz 2024-02-23 14:27:20 -05:00
Vitor Pamplona
de8870f240 Merge pull request #780 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-23 13:45:11 -05:00
Crowdin Bot
13031223a4 New Crowdin translations by GitHub Action 2024-02-23 18:22:53 +00:00
Vitor Pamplona
553500460a Merge pull request #779 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-23 13:21:41 -05:00
Vitor Pamplona
a7e601beee Keeping signing and util classes in the final jar 2024-02-23 13:21:25 -05:00
Crowdin Bot
d21792a46f New Crowdin translations by GitHub Action 2024-02-23 17:17:02 +00:00
Vitor Pamplona
b8b9aaf4aa Adds time utils tests to see if jitpack exports it 2024-02-23 12:15:20 -05:00
Vitor Pamplona
dd160ecb71 Improves relay list NIP-11 caching, updates NIP-11 properties in the document and adds icon rendering from the NIP11 document. 2024-02-22 18:43:46 -05:00
Vitor Pamplona
2ee4b24c9b Moves Relay information to Quartz
Renames Nip19 object for better readability
2024-02-22 16:08:36 -05:00
Vitor Pamplona
606c483d09 Moves Wallet Connect to Quartz 2024-02-22 15:40:07 -05:00
Vitor Pamplona
b1d1c4089c Speeds up some of image calculations 2024-02-22 14:46:18 -05:00
Vitor Pamplona
5740b75a64 Adds a few more benchmarks 2024-02-22 14:05:47 -05:00
Vitor Pamplona
8c9971d9f2 Moves benchmark tests to the benchmark release type 2024-02-22 13:59:59 -05:00
Vitor Pamplona
860c3773c3 Moves emoji parsers to commons and 2024-02-22 13:56:56 -05:00
Vitor Pamplona
8ff4ac3709 Inserts uploaded URLs where the cursor is and not at the end of the file. 2024-02-22 09:54:21 -05:00
Vitor Pamplona
5b77e39c8b Improves the speed of the text parser. 2024-02-21 19:18:30 -05:00
Vitor Pamplona
5886c866d3 Makes the name of the test follow the refactored name 2024-02-21 16:24:06 -05:00
Vitor Pamplona
96f29fc5ca Refactoring some names for the parsers. 2024-02-21 16:22:17 -05:00
Vitor Pamplona
90175198f0 Preparing to move Kind1 content parsers to commons. 2024-02-21 15:25:40 -05:00
Vitor Pamplona
4b9a55e178 Increasing the speed of the Robohash SVG to buffer function 2024-02-21 13:51:27 -05:00
Vitor Pamplona
0429c4dc3a Faster comparisons on GiftWrap Benchmarks 2024-02-21 11:25:08 -05:00
Vitor Pamplona
e3f3350c85 Fixes Robohash test 2024-02-21 11:24:51 -05:00
Vitor Pamplona
5cd1c5ab90 Faster hex validation 2024-02-21 11:23:32 -05:00
Vitor Pamplona
d982f029e4 Fixes Ukrainian translation 2024-02-21 10:22:20 -05:00
Vitor Pamplona
f44bc02767 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-21 10:13:18 -05:00
Vitor Pamplona
2e096069b9 Merge pull request #778 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-21 10:04:53 -05:00
Vitor Pamplona
f10dd38126 refactoring on method names to make them clearer 2024-02-21 10:04:40 -05:00
Crowdin Bot
74b1cad578 New Crowdin translations by GitHub Action 2024-02-21 15:02:57 +00:00
Vitor Pamplona
dbcf59779e Fix benchmark test 2024-02-21 09:59:39 -05:00
Vitor Pamplona
f4cee92702 Fixes the crash on images not being present in the image pager. 2024-02-20 16:51:18 -05:00
Vitor Pamplona
b70407f447 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-20 15:52:32 -05:00
Vitor Pamplona
444857ad95 Updating Vico 2024-02-20 15:52:25 -05:00
Vitor Pamplona
ae291d278f Fixes text's vertical alignment when emoji's are present. 2024-02-20 12:28:46 -05:00
Vitor Pamplona
0d44c44c6e Adds release note ID to gradle. 2024-02-20 11:30:27 -05:00
Vitor Pamplona
a03f9d2b98 Fixes position of the notification dot 2024-02-20 11:30:11 -05:00
Vitor Pamplona
49a090da00 Merge pull request #777 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-20 11:00:50 -05:00
Crowdin Bot
c8d11fdcac New Crowdin translations by GitHub Action 2024-02-20 15:59:43 +00:00
Vitor Pamplona
33342b85d1 Merge pull request #776 from davotoula/update_translations
Updated translations. DE, SV, PT, CS
2024-02-20 10:58:17 -05:00
David Kaspar
a148da4b7b Updated translations. DE, SV, PT, CS 2024-02-20 11:40:16 +00:00
Vitor Pamplona
1b93fc57cb Creates a way to ask for donations in the Notification page. 2024-02-19 18:57:21 -05:00
Vitor Pamplona
2514b74883 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-19 18:55:38 -05:00
Vitor Pamplona
a97ade41fa adds a release drafter to test 2024-02-19 18:55:29 -05:00
Vitor Pamplona
e44ee1e549 Merge pull request #775 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-19 08:31:44 -05:00
Crowdin Bot
b170d04d5e New Crowdin translations by GitHub Action 2024-02-19 13:20:13 +00:00
Vitor Pamplona
dc55d71486 Removing the need to recreate modifiers on BottomBar 2024-02-19 08:17:44 -05:00
Vitor Pamplona
dd071dc65a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-17 13:54:45 -05:00
Vitor Pamplona
dd7427971c refactoring 2024-02-17 13:53:53 -05:00
Vitor Pamplona
d715ea44df Typo 2024-02-17 12:39:48 -05:00
Vitor Pamplona
7af2fcdb27 Adds playstore badge 2024-02-17 11:51:42 -05:00
Vitor Pamplona
879f7cf8e3 Smaller download badges 2024-02-17 11:29:18 -05:00
Vitor Pamplona
88033c2acd Update README.md 2024-02-17 11:16:45 -05:00
Vitor Pamplona
e7fc5545ba Update README.md 2024-02-17 11:15:19 -05:00
Vitor Pamplona
e7240c0acf Using an SVG as readme logo 2024-02-17 11:09:40 -05:00
Vitor Pamplona
1fa8cd95dc Improves Layout of Readme 2024-02-17 11:05:40 -05:00
Vitor Pamplona
9424147a08 v0.84.3 2024-02-16 16:00:00 -05:00
Vitor Pamplona
26356d6060 simple refactoring 2024-02-16 15:51:43 -05:00
Vitor Pamplona
56bc10b513 Speeds up set building 2024-02-16 15:49:57 -05:00
Vitor Pamplona
67a1aae1d0 Avoids the need to tag a cited user to count as a citation. 2024-02-16 15:49:42 -05:00
Vitor Pamplona
b88ab06154 Counts quotes as retweets in the notification stats bar 2024-02-16 15:48:54 -05:00
Vitor Pamplona
7d9784ee44 Updates secp256k1 2024-02-16 15:40:53 -05:00
Vitor Pamplona
8034fd0975 Adds normalizing test for emoji passwords in NIP49 2024-02-16 13:53:06 -05:00
Vitor Pamplona
42cc04c194 updates license notice year 2024-02-15 18:31:26 -05:00
Vitor Pamplona
52c6ce4849 Updates scrypt lib to support empty keys. 2024-02-15 17:42:19 -05:00
Vitor Pamplona
6553f3bec5 Normalizes passwords to NFKC in NIP49 2024-02-15 16:05:53 -05:00
Vitor Pamplona
03a9ec8ed2 Refactors clickable text 2024-02-15 11:03:01 -05:00
Vitor Pamplona
133d7d5ce0 v0.84.2 2024-02-15 11:00:23 -05:00
Vitor Pamplona
215989108b Fixes misalignment of Clickable Texts 2024-02-15 11:00:15 -05:00
Vitor Pamplona
ed7dcf94f8 Fixes extra spaces after urls 2024-02-15 10:59:52 -05:00
Vitor Pamplona
b2c7f150ec Merge pull request #774 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-15 08:46:02 -05:00
Crowdin Bot
c85d74a688 New Crowdin translations by GitHub Action 2024-02-15 13:41:21 +00:00
Vitor Pamplona
cbf23fedf1 Merge pull request #773 from davotoula/update_translations
Update translations
2024-02-15 08:39:53 -05:00
David Kaspar
5ed64bd5b5 Change to ellipsis character 2024-02-15 12:19:12 +00:00
David Kaspar
b79f9c8395 Updated translations. DE, SV, PT, CS 2024-02-15 12:18:19 +00:00
Vitor Pamplona
02da606461 v0.84.1 2024-02-14 17:49:07 -05:00
Vitor Pamplona
7074446cae Removing scrypt native library that doesn't work on Android 2024-02-14 17:48:57 -05:00
Vitor Pamplona
4746b5b18a removes unneeded translation keys 2024-02-14 17:40:50 -05:00
fiatjaf
103a61f35c useless pt-BR translation changes. 2024-02-14 17:29:28 -05:00
Vitor Pamplona
d9c48d4039 v0.84.0 2024-02-14 17:09:10 -05:00
Vitor Pamplona
9479e86b66 Enabled citation via @ on chats 2024-02-14 16:52:25 -05:00
Vitor Pamplona
bb7ec1ee4b Merge pull request #772 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-14 12:59:57 -05:00
Crowdin Bot
7a213545a2 New Crowdin translations by GitHub Action 2024-02-14 17:56:37 +00:00
Vitor Pamplona
48f27f656d Adds NIP49 to login and back up key screens 2024-02-14 12:54:08 -05:00
Vitor Pamplona
47176421e0 Adds copyright sentence. 2024-02-14 09:17:39 -05:00
Vitor Pamplona
9c84a538d6 Removes the NSEC form nip19 2024-02-13 20:49:21 -05:00
Vitor Pamplona
8bc1c2ec68 Removing nip19 test with ncryptosec 2024-02-13 20:44:00 -05:00
Vitor Pamplona
b94c1e2792 Quartz support for NIP-49 encrypted nsecs 2024-02-13 19:51:32 -05:00
Vitor Pamplona
74f0393706 Adds "₿itcoin" to the set of custom hashtags 2024-02-13 12:42:46 -05:00
Vitor Pamplona
0ec4b9b59f Merge pull request #771 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-13 11:58:43 -05:00
Crowdin Bot
1592d55db5 New Crowdin translations by GitHub Action 2024-02-13 16:43:27 +00:00
Vitor Pamplona
629c8b8167 Avoids the memory use of the flatten operation 2024-02-13 11:40:42 -05:00
Vitor Pamplona
9d8f70e04b No need to remember this param. 2024-02-13 11:39:54 -05:00
Vitor Pamplona
f75104e616 Updates Compose dependencies 2024-02-13 11:38:55 -05:00
Vitor Pamplona
c9d59fd5ac Adds a check for the main thread when pulling opengraph tags. 2024-02-13 11:38:28 -05:00
Vitor Pamplona
d41eb4ce31 No need to crossfade when expanding content 2024-02-12 18:11:28 -05:00
Vitor Pamplona
60e87d943a v0.83.13 2024-02-12 16:09:03 -05:00
Vitor Pamplona
1deed59d5b Fixes: https://github.com/vitorpamplona/amethyst/issues/740
Displays error messages when the URI sent to Amethyst is invalid
2024-02-12 16:03:37 -05:00
Vitor Pamplona
bbc4ec2625 Fixes thread rendering when "mention" events are added with mentioning the event. 2024-02-12 15:10:48 -05:00
Vitor Pamplona
5109b65e37 Fixes crash when MuteList has e tags that are not valid hexes 2024-02-12 14:21:32 -05:00
Vitor Pamplona
c0a9ce8e21 Small refactoring to focus the encrypted storage procedures to the application context. 2024-02-12 11:52:18 -05:00
Vitor Pamplona
bdff8c6c0d Moves language translation cleanup to the IO thread since it clears disk files as well 2024-02-12 11:15:59 -05:00
Vitor Pamplona
a3ac8a445d Merge pull request #768 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-10 10:22:09 -05:00
Crowdin Bot
84d4d68116 New Crowdin translations by GitHub Action 2024-02-10 15:00:43 +00:00
Vitor Pamplona
27248bff17 Invert the order of the hidden users in the screen. 2024-02-10 09:59:01 -05:00
Vitor Pamplona
dc9a9f288b Merge pull request #767 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-10 08:40:44 -05:00
Crowdin Bot
88662a4e55 New Crowdin translations by GitHub Action 2024-02-10 13:27:57 +00:00
Vitor Pamplona
9d269f7cba Merge pull request #766 from davotoula/update_translations
Updated translations. DE, SV, PT
2024-02-10 08:26:53 -05:00
David Kaspar
04fe439e0b Updated translations. DE, SV, PT 2024-02-10 09:45:17 +00:00
Vitor Pamplona
72503b9061 Adds community and public chat list events from nip51 2024-02-09 15:54:56 -05:00
Vitor Pamplona
f7ab963db9 Small fixes in the use of modifiers 2024-02-09 14:37:49 -05:00
Vitor Pamplona
f97128e11c Fixing accessibility issues raised on https://github.com/vitorpamplona/amethyst/issues/765 2024-02-08 17:16:54 -05:00
Vitor Pamplona
e2d4107297 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-08 14:54:04 -05:00
Vitor Pamplona
18709a1b22 Disables the notification dot in the discovery tab 2024-02-08 14:53:53 -05:00
Vitor Pamplona
ddfe2abb42 Fixes send to Top in marketplaces. 2024-02-08 14:53:35 -05:00
Vitor Pamplona
0cf82f7edd Merge pull request #764 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-07 16:37:18 -05:00
Crowdin Bot
d1c40efd90 New Crowdin translations by GitHub Action 2024-02-07 21:16:44 +00:00
Vitor Pamplona
68e90ce3ed Unwrapps the reply message if the GiftWrap was tagged and not the inner message id. 2024-02-07 16:14:48 -05:00
Vitor Pamplona
95e19cf681 v0.83.12 2024-02-05 20:29:43 -05:00
Vitor Pamplona
3298fd4493 BugFix for short lists 2024-02-05 20:26:37 -05:00
Vitor Pamplona
248bdffd50 v0.83.11 2024-02-05 19:49:27 -05:00
Vitor Pamplona
cce7a20918 Speeds up parsing of the event content. 2024-02-05 19:47:33 -05:00
Vitor Pamplona
845c99b798 Faster Expandable Text calculator. 2024-02-05 19:37:21 -05:00
Vitor Pamplona
b13b832696 Starting a new module for app benchmarks 2024-02-05 18:18:33 -05:00
Vitor Pamplona
eb6e235f1b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-05 16:52:57 -05:00
Vitor Pamplona
36c49b2171 Updates Zoomable and Zxing Qr Reader 2024-02-05 16:52:48 -05:00
Vitor Pamplona
5cd3096221 Merge pull request #761 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-05 12:43:57 -05:00
Crowdin Bot
5bdc59d0e3 New Crowdin translations by GitHub Action 2024-02-05 17:37:48 +00:00
Vitor Pamplona
be43f2025f v0.83.10 2024-02-05 12:35:42 -05:00
Vitor Pamplona
0bb39f91b9 README typos and formatting 2024-02-02 18:53:36 -05:00
Vitor Pamplona
63a7b44264 Adds crowdin address to the readme 2024-02-02 18:44:10 -05:00
Vitor Pamplona
b5341f67e0 Finally fixes: https://github.com/vitorpamplona/amethyst/issues/436 2024-02-02 18:23:44 -05:00
Vitor Pamplona
3a2045eccc Allows buttons in the New Post screen to scroll horizontally to partially fix https://github.com/vitorpamplona/amethyst/issues/723
We are waiting the default scrollbar implementation from Jetpack Compose
2024-02-02 17:39:48 -05:00
Vitor Pamplona
c78239a6b7 Fixes some connectivity issues by basing the timeout from the last connection tentative instead of the drop out time. 2024-02-02 16:08:36 -05:00
Vitor Pamplona
4be4306540 Merge pull request #759 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-02 10:27:22 -05:00
Crowdin Bot
48887f23c1 New Crowdin translations by GitHub Action 2024-02-02 14:37:01 +00:00
Vitor Pamplona
103809d35b Fixes the top padding of the quoted note in a post. 2024-02-02 09:35:17 -05:00
Vitor Pamplona
9d92976eb9 Optimizes memory use of the visual user and url tagger in new posts. 2024-02-02 09:30:36 -05:00
Vitor Pamplona
d3d19b497b Renames from NIP-29 to 92 2024-02-01 19:20:26 -05:00
Vitor Pamplona
316c32f076 v0.83.9 2024-02-01 18:38:25 -05:00
Vitor Pamplona
6f27201244 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-02-01 18:31:36 -05:00
Vitor Pamplona
e56377f8c3 Adds support for NIP-29 in public messages and new DMs. NIP-54 stays in NIP-54 2024-02-01 18:31:28 -05:00
Vitor Pamplona
44e04864ac Merge pull request #758 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-02-01 16:14:42 -05:00
Crowdin Bot
eea727570c New Crowdin translations by GitHub Action 2024-02-01 20:57:56 +00:00
Vitor Pamplona
54155a3c30 Includes the product in the first message of the marketplace. 2024-02-01 15:56:13 -05:00
Vitor Pamplona
34d373c293 Updates to Android Studio Hedgehog | 2023.1.1 Patch 2 2024-01-31 16:05:00 -05:00
Vitor Pamplona
d7563032c4 Merge pull request #756 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-31 14:37:59 -05:00
Crowdin Bot
ed75489975 New Crowdin translations by GitHub Action 2024-01-31 19:29:03 +00:00
Vitor Pamplona
145b0d8f0a v0.83.8 2024-01-31 14:26:58 -05:00
Vitor Pamplona
eae07e2fe0 Making sure Cancellation of coroutines stops long processes. 2024-01-30 17:33:44 -05:00
Vitor Pamplona
6b60b1434f Name refactoring 2024-01-30 13:52:02 -05:00
Vitor Pamplona
3ff4db264a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-30 13:40:08 -05:00
Vitor Pamplona
21f042fa95 Fixes secondary buttons theme in the login and signup screens. 2024-01-30 13:38:41 -05:00
Vitor Pamplona
e26bbc2e61 Merge pull request #755 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-30 13:16:43 -05:00
Crowdin Bot
5135026e6a New Crowdin translations by GitHub Action 2024-01-30 18:12:06 +00:00
Vitor Pamplona
a72f5ae01f Removing unnecessary logs 2024-01-30 13:09:43 -05:00
Vitor Pamplona
f2ed2d94c4 Update README.md
Fixing Markdown
2024-01-30 12:01:23 -05:00
Vitor Pamplona
75d703ac0a Testing GitStr with a Readme patch 2024-01-30 11:54:01 -05:00
Vitor Pamplona
277bf0a603 Fixes misalignment of some links to users in the middle of the note. 2024-01-30 11:51:02 -05:00
Vitor Pamplona
1ac990b67c Set account to avoid lateinit property account has not been initialized 2024-01-30 11:20:25 -05:00
Vitor Pamplona
e59f676d47 updates secp256k1 2024-01-30 11:00:33 -05:00
Vitor Pamplona
312d82fdf6 moving the order or contributing 2024-01-30 09:47:31 -05:00
Vitor Pamplona
08659738bd Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-30 09:46:06 -05:00
Vitor Pamplona
f1b0958903 Merge pull request #754 from davotoula/fix_gr_locale
Correct GR locale config
2024-01-30 09:45:36 -05:00
Vitor Pamplona
998aa2aed1 Adding GitStr 2024-01-30 09:44:48 -05:00
David Kaspar
370a18f716 Correct GR locale config 2024-01-30 13:30:25 +00:00
Vitor Pamplona
6638defbb9 Fixes content sensitivity for NIP-54 images. 2024-01-29 18:26:25 -05:00
Vitor Pamplona
d88eb014b7 Adding the exception to the Zap error message. 2024-01-29 17:14:31 -05:00
Vitor Pamplona
14e9a91d40 Updates dependencies 2024-01-29 16:28:53 -05:00
Vitor Pamplona
3ed45f282c Fixing NIP number for wraps/seals 2024-01-29 11:19:56 -05:00
Vitor Pamplona
2ff090dac1 New NIP available 2024-01-29 11:03:15 -05:00
Vitor Pamplona
281d86b4bf Small refactoring on the HTTP client 2024-01-27 11:58:39 -05:00
Vitor Pamplona
504e55a93f Merge pull request #753 from greenart7c3/proxy_fix
Recreate http client when changing the proxy or timeout
2024-01-27 11:39:29 -05:00
greenart7c3
c7b2c20119 recreate httpclient when changing the proxy or timeout 2024-01-24 17:18:25 -03:00
greenart7c3
cdd620987b implement reply draft 2024-01-24 09:55:31 -03:00
greenart7c3
2c086f76e2 open the post screen after editing the text 2024-01-24 08:40:55 -03:00
greenart7c3
99965ecd2d add an edit draft in the drawer 2024-01-24 08:22:35 -03:00
greenart7c3
ba7c59fdd5 draft was not saving in some places 2024-01-24 08:22:11 -03:00
greenart7c3
76a93f84c3 fix default value for the draft note 2024-01-24 08:21:04 -03:00
Vitor Pamplona
3e102c064b Merge pull request #750 from greenart7c3/queries
remove the need for application package name in the androidManifest for the external signer
2024-01-23 09:22:58 -05:00
greenart7c3
e0ec8a741c remove the need for application package name in the androidManifest for the external signer 2024-01-22 11:29:10 -03:00
greenart7c3
26a1624399 save a draft while you are typing the post 2024-01-22 09:02:44 -03:00
Vitor Pamplona
f1388b46d5 Merge pull request #746 from greenart7c3/main
add a copy url to the url preview card
2024-01-19 06:07:42 -05:00
greenart7c3
0f24e458d8 add a copy url to the url preview card 2024-01-19 07:18:25 -03:00
Vitor Pamplona
b8f3625954 Fixes remember of the wrong hashtag because it's a different post. 2024-01-18 15:57:21 -03:00
Vitor Pamplona
0babc46f7c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-18 13:36:24 -03:00
Vitor Pamplona
6d368148cc makes the benchmark module profileable 2024-01-18 13:35:53 -03:00
Vitor Pamplona
3241f0ada1 formatting 2024-01-18 13:35:38 -03:00
Vitor Pamplona
49ad972af8 activates nostr.mom and nos.lol in the default relay set for global because of the Video feed. 2024-01-18 13:34:30 -03:00
Vitor Pamplona
e061575454 Removes unused imports 2024-01-18 13:33:48 -03:00
Vitor Pamplona
694642b42b Merge pull request #745 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-17 07:27:51 -05:00
Crowdin Bot
f4684ed078 New Crowdin translations by GitHub Action 2024-01-17 12:24:41 +00:00
Vitor Pamplona
db12ff0532 Merge pull request #744 from greenart7c3/reject_permission_amber
add support for always rejected permissions from external signer
2024-01-17 07:23:13 -05:00
greenart7c3
13d9c1ab10 add support for always rejected permissions from external signer 2024-01-17 09:05:24 -03:00
Vitor Pamplona
ac8f0e8600 v0.83.7 Testing new build tools 2024-01-11 12:33:55 -05:00
Vitor Pamplona
21472daab5 Uses new build tools to sign releases 2024-01-11 12:32:58 -05:00
Vitor Pamplona
c6a00386c8 adds distribution to github actions 2024-01-11 12:23:36 -05:00
Vitor Pamplona
9e3aa31c25 Updates Github actions scripts. 2024-01-11 12:06:45 -05:00
Vitor Pamplona
47987fac0f Merge pull request #743 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-11 10:52:13 -05:00
Crowdin Bot
46c782cf37 New Crowdin translations by GitHub Action 2024-01-11 15:51:24 +00:00
Vitor Pamplona
fd67f4875d Merge pull request #742 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-11 10:50:28 -05:00
Vitor Pamplona
4e2ff591e6 v0.83.6 2024-01-11 10:49:37 -05:00
Crowdin Bot
0e46145e2d New Crowdin translations by GitHub Action 2024-01-11 15:45:38 +00:00
Vitor Pamplona
383d859544 Fixes the BookmarkScreen update after adding and removing a bookmark to the list 2024-01-11 10:43:51 -05:00
Vitor Pamplona
81400eb863 Solving viewModel creation bug for Public Bookmarks 2024-01-11 10:36:52 -05:00
Vitor Pamplona
422446f53e Fixes bug when creating bookmarks for the first time. 2024-01-11 10:09:41 -05:00
Vitor Pamplona
f1e721f323 Fixes bug in checking bookmarks without running the onReady. 2024-01-11 10:09:19 -05:00
Vitor Pamplona
0fc72eb84d updates mockk 2024-01-11 09:43:11 -05:00
Vitor Pamplona
9234f4b7a8 Improves previews 2024-01-09 17:47:34 -05:00
Vitor Pamplona
0198d56099 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-09 16:35:04 -05:00
Vitor Pamplona
8f31c60cd1 Fixing some of the margins in the login screen. 2024-01-09 16:33:50 -05:00
Vitor Pamplona
7cb17954d9 Merge pull request #741 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-01-09 16:26:37 -05:00
Crowdin Bot
baf35b202f New Crowdin translations by GitHub Action 2024-01-09 21:25:47 +00:00
Vitor Pamplona
7b7e3624ac New Signup screen 2024-01-09 16:23:17 -05:00
Vitor Pamplona
d27b9ae5a8 Reducing the size of the following icon 2024-01-06 19:16:15 -05:00
Vitor Pamplona
4c48463091 v0.83.5 2024-01-06 16:53:18 -05:00
Vitor Pamplona
eb08d485e1 Fixing some of the tests due to the line breaks added by spotless 2024-01-06 15:11:12 -05:00
Vitor Pamplona
6e99e6f4a2 Makes sure the relay list contains unique urls. 2024-01-06 14:36:05 -05:00
Vitor Pamplona
d269e9e597 reduces some of the huge lines we have. 2024-01-06 11:59:35 -05:00
Vitor Pamplona
f8e7dd78d9 Reverts to the non-Google kotlin style. 2024-01-06 11:32:41 -05:00
Vitor Pamplona
0be1f89368 Fixes the lack of relay icons in the NIP44 messages 2024-01-06 11:24:52 -05:00
Vitor Pamplona
5c7e9258c1 Fixing some of the minification issues. 2024-01-06 11:13:57 -05:00
Vitor Pamplona
f877b6ff68 Creating the benchmark build type on the modules as well. 2024-01-06 10:53:54 -05:00
Vitor Pamplona
f5f43c5094 Creates a benchmark release type to avoid going back and forth with signing keys on the release 2024-01-06 10:48:11 -05:00
Vitor Pamplona
ec867ae8a2 Spotless fixes. 2024-01-06 10:44:32 -05:00
Vitor Pamplona
de61f7f383 Moves to spotless 2024-01-06 10:44:05 -05:00
Vitor Pamplona
ed6c1e62e4 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-01-05 16:46:30 -05:00
Vitor Pamplona
620b2bfa9f Improving the speed of contains 2024-01-05 16:46:18 -05:00
Vitor Pamplona
6715d48a9d Reverts kotlin version change 2024-01-05 16:45:58 -05:00
Vitor Pamplona
4291a58f85 Updates dependencies 2024-01-05 15:28:46 -05:00
Vitor Pamplona
00cffc8b6b fixing the benchmark for Robohash 2024-01-05 15:19:53 -05:00
Vitor Pamplona
7143d78663 Fixing the giftWrap test cases since the migration to v2 2024-01-05 15:18:46 -05:00
Vitor Pamplona
eaaa9a6446 Adjust position of the participant list in live streams. 2024-01-05 09:54:27 -05:00
Vitor Pamplona
41270c3a98 Merge pull request #739 from vuittont60/main
Fix some typos
2024-01-05 00:42:42 -05:00
vuittont60
a08a052b30 Fix typos 2024-01-05 11:27:52 +08:00
vuittont60
9403f12d2d Fix typos 2024-01-05 11:27:48 +08:00
Vitor Pamplona
89fb83cd9a Initializing the isHidden state for the note correctly 2023-12-29 19:44:48 -05:00
Vitor Pamplona
2355a95202 Avoiding the creation of modifiers. 2023-12-29 19:44:21 -05:00
Vitor Pamplona
53a320b4dc no need to remember the showProfilePicture state 2023-12-29 19:43:14 -05:00
Vitor Pamplona
2de3d19a34 Adds support for displaying video events. 2023-12-29 17:24:50 -05:00
Vitor Pamplona
57430c4366 Refactoring some of the old nomenclature of Kind 1 tags. 2023-12-29 14:25:14 -05:00
Vitor Pamplona
941ed77f22 Merge pull request #738 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-29 10:37:41 -05:00
Crowdin Bot
005b082873 New Crowdin translations by GitHub Action 2023-12-29 15:27:29 +00:00
Vitor Pamplona
9f219e31ab Merge pull request #737 from tyiu/nip-10-backwards-compatibility
Add write support for NIP-10 deprecated positional tags in text notes to maximize backwards compatibility
2023-12-29 10:26:15 -05:00
Terry Yiu
9101fb1e9e Add write support for NIP-10 deprecated positional tags in text notes to maximize backwards compatibility 2023-12-29 01:56:01 -05:00
Vitor Pamplona
c215beeb8e v0.83.4 2023-12-27 17:38:34 -05:00
Vitor Pamplona
20b730318f Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-27 17:00:48 -05:00
Vitor Pamplona
abef3d13f2 Improves the design of the discovery cards for Live Activity and Chats. 2023-12-27 17:00:41 -05:00
Vitor Pamplona
77eb066362 Fixes the loading of reactions and zaps to replaceable events. 2023-12-27 17:00:08 -05:00
Vitor Pamplona
ec4981852b Fixing the zap of live streams to go to the Host and not the pubkey in the event. 2023-12-27 16:59:28 -05:00
Vitor Pamplona
56c2c60c58 reorganizing Event Factory. 2023-12-27 16:58:56 -05:00
Vitor Pamplona
48aa9f950d adds support for NIP-75 events 2023-12-27 16:58:43 -05:00
Vitor Pamplona
37ae06347f Merge pull request #735 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-27 11:49:01 -05:00
Crowdin Bot
78616c07b1 New Crowdin translations by GitHub Action 2023-12-27 16:25:05 +00:00
Vitor Pamplona
73eff8a6c3 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-27 11:23:06 -05:00
Vitor Pamplona
4afb0027fb Initial support for separate vertical and horizontal videos. 2023-12-26 18:02:13 -05:00
Vitor Pamplona
1cedc8a6c8 Fixes reconnecting issue when DataSources were not active and become active later. 2023-12-26 18:01:31 -05:00
Vitor Pamplona
216ee59fae Merge pull request #732 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-26 15:22:19 -05:00
Crowdin Bot
adc4599adf New Crowdin translations by GitHub Action 2023-12-26 19:47:28 +00:00
Vitor Pamplona
8ed960530c Updates dependencies. 2023-12-26 14:43:01 -05:00
Vitor Pamplona
8f13a76870 v0.83.3 2023-12-23 17:45:56 -05:00
Vitor Pamplona
adaab4ced8 v0.83.2 2023-12-23 17:33:47 -05:00
Vitor Pamplona
5a2e149fa0 No need to remember simple state. 2023-12-23 17:28:04 -05:00
Vitor Pamplona
560c1f797c Performance: Reuse of modifiers in ReactionRow and Column 2023-12-23 16:28:02 -05:00
Vitor Pamplona
2d5be92f34 Performance: Improving the use or remember, less jittering when scrolling. 2023-12-23 15:28:04 -05:00
Vitor Pamplona
e9285fdac0 Simplifying the reaction row zap click. 2023-12-23 12:37:10 -05:00
Vitor Pamplona
4f12c127af Adds party, thinking and shock emoji to the default set 2023-12-23 12:04:41 -05:00
Vitor Pamplona
be7522382f Adds a log to debug notification receipts in production. 2023-12-23 11:38:11 -05:00
Vitor Pamplona
987fb02be1 Fixes relay icon size 2023-12-23 11:27:03 -05:00
Vitor Pamplona
bbd8a34f44 Initial refactoring of the elements within a post. 2023-12-22 19:44:01 -05:00
Vitor Pamplona
720ebfd0ea Avoids creating a new byte array at every call of conversation key. 2023-12-22 17:09:36 -05:00
Vitor Pamplona
159228972c Finishing the refactoring of Optout filters 2023-12-22 14:07:37 -05:00
Vitor Pamplona
2a65944eeb Removing unused parameter in this class. 2023-12-22 14:00:47 -05:00
Vitor Pamplona
e7902c4565 Testing new version 2023-12-22 13:49:29 -05:00
Vitor Pamplona
f12588ef7c Merge pull request #730 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-22 11:36:14 -05:00
Crowdin Bot
5fc197a40b New Crowdin translations by GitHub Action 2023-12-22 16:35:48 +00:00
Vitor Pamplona
64608f771e v0.83.0 2023-12-22 11:33:40 -05:00
Vitor Pamplona
a9cebaf3d9 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-22 11:33:13 -05:00
Vitor Pamplona
6c33dae498 Small refactoring before release. 2023-12-22 11:32:54 -05:00
Vitor Pamplona
e42d2dca04 Updates dependencies 2023-12-22 10:25:28 -05:00
Vitor Pamplona
35cd9cb6c5 Starts relay observers with a non null value 2023-12-22 09:15:21 -05:00
Vitor Pamplona
2ce16325a1 Improving the memory use of hashmaps 2023-12-21 15:26:01 -05:00
Vitor Pamplona
275aeb7833 Merge pull request #729 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-21 10:58:23 -05:00
Crowdin Bot
34737be5eb New Crowdin translations by GitHub Action 2023-12-21 15:57:34 +00:00
Vitor Pamplona
6fb5c873cc Removes signingConfig for debug 2023-12-21 10:52:46 -05:00
Vitor Pamplona
e0ffba5295 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-21 08:53:29 -05:00
Vitor Pamplona
82f03e0769 Merge pull request #728 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-20 19:20:00 -05:00
Vitor Pamplona
b223cc4028 updates okhttp 2023-12-20 19:19:58 -05:00
Crowdin Bot
550a9abdbb New Crowdin translations by GitHub Action 2023-12-20 23:09:16 +00:00
Vitor Pamplona
a121d1b301 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-20 18:07:46 -05:00
Vitor Pamplona
e794ff44a1 Moves to NIP-44v2 2023-12-20 18:07:37 -05:00
Vitor Pamplona
a22b6db130 Avoids Concurrent Modification Exception 2023-12-20 14:55:12 -05:00
Vitor Pamplona
1f7fc236db Fixes thread test 2023-12-20 12:47:28 -05:00
Vitor Pamplona
1edaea7905 Fix image uploader test 2023-12-20 11:46:44 -05:00
Vitor Pamplona
e8cae710c7 Merge pull request #726 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-19 16:46:57 -05:00
Crowdin Bot
489d92f9bb New Crowdin translations by GitHub Action 2023-12-19 21:29:59 +00:00
Vitor Pamplona
4d4c568afc Updates dependencies 2023-12-19 15:54:18 -05:00
Vitor Pamplona
e452c33a98 Uses primitive instead of object for Float 2023-12-19 15:54:10 -05:00
Vitor Pamplona
fdfd875580 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-19 15:53:43 -05:00
Vitor Pamplona
a6e8ee20ae Improves the performance of Robohash 2023-12-19 15:46:55 -05:00
Vitor Pamplona
065dba2431 Add a less memory intensive timeAgo calculation 2023-12-19 15:46:36 -05:00
Vitor Pamplona
5f581c2f5e Merge pull request #722 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-17 19:43:23 -05:00
Crowdin Bot
f7bfb68e7c New Crowdin translations by GitHub Action 2023-12-17 23:54:58 +00:00
Vitor Pamplona
2371e0678a Reusing SessionToken for all Playback connections 2023-12-17 18:44:18 -05:00
Vitor Pamplona
97c78aaa3c Moves the creation of the MediaItem to a thread. 2023-12-17 17:26:53 -05:00
Vitor Pamplona
9026c877ac - Adds i18n for error messages when uploading images.
- Forces nip95 to be under 80Kb
- Fixes bug that wouldn't show an error when uploading images in the reels page.
- Only saves default server if not NIP95
2023-12-17 17:26:17 -05:00
Vitor Pamplona
238ade9ccb Removes unused translation phrases. 2023-12-17 14:33:08 -05:00
Vitor Pamplona
016bd1ed81 Does apply the video's dimensions when the user has selected to not automatically start the playback 2023-12-17 14:30:04 -05:00
Vitor Pamplona
ed21d4cafb Detecting images by pinging the server instead of relying on the url. 2023-12-16 11:55:09 -05:00
Vitor Pamplona
ecc0f39f7d Moves relay modifiers to the app's theme 2023-12-16 11:42:11 -05:00
Vitor Pamplona
26846fa91a Refining recompositions of Route. 2023-12-15 22:21:43 -05:00
Vitor Pamplona
fe3a0568f2 Update README.md
Fixing the todo list in readme
2023-12-15 18:46:52 -05:00
Vitor Pamplona
56252d33f1 Updated Readme with the new refactorings from the NIP repo. 2023-12-15 18:38:06 -05:00
Vitor Pamplona
fccfb2aaba Support for NIP-31 alt tags 2023-12-15 18:19:37 -05:00
Vitor Pamplona
7b55b69c12 Adds a k tag to reactions 2023-12-15 18:00:26 -05:00
Vitor Pamplona
23fad2a818 removing some old time measuring code for additional performance. 2023-12-15 15:30:16 -05:00
Vitor Pamplona
2dc23b601a Performance: uses primitive instead of the wrapped object. 2023-12-15 14:58:57 -05:00
Vitor Pamplona
504980d263 Fixes bug with cropped joinToString 2023-12-14 20:17:38 -05:00
Vitor Pamplona
7cf7c9f082 Refactoring 2023-12-14 18:56:53 -05:00
Vitor Pamplona
dd8904de15 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-14 18:45:42 -05:00
Vitor Pamplona
d1c43f8147 Avoiding the creation of new sets when looping through maps in User and Note 2023-12-14 18:42:36 -05:00
Vitor Pamplona
68506fd6af Avoiding the creation of new sets when looping through maps in User and Note 2023-12-14 18:31:00 -05:00
Vitor Pamplona
b795ca06d1 Avoids creating another keySet. 2023-12-14 17:41:50 -05:00
Vitor Pamplona
8695acb742 No need to recreate Sets. 2023-12-14 17:41:25 -05:00
Vitor Pamplona
ed491f5e3a Avoids recreating the EOSE array when changing filters. 2023-12-14 15:52:52 -05:00
Vitor Pamplona
3a76c2a2de Minor refactoring 2023-12-14 15:02:49 -05:00
Vitor Pamplona
91b3b9e0e9 Moves to a less memory intensive way to send filters to the server. 2023-12-14 14:33:15 -05:00
Vitor Pamplona
fb280650a5 Ignore app insights local settings for Android studio 2023-12-14 11:03:33 -05:00
Vitor Pamplona
e4baebf2f9 Must be on main. 2023-12-14 11:02:55 -05:00
Vitor Pamplona
1a33da2537 Moving buildConfig to DSL 2023-12-14 10:45:14 -05:00
Vitor Pamplona
3734ee58ea Disabling log messages 2023-12-14 10:39:16 -05:00
Vitor Pamplona
9554b36f80 Updates secp256k1 lib 2023-12-14 10:38:41 -05:00
Vitor Pamplona
5225c6ad07 v0.82.3 2023-12-13 18:57:16 -05:00
Vitor Pamplona
c7be0e4a95 Memory improvements on relay lists and LiveData objects 2023-12-13 18:51:30 -05:00
Vitor Pamplona
d4f060d509 Improvements to the rendering of chat headers 2023-12-13 18:50:36 -05:00
Vitor Pamplona
9ba9ddee50 Using the new ChatHeader layout 2023-12-13 15:23:46 -05:00
Vitor Pamplona
7bbdd5e877 Avoid remembering Modifier 2023-12-13 15:23:32 -05:00
Vitor Pamplona
6cc65ce84a Avoid remembering Modifier 2023-12-13 15:22:23 -05:00
Vitor Pamplona
6c711f8acc v0.82.2 2023-12-13 12:10:06 -05:00
Vitor Pamplona
acefff80ee - Fixes the transition between short preview and full text on markdown.
- Adds NIP-44 metatags to markdown rendering.
- Adds background video rendering on markdown
- Refactors a few class names.
2023-12-13 12:09:59 -05:00
Vitor Pamplona
0e5b5d657f Calculate the text width of a space outside a composable. 2023-12-12 20:31:03 -05:00
Vitor Pamplona
c2532322fd Fixing the image link classification with uppercase extensions. 2023-12-12 20:29:28 -05:00
Vitor Pamplona
dc73aec334 No need to remember this. 2023-12-12 18:57:14 -05:00
Vitor Pamplona
aa909184fd Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-12 18:09:27 -05:00
Vitor Pamplona
205dd75e7e Update README.md
Aligning screenshots
2023-12-12 18:09:18 -05:00
Vitor Pamplona
be9100dc5e Minor refactoring. 2023-12-12 18:00:02 -05:00
Vitor Pamplona
b2e35cdb3e v0.82.1 2023-12-12 11:20:33 -05:00
Vitor Pamplona
827e168bf2 Fixing minification issue on release. 2023-12-12 11:16:46 -05:00
Vitor Pamplona
a14d4e6f4e v0.82.0 2023-12-12 09:32:59 -05:00
Vitor Pamplona
de61c0350e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-12 09:31:25 -05:00
Vitor Pamplona
3e3c97076d Formatting 2023-12-12 09:31:14 -05:00
Vitor Pamplona
f62c986016 Improving sizes of blurhashes 2023-12-12 08:50:11 -05:00
Vitor Pamplona
04108e893d Merge pull request #720 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-12 08:44:58 -05:00
Crowdin Bot
042394796a New Crowdin translations by GitHub Action 2023-12-12 13:44:16 +00:00
Vitor Pamplona
bfce57f686 Improving the video switching from blurhash to video 2023-12-11 19:58:29 -05:00
Vitor Pamplona
5bb983ba5a Adding blurhash when images are not supposed to be downloaded. 2023-12-11 17:57:19 -05:00
Vitor Pamplona
debae02421 Improves the rendering of inline metadata 2023-12-11 16:22:01 -05:00
Vitor Pamplona
6fe93fdf33 Reverting image comment 2023-12-11 14:26:28 -05:00
Vitor Pamplona
3d225423ef Adds support for nip54 2023-12-11 12:51:53 -05:00
Vitor Pamplona
49e1da8812 Spacing 2023-12-11 12:50:44 -05:00
Vitor Pamplona
04addc4cba Adds support for the FileServers kind 2023-12-10 21:27:47 -05:00
Vitor Pamplona
c5cf2c5442 Adding the delete option for NIP-96 2023-12-10 21:21:00 -05:00
Vitor Pamplona
ddb7e6b7f2 Updates JNA 2023-12-10 18:32:47 -05:00
Vitor Pamplona
e5af34efca Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-08 22:08:30 -05:00
Vitor Pamplona
b3bdbbed98 - Migrates the old imageservers to NIP-96 for the main feed, chats, private messages, stories and user metadata uploads.
- Fixes hash calculation of the entire payload.
- Unifies uploads into NIP-94 images.
2023-12-08 22:08:19 -05:00
Vitor Pamplona
656b066d3a Merge pull request #719 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-08 19:50:21 -05:00
Crowdin Bot
4248fadc69 New Crowdin translations by GitHub Action 2023-12-09 00:45:17 +00:00
Vitor Pamplona
18281cbc49 Merge pull request #718 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-08 19:44:05 -05:00
Crowdin Bot
1d67af328a New Crowdin translations by GitHub Action 2023-12-08 16:23:27 +00:00
Vitor Pamplona
72b4f6acef Merge pull request #715 from davotoula/tr
Correct spelling of localised string reference
2023-12-08 11:21:52 -05:00
Vitor Pamplona
26273270ba Merge branch 'main' into tr 2023-12-08 11:21:46 -05:00
Vitor Pamplona
db3824c3c7 New Cashu Redeeming card. 2023-12-06 18:43:54 -05:00
Vitor Pamplona
d6de1d03d8 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-06 15:40:05 -05:00
Vitor Pamplona
8e4b7c66b5 Adds a UserAgent to all HTTP requests. 2023-12-06 15:40:01 -05:00
Vitor Pamplona
45c750069a Refactoring the update of channel information. 2023-12-06 15:38:31 -05:00
Vitor Pamplona
a411a72024 Merge pull request #716 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-06 14:59:12 -05:00
Crowdin Bot
219d163680 New Crowdin translations by GitHub Action 2023-12-06 19:38:31 +00:00
Vitor Pamplona
a4c9f42205 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-06 14:36:21 -05:00
Vitor Pamplona
a8936c54d8 Adds a Marketplace tab to Discovery 2023-12-06 13:40:54 -05:00
David Kaspar
74875bb564 Correct spelling of localised string reference 2023-12-06 15:07:52 +01:00
Vitor Pamplona
61d412d941 Merge pull request #714 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-06 08:14:16 -05:00
Crowdin Bot
5bb1e47edf New Crowdin translations by GitHub Action 2023-12-06 13:13:27 +00:00
Vitor Pamplona
b023614b5f Merge pull request #713 from davotoula/tr
Updated translations for cs/de/sv/pt
2023-12-06 08:11:39 -05:00
David Kaspar
7245718003 Updated translations for cs/de/sv/pt 2023-12-06 12:15:02 +01:00
David Kaspar
ba1f98d930 Updated translations for cs/de/sv/pt 2023-12-06 12:08:20 +01:00
David Kaspar
ceeabffcb7 Correct spelling of localised string reference 2023-12-06 12:04:50 +01:00
David Kaspar
2a943493b3 Updated translations for cs/de/sv/pt 2023-12-06 11:21:41 +01:00
Vitor Pamplona
332a2f41b6 Moves the relay observer and reconnection to Service Manager's scope. 2023-12-05 21:48:43 -05:00
Vitor Pamplona
ce2cfb7c86 fixes bug when updating relay list that used keep the previous list. 2023-12-05 21:48:05 -05:00
Vitor Pamplona
41ef1abf0e Fixes deletion of replaceable events. 2023-12-05 21:47:20 -05:00
Vitor Pamplona
fce27c3c55 Presenting better error messages when the image upload fails. 2023-12-05 21:46:35 -05:00
Vitor Pamplona
6662966ab3 Fixing crash when image is an SVG and tries to compress. 2023-12-05 21:45:56 -05:00
Vitor Pamplona
f458f00edc 1. Adds Classified creation from Amethyst.
2. Adds relay information for Replaceable events.
2023-12-05 21:44:49 -05:00
Vitor Pamplona
8f1fbe10e9 Merge pull request #712 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-05 11:41:45 -05:00
Crowdin Bot
69efa78fbc New Crowdin translations by GitHub Action 2023-12-05 16:16:54 +00:00
Vitor Pamplona
64a07d8760 Updates to fix Crash due to malformed video formats. 2023-12-05 09:52:15 -05:00
Vitor Pamplona
cb672d92d0 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-04 13:25:54 -05:00
Vitor Pamplona
95bcbfdded Reusing common icons for Cashu's Redemption 2023-12-04 13:23:19 -05:00
Vitor Pamplona
ac269727bc Merge pull request #711 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-04 11:57:45 -05:00
Crowdin Bot
b1a7d18c0a New Crowdin translations by GitHub Action 2023-12-04 15:53:21 +00:00
Vitor Pamplona
619b59357b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-04 10:52:23 -05:00
Vitor Pamplona
86dcc3ccd9 Merge pull request #710 from believethehype/main
add a button to cashu preview to redeem on external wallet
2023-12-04 10:51:41 -05:00
Vitor Pamplona
6772360ae7 Fixes bottom bar appearing in chats when the keyboard is open. 2023-12-04 10:48:29 -05:00
Vitor Pamplona
05cbbd79cb Uses nostr.wine instead of filter.nostr.wine as a search relay. 2023-12-04 10:47:38 -05:00
Believethehype
a80c03a2d2 Update CashuRedeem.kt 2023-12-04 16:20:40 +01:00
Believethehype
6c3abe17ed add a button to cashu preview to redeem on external wallet, additional to melting to ln 2023-12-03 15:02:22 +01:00
Vitor Pamplona
7cd09261de Optimizes the rendering of the side bar. 2023-12-02 17:23:15 -05:00
Vitor Pamplona
6ef1bee8a0 Updates EOSE in the same thread of the new event to reduce the amount of coroutine launches. 2023-12-02 14:54:34 -05:00
Vitor Pamplona
66b8b04db3 Updates dependencies. 2023-12-02 14:14:15 -05:00
Vitor Pamplona
3b06abb2bc Upgrades junit benchmark 2023-12-02 14:12:10 -05:00
Vitor Pamplona
138fafa204 Updates gradle 2023-12-02 14:11:19 -05:00
Vitor Pamplona
9cbdbb8592 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-12-02 13:56:53 -05:00
Vitor Pamplona
1c0ec93cbb Testing the use of just one HttpClient for the entire app. 2023-12-02 13:55:55 -05:00
Vitor Pamplona
c6fa2b5f80 Merge pull request #708 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-02 13:46:20 -05:00
Crowdin Bot
dd55ad18e3 New Crowdin translations by GitHub Action 2023-12-02 18:39:06 +00:00
Vitor Pamplona
284da365a1 Merge pull request #707 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-02 13:37:59 -05:00
Crowdin Bot
7a06c2e9c4 New Crowdin translations by GitHub Action 2023-12-02 16:21:46 +00:00
Vitor Pamplona
61449774a1 Improves the Redeeming feedback
Improves fee calculation from the mint
2023-12-02 11:18:01 -05:00
Vitor Pamplona
81a3baa41c Merge pull request #706 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-12-01 09:35:18 -05:00
Crowdin Bot
4b7e58dd1c New Crowdin translations by GitHub Action 2023-12-01 14:06:41 +00:00
Vitor Pamplona
14ce42401b Merge pull request #705 from greenart7c3/main
fix zap splits when using amber with intents
2023-12-01 09:05:26 -05:00
greenart7c3
ecb8344287 fix zap splits when using amber with intents 2023-12-01 10:52:15 -03:00
Vitor Pamplona
9bc5549d76 v0.81.5 2023-11-30 17:28:58 -05:00
Vitor Pamplona
8ac8894ccd Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-11-30 17:27:03 -05:00
Vitor Pamplona
efb9814f1b Fixes blocked lists on Videos. 2023-11-30 17:18:41 -05:00
Vitor Pamplona
3582f587b3 Merge pull request #704 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-30 17:05:19 -05:00
Crowdin Bot
4fbc9f6d53 New Crowdin translations by GitHub Action 2023-11-30 21:26:37 +00:00
Vitor Pamplona
e9ec1c45a0 v0.81.4 2023-11-30 16:25:07 -05:00
Vitor Pamplona
b2b7cba352 Loads basic information for all the accounts in the app. 2023-11-30 16:24:53 -05:00
Vitor Pamplona
393fbd0359 clean up 2023-11-30 15:17:21 -05:00
Vitor Pamplona
47ae42e3fe Avoiding bug when creating accounts in the background and trying to create the live set 2023-11-30 15:16:21 -05:00
Vitor Pamplona
5db3ede09e Makes Client thread safe. Forcing the disconnect of an old relay list before connecting to a new one. 2023-11-30 15:03:58 -05:00
Vitor Pamplona
eca420d789 Fixing the scope in HomeDataSource. 2023-11-30 15:02:16 -05:00
Vitor Pamplona
5347373cde Avoids sending filters with empty lists. 2023-11-30 14:53:59 -05:00
Vitor Pamplona
05596cb9bf Forces websocket closure onFailure 2023-11-30 14:51:55 -05:00
Vitor Pamplona
3d1310295b No need to call two launch functions 2023-11-30 14:51:13 -05:00
Vitor Pamplona
35c780f81b Avoids bug on creating the live set in the IO thread. 2023-11-30 14:50:24 -05:00
Vitor Pamplona
b021920eaa Emitting an empty list if it cannot decrypt it 2023-11-30 11:22:42 -05:00
Vitor Pamplona
2b27ac3aec Cache zap calculations in notification cards. 2023-11-30 11:22:06 -05:00
Vitor Pamplona
c692c9125b Fixes null list names showing before the list is loaded 2023-11-30 10:33:48 -05:00
Vitor Pamplona
7322fd7aae Fixes hidden buttons in the Chat creation screen. 2023-11-30 10:30:12 -05:00
Vitor Pamplona
07a92cd70d Fixes all or nothing decryption of Zaps failing in a few cases. Now it has a fallback when decryption fails. 2023-11-30 10:15:36 -05:00
Vitor Pamplona
21a797be9d v0.81.3 2023-11-29 17:34:15 -05:00
Vitor Pamplona
fe49cbabce Improves the rendering of LN Invoice Previews. 2023-11-29 17:22:04 -05:00
Vitor Pamplona
9f674a0088 Adds flexibility on timeouts between WIFI and mobile connections. 2023-11-29 17:21:40 -05:00
Vitor Pamplona
b44e94cb16 Adds a test case for urls ending with a period. 2023-11-29 16:50:36 -05:00
Vitor Pamplona
a8da645be3 Increasing timeout to account for slower connections. 2023-11-29 16:50:11 -05:00
Vitor Pamplona
c6e6bf6032 Loads the user profile before building the live set. 2023-11-29 16:14:02 -05:00
Vitor Pamplona
7596f2a43e v0.81.2 2023-11-29 15:13:19 -05:00
Vitor Pamplona
6b7bb209d4 Refinements to ShopStr's event rendering. 2023-11-29 14:56:42 -05:00
Vitor Pamplona
fcdb863c2b Merge pull request #701 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-29 10:54:19 -05:00
Crowdin Bot
4e5a9f7366 New Crowdin translations by GitHub Action 2023-11-29 15:52:07 +00:00
Vitor Pamplona
758f3636f3 Merge pull request #700 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-29 10:50:54 -05:00
Vitor Pamplona
7dafb8317a version 0.81.1 2023-11-29 10:50:22 -05:00
Crowdin Bot
21a5827719 New Crowdin translations by GitHub Action 2023-11-29 15:40:53 +00:00
Vitor Pamplona
b9486eb3e6 Adds back button to the top of the Nav bar of the Thread view. 2023-11-29 10:38:31 -05:00
Vitor Pamplona
dbbfaae02c Fixes video release coroutine being killed by Android 2023-11-29 10:17:57 -05:00
Vitor Pamplona
44b25b7c7c Refines the EOSE strategy to make sure relays that have not replied have a chance to 2023-11-29 10:06:28 -05:00
Vitor Pamplona
0dabf3f2d3 Scopes the service manager into the AccountState ViewModel 2023-11-29 09:14:04 -05:00
Vitor Pamplona
a85ea70e4f Avoids requesting an empty list of chats 2023-11-28 22:23:34 -05:00
Vitor Pamplona
a02519b021 Forces close to happen in an IO thread 2023-11-28 22:22:28 -05:00
Vitor Pamplona
04e70a7886 Make the afterEOSE logic subscription dependent. 2023-11-28 22:22:08 -05:00
Vitor Pamplona
3bfaef2b45 Using the entire event id to filter threads. 2023-11-28 22:17:24 -05:00
Vitor Pamplona
e992e72f4d Making sure the service manager stops the relay subscriptions before disconnecting. 2023-11-28 22:17:01 -05:00
Vitor Pamplona
1c545ee33a Forces the closure. The method close wasn't closing all connections. 2023-11-28 18:00:44 -05:00
Vitor Pamplona
ab93a39ad6 Avoids race conditions when the data source is supposed to have stopped 2023-11-28 18:00:22 -05:00
Vitor Pamplona
7f25e1c434 Limits Notification downloads to 1 week. 2023-11-28 17:59:28 -05:00
Vitor Pamplona
67b7bd3258 Protection against NPEs 2023-11-28 16:36:27 -05:00
Vitor Pamplona
e2aaa8efe0 Migrates from PAY to NOTIFY as per https://github.com/nostr-protocol/nips/pull/901 2023-11-28 16:07:24 -05:00
Vitor Pamplona
5356d869fd Reduces the amount of wasted memory by using Array<Array<List>> as opposed to List<List<String>> for tags. 2023-11-28 15:00:41 -05:00
Vitor Pamplona
d87542a949 Reduces JC usage by creating arrays with the exact amount of bytes when deserializing events. 2023-11-28 09:37:00 -05:00
Vitor Pamplona
98d29ac6dc v0.81.0 2023-11-28 09:05:25 -05:00
Vitor Pamplona
532048c35e Merge pull request #699 from greenart7c3/main
fix crash tryng to decrypt/parse gossip
2023-11-27 12:49:15 -05:00
greenart7c3
386960dc62 fix crash tryng to decrypt/parse gossip 2023-11-27 14:28:15 -03:00
Vitor Pamplona
ae3cc5da2a Fixing benchmark tests after the refactoring. 2023-11-26 21:10:15 -05:00
Vitor Pamplona
34c2ba4922 Typo 2023-11-26 18:30:29 -05:00
Vitor Pamplona
074a7d41fd Adding payment requests from nostr.wine 2023-11-26 18:00:08 -05:00
Vitor Pamplona
7d4fbc3e04 Reducing the precision to load images. 2023-11-26 17:59:10 -05:00
Vitor Pamplona
a2d38381b1 Runs the translation as a UI Scope instead of ViewModel's 2023-11-26 15:02:10 -05:00
Vitor Pamplona
19fb129f7c Fixing test cases after the mega refactoring 2023-11-26 14:59:00 -05:00
Vitor Pamplona
5bfc53136b updates jackson. 2023-11-25 19:49:51 -05:00
Vitor Pamplona
1677606b15 updating via AGP 2023-11-25 19:46:49 -05:00
Vitor Pamplona
8c10173b68 Merge pull request #698 from greenart7c3/main
fix wrong pubkey in encrypt/decrypt nip44 with external signer
2023-11-25 16:31:42 -05:00
greenart7c3
285ae056a2 fix wrong pubkey in encrypt and decrypt nip44 external signer 2023-11-25 14:28:48 -03:00
Vitor Pamplona
8cdd3be085 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-11-24 12:28:01 -05:00
Vitor Pamplona
8f586780a1 Slightly improving performance 2023-11-24 12:27:52 -05:00
Vitor Pamplona
dc3c730e3b adding some labels to crossfades 2023-11-24 11:54:35 -05:00
Vitor Pamplona
df823102da Typo 2023-11-24 11:53:51 -05:00
Vitor Pamplona
1ab857d72b Merge pull request #697 from greenart7c3/main
support for sending/receiving multiple events from external signer
2023-11-24 10:41:06 -05:00
Vitor Pamplona
78863b26d0 Merge pull request #696 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-24 10:40:16 -05:00
Crowdin Bot
0f74bc7b74 New Crowdin translations by GitHub Action 2023-11-24 15:37:59 +00:00
Vitor Pamplona
e7893cacd5 Updating coil 2023-11-24 10:35:02 -05:00
greenart7c3
8a18781048 support for sending/receiving multiple events from external signer 2023-11-24 12:33:10 -03:00
Vitor Pamplona
ad52855f5a Merge pull request #695 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-23 21:59:47 -05:00
Crowdin Bot
50fbde17df New Crowdin translations by GitHub Action 2023-11-24 02:29:22 +00:00
Vitor Pamplona
c6d47ca9c6 Merge pull request #694 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-23 21:26:55 -05:00
Crowdin Bot
072b14bdda New Crowdin translations by GitHub Action 2023-11-23 15:49:32 +00:00
Vitor Pamplona
70964e680a Migrates to MuteList 10000 2023-11-23 10:46:56 -05:00
Vitor Pamplona
954330064d Trying to save from crashes on the signer 2023-11-22 17:04:57 -05:00
Vitor Pamplona
3666a0b980 Fixes race conditions when opening videos. 2023-11-22 16:29:05 -05:00
Vitor Pamplona
61f52ef62e Trimming memory onStop 2023-11-22 16:10:55 -05:00
Vitor Pamplona
74d66f2035 Fixes logging for new events. 2023-11-22 14:39:01 -05:00
Vitor Pamplona
0de4cd5c18 Adding a geohash mipmap to events. 2023-11-22 12:48:19 -05:00
Vitor Pamplona
d1bef11f88 Fixes spacing when drawing POW and Geolocation at the same time. 2023-11-22 12:37:02 -05:00
Vitor Pamplona
3253b8528f Fix URL Preview card when websites blend OG specs 2023-11-22 12:15:09 -05:00
Vitor Pamplona
81661c003a Fixes the writting of nostr addresses in the text with additional words connected to them without a space. It adds a space if the first char is in the bech alphabet. 2023-11-22 11:40:12 -05:00
Vitor Pamplona
fbe395f071 Adds logs for lifetime events. 2023-11-22 11:00:52 -05:00
Vitor Pamplona
d5cf64f048 Removes log message 2023-11-22 09:54:36 -05:00
Vitor Pamplona
7212d566c9 Fixes the blue notification dot appearing when the user receives a notification from a blocked account before downloading the blocklist 2023-11-22 09:10:15 -05:00
Vitor Pamplona
17490e948c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-11-22 08:44:53 -05:00
Vitor Pamplona
b392ea0dca Merge pull request #691 from greenart7c3/test_external_signer
Fixes/improvements to external signer
2023-11-22 08:44:46 -05:00
greenart7c3
9fc0c37845 fix relay reconnecting when returning from external signer 2023-11-22 08:33:41 -03:00
greenart7c3
6db031b208 fix sending zaps using amber, send some default permissions on login, save the signer package name 2023-11-22 08:33:41 -03:00
Vitor Pamplona
7246a545af Makes sure it doesn't crash on starting. 2023-11-21 14:17:32 -05:00
Vitor Pamplona
c8365439ee Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-11-21 13:29:51 -05:00
Vitor Pamplona
003d6bf018 Updating dependencies. 2023-11-21 13:29:48 -05:00
Vitor Pamplona
060286c292 Merge pull request #692 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-21 10:30:16 -05:00
Crowdin Bot
270935a30a New Crowdin translations by GitHub Action 2023-11-21 15:14:23 +00:00
Vitor Pamplona
05e9a1ac3f Fixes issue with cache of disappearing User Zaps in Notifications 2023-11-20 19:03:34 -05:00
Vitor Pamplona
9acad8b3f5 Narrowing the redownloads of reactions down. 2023-11-20 18:58:25 -05:00
Vitor Pamplona
5a9d6929e5 Make local image behave as remove images. 2023-11-20 15:52:18 -05:00
Vitor Pamplona
e45f02c110 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-11-19 17:58:27 -05:00
Vitor Pamplona
4f1ae72ab5 Merge pull request #690 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-19 17:47:22 -05:00
Vitor Pamplona
d4ed6661dd Fixes single click login screen 2023-11-19 17:45:00 -05:00
Crowdin Bot
2976349114 New Crowdin translations by GitHub Action 2023-11-19 22:31:15 +00:00
Vitor Pamplona
b1cfa2219d Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-11-19 17:29:20 -05:00
Vitor Pamplona
df9b764c1d Massive refactoring to unify internal signer and the Amber signer. 2023-11-19 17:28:17 -05:00
Vitor Pamplona
69941002df Adds JUnitRunner after upstream changed packages. 2023-11-18 15:26:48 -05:00
Vitor Pamplona
afc6a91c6b Merge pull request #685 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-10 09:12:56 -05:00
Crowdin Bot
c8b9858e4d New Crowdin translations by GitHub Action 2023-11-10 14:07:24 +00:00
Vitor Pamplona
c4ea6f3619 Merge pull request #686 from vicariousdrama/20231110-unabbrev-amounts-up-to-4-digit
Display amounts up to 4 digits without abbreviation
2023-11-10 09:05:43 -05:00
vicariousdrama
872d70c175 Allow amounts up to 4 digits without abbreviation 2023-11-10 07:21:06 +00:00
Vitor Pamplona
5ad6a2c70b Moves service manager to an Activity scoped class. 2023-11-09 15:25:59 -05:00
Vitor Pamplona
408b6793c0 Moves image cache to the Application class 2023-11-09 15:25:27 -05:00
Vitor Pamplona
43d03bc109 Caching total Zaps 2023-11-09 14:26:11 -05:00
Vitor Pamplona
9377c902a1 Moves creation of the LiveData set to a Synchronized block to avoid double creations without destruction. 2023-11-09 14:18:54 -05:00
Vitor Pamplona
57d97447f4 Fix error message when parsing LnInvoice 2023-11-09 14:07:50 -05:00
Vitor Pamplona
5493ce0dcf Updating dependencies 2023-11-09 12:02:28 -05:00
Vitor Pamplona
499149b7d9 Speeds up LNInvoice parsing 2023-11-09 10:56:37 -05:00
Vitor Pamplona
3fd0c61c0e Updating to support all crowdin languages 2023-11-08 17:08:23 -05:00
Vitor Pamplona
1d62d8ed73 v0.80.7 2023-11-08 14:41:07 -05:00
Vitor Pamplona
2c3a7daa65 Fixes bug when the app calls this method directly, bypassing the other checks in AccountViewModel 2023-11-08 14:32:54 -05:00
Vitor Pamplona
1771b6e667 v0.80.6 2023-11-07 17:39:18 -05:00
Vitor Pamplona
853dcb2127 minimizes race condition when pausing and restarting relay connections 2023-11-07 17:38:59 -05:00
Vitor Pamplona
b25f346370 v0.80.5 2023-11-07 16:44:49 -05:00
Vitor Pamplona
edd13fbe5b Uses the indexing operator 2023-11-07 16:31:36 -05:00
Vitor Pamplona
92c72e5a7b Merge pull request #669 from VictorieeMan/patch-1
changing externa sharing service for text notes to njump.me
2023-11-07 10:33:24 -05:00
Vitor Pamplona
e8e676afd5 Merge pull request #683 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-07 09:31:23 -05:00
Crowdin Bot
bb16ee7f0f New Crowdin translations by GitHub Action 2023-11-07 14:25:33 +00:00
Vitor Pamplona
4eb1c101b9 Merge pull request #682 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-07 09:23:49 -05:00
Crowdin Bot
eb67b6e6cf New Crowdin translations by GitHub Action 2023-11-07 02:19:53 +00:00
Vitor Pamplona
29541bd1e3 Merge pull request #681 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-06 21:18:43 -05:00
Crowdin Bot
08f94e9199 New Crowdin translations by GitHub Action 2023-11-07 02:18:14 +00:00
Vitor Pamplona
a7c7773a81 Merge pull request #680 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-11-06 21:16:55 -05:00
Crowdin Bot
75f2d5710a New Crowdin translations by GitHub Action 2023-11-07 02:15:40 +00:00
Vitor Pamplona
2bff9fe891 Merge pull request #679 from vitorpamplona/l10n_crowdin_translations
New Greek Translations
2023-11-06 21:14:07 -05:00
Vitor Pamplona
4d2c405aea Removes unnecessary methods 2023-11-06 20:45:53 -05:00
Crowdin Bot
ca24afd23e New Crowdin translations by GitHub Action 2023-11-07 01:15:03 +00:00
Vitor Pamplona
47e124c7fc Correctly highliting a notification when dragged 2023-11-06 20:13:00 -05:00
Vitor Pamplona
885aded701 Merge pull request #678 from vitorpamplona/l10n_crowdin_translations
New Spanish Translations
2023-11-06 16:38:32 -05:00
Crowdin Bot
e0729b7236 New Crowdin translations by GitHub Action 2023-11-06 21:29:19 +00:00
Vitor Pamplona
a058b75ee4 Refining the padding of chat messages and reaction row 2023-11-06 16:26:38 -05:00
Vitor Pamplona
0a5e84fd3c Refining layout of URL Previews 2023-11-05 11:27:30 -05:00
Vitor Pamplona
6fa36255b2 Refining the layout of Author Pictures 2023-11-05 11:27:15 -05:00
Vitor Pamplona
3de52a6c0d Simplifying Compose stack 2023-11-04 16:48:29 -04:00
Vitor Pamplona
6d17c8d79f Fixing translations of the same term but with different case. 2023-11-04 11:29:47 -04:00
Vitor Pamplona
26c4677b75 Forces reconnection when a new service is available. 2023-11-04 11:23:37 -04:00
Vitor Pamplona
c5882a9f05 Migrates the check if stream is online to a single compose object. 2023-11-04 11:23:18 -04:00
Vitor Pamplona
f99dc471a1 Merge pull request #676 from vitorpamplona/l10n_crowdin_translations
Updated Translations for Arabic, Greek, Spanish, Indonesian and Vietnamese
2023-11-03 17:25:13 -04:00
Crowdin Bot
25e31b00d2 New Crowdin translations by GitHub Action 2023-11-03 21:16:29 +00:00
Vitor Pamplona
c1a59a1d21 Improve position of video controlling buttons when top bars are present. 2023-11-03 17:14:41 -04:00
Vitor Pamplona
e0d2a71b47 Remove a compose recomposition between the started state and the isOnline state that is already cached. 2023-11-03 17:14:10 -04:00
Vitor Pamplona
ef90b82396 Updates Kotlin compiler version 2023-11-03 17:11:16 -04:00
Vitor Pamplona
9ec6e6d955 v0.80.4 2023-10-30 17:31:50 -04:00
Vitor Pamplona
6b7ec2caa9 Starting videos from Main, but in a thread. 2023-10-30 17:31:33 -04:00
VictorieeMan
1f28d3f32f changing externa sharing service for text notes to njump.me
Changed "https://snort.social/e/" to "https://njump.me/"

Benefits of this change are the link previews that njump.me generates for shared content. The snort social links are just the url root and a long nevent, making different shares difficult to tell apart, and a bit scary to click on for some.

More benefits of njump.me listed on https://njump.me/, hosted by fiatjaf. The included nostr onboarding paired with client agnosticism is also beneficial.
2023-10-28 12:12:31 +02:00
1326 changed files with 151760 additions and 73941 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
root = true
[*]
charset = utf-8
trim_trailing_whitespace = true
[*.{kt,kts}]
max_line_length = 5000
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test
[*.gradle]
indent_size = 4

View File

@@ -12,15 +12,17 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v2
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 17
- name: Cache gradle
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
@@ -28,7 +30,7 @@ jobs:
${{ runner.os }}-gradle-
- name: Linter (gradle)
run: ./gradlew ktlintCheck
run: ./gradlew spotlessCheck
- name: Test (gradle)
run: ./gradlew test --no-daemon
@@ -44,11 +46,11 @@ jobs:
uses: actions/upload-artifact@v2
with:
name: Debug APK
path: app/build/outputs/apk/debug/app-debug.apk
path: amethyst/build/outputs/apk/debug/app-debug.apk
- name: Upload Test Results
uses: actions/upload-artifact@v2
with:
name: Build Reports
path: app/build/reports
path: amethyst/build/reports

View File

@@ -10,15 +10,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 17
- name: Cache gradle
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
@@ -31,20 +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"
- 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"
- name: Build APK
run: ./gradlew assembleRelease --stacktrace
@@ -52,20 +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"
- 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"
- name: Create Release
id: create_release
@@ -86,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
@@ -97,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
@@ -108,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
@@ -119,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
@@ -130,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
@@ -142,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
@@ -153,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
@@ -164,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
@@ -175,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
@@ -186,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
@@ -200,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
@@ -212,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
asset_content_type: application/zip

View File

@@ -10,8 +10,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v4
- name: crowdin action
uses: crowdin/github-action@v1

7
.gitignore vendored
View File

@@ -7,6 +7,12 @@
/.idea/assetWizardSettings.xml
/.idea/androidTestResultsUserPreferences.xml
/.idea/deploymentTargetDropDown.xml
/.idea/deploymentTargetSelector.xml
/.idea/appInsightsSettings.xml
/.idea/ktlint-plugin.xml
/.idea/ktfmt.xml
/.idea/studiobot.xml
/.idea/other.xml
.DS_Store
/build
/captures
@@ -36,6 +42,7 @@ out/
.gradle
.gradle/
build/
.kotlin/
# Local configuration file (sdk path, etc)
local.properties

View File

@@ -3,30 +3,39 @@
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" 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" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" 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" />
</inspection_tool>
</profile>
</component>

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
<option name="version" value="2.0.0" />
</component>
</project>

283
README.md
View File

@@ -1,139 +1,188 @@
# Amethyst: Nostr client for Android
<div align="center">
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px"/>
<a href="https://amethyst.social">
<img src="./docs/design/3rd%20Logo%20-%20Zitron/amethyst.svg" alt="Amethyst Logo" title="Amethyst logo" width="80"/>
</a>
Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.
# Amethyst
## Nostr Client for Android
Join the social network you control.
[![GitHub downloads](https://img.shields.io/github/downloads/vitorpamplona/amethyst/total?label=Downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/vitorpamplona/amethyst/releases)
[![PlayStore downloads](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dcom.vitorpamplona.amethyst%26gl%3DUS%26hl%3Den%26l%3DPlayStore%26m%3D%24shortinstalls)](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
[![Last Version](https://img.shields.io/github/release/vitorpamplona/amethyst.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/vitorpamplona/amethyst)
[![JitPack version](https://jitpack.io/v/vitorpamplona/amethyst.svg)](https://jitpack.io/#vitorpamplona/amethyst)
[![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE)
## Download and Install
[<img src="./docs/design/obtainium.png"
alt="Get it on Obtaininum"
height="80">](https://github.com/ImranR98/Obtainium)
height="70">](https://github.com/ImranR98/Obtainium)
[<img src="https://github.com/machiav3lli/oandbackupx/raw/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub"
height="80">](https://github.com/vitorpamplona/amethyst/releases)
height="70">](https://github.com/vitorpamplona/amethyst/releases)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
height="70">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
# Current Features
</div>
- [x] Event Builders / WebSocket Subscriptions (NIP-01)
- [x] Home Feed
- [x] Notifications Feed
- [x] Global Feed
- [x] Replies and mentions (NIP-10)
- [x] Reactions (NIP-25)
- [x] Reposts (NIP-18)
- [x] Image/Video/Url/LnInvoice Previews
- [x] View Threads
## Supported Features
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px">
- [x] Events / Relay Subscriptions (NIP-01)
- [x] Follow List (NIP-02)
- [x] OpenTimestamps Attestations (NIP-03)
- [x] Private Messages (NIP-04)
- [x] User Profiles (edit/follow/unfollow - NIP-02)
- [x] Public Chats (NIP-28)
- [x] Bech Encoding support (NIP-19)
- [x] Notification Dots
- [x] Reporting and Hide User capability (NIP-56)
- [x] Automatic Translations
- [x] Relay Sets (home, dms, public chats, global)
- [x] User/Note Tagging (NIP-08, NIP-10)
- [x] Lightning Tips
- [x] Zaps (private, public, anon, non-zap) (NIP-57)
- [x] URI Support (NIP-21)
- [x] Created_at Limits (NIP-22)
- [x] DNS Address (NIP-05)
- [x] Mnemonic seed phrase (NIP-06)
- [ ] WebBrowser Signer (NIP-07, Not applicable)
- [x] Old-style mentions (NIP-08)
- [x] Event Deletion (NIP-09)
- [x] Nostr Address (NIP-05)
- [x] Replies, mentions, Threads, and Notifications (NIP-10)
- [x] Relay Information Document (NIP-11)
- [x] Generic Tag Queries (NIP-12)
- [x] Proof of Work Display (NIP-13)
- [ ] Proof of Work Calculations (NIP-13)
- [x] Events with a Subject (NIP-14)
- [ ] Marketplace (NIP-15)
- [x] Event Treatment (NIP-16)
- [x] Private Direct Messages (NIP-17)
- [x] Image/Video/Url/LnInvoice Previews
- [x] Reposts, Quotes, Generic Reposts (NIP-18)
- [x] Bech Encoding support (NIP-19)
- [x] Command Results (NIP-20)
- [x] URI Support (NIP-21)
- [x] Long-form Content (NIP-23)
- [x] User Profile Fields / Relay list (NIP-24)
- [x] Reactions (NIP-25)
- [ ] Delegated Event Signing (NIP-26, Will not implement)
- [x] Text Note References (NIP-27)
- [x] Public Chats (NIP-28)
- [ ] Relay-based Groups (NIP-29)
- [x] Custom Emoji (NIP-30)
- [x] Event kind summaries (NIP-31)
- [ ] Labeling (NIP-32)
- [x] Parameterized Replaceable Events (NIP-33)
- [x] Online Relay Search (NIP-50)
- [x] Internationalization
- [x] Badges (NIP-58)
- [x] Hashtag Following and Custom Hashtags
- [x] Polls (NIP-69)
- [x] Verifiable static content in URLs (NIP-94)
- [x] Login with QR
- [x] Git Stuff (NIP-34)
- [ ] Torrents (NIP-35)
- [x] Sensitive Content (NIP-36)
- [x] Edits (NIP-37/Draft)
- [x] User Status Event (NIP-38)
- [x] External Identities (NIP-39)
- [x] Expiration Support (NIP-40)
- [x] Relay Authentication (NIP-42)
- [x] Versioned Encrypted Payloads (NIP-44)
- [ ] Event Counts (NIP-45, Will not implement)
- [ ] Nostr Connect (NIP-46)
- [x] Wallet Connect API (NIP-47)
- [x] Accessible uploads
- [ ] Proxy Tags (NIP-48, Not applicable)
- [x] Private key encryption for import/export (NIP-49)
- [x] Online Relay Search (NIP-50)
- [x] Lists (NIP-51)
- [ ] Calendar Events (NIP-52)
- [x] Live Activities & Live Chats (NIP-53)
- [x] Wiki (NIP-54)
- [x] Inline Metadata (NIP-55 - Draft)
- [x] Reporting (NIP-56)
- [x] Lightning Tips
- [x] Zaps (NIP-57)
- [x] Private Zaps
- [x] Zap Splits (NIP-57)
- [x] Badges (NIP-58)
- [x] Gift Wraps & Seals (NIP-59)
- [x] Zapraiser (NIP-TBD)
- [ ] Relay List Metadata (NIP-65)
- [x] Polls (NIP-69)
- [x] Video Events (NIP-71)
- [x] Moderated Communities (NIP-72)
- [ ] Zap Goals (NIP-75)
- [ ] Arbitrary Custom App Data (NIP-78)
- [x] Highlights (NIP-84)
- [x] Notify Request (NIP-88/Draft)
- [x] Recommended Application Handlers (NIP-89)
- [x] Data Vending Machine (NIP-90)
- [x] Inline Metadata (NIP-92)
- [x] Verifiable file URLs (NIP-94)
- [x] Binary Blobs (NIP-95/Draft)
- [x] HTTP File Storage Integration (NIP-96)
- [x] HTTP Auth (NIP-98)
- [x] Classifieds (NIP-99)
- [x] Audio Tracks (zapstr.live) (kind:31337)
- [x] Push Notifications (Google and Unified Push)
- [x] In-Device Automatic Translations
- [x] Hashtag Following and Custom Hashtags
- [x] Login with QR
- [x] Bounty support (nostrbounties.com)
- [x] De-googled F-Droid flavor
- [x] External Identity Support (NIP-39)
- [x] Multiple Accounts
- [x] Markdown Support
- [x] Relay Authentication (NIP-42)
- [x] Content stored in relays themselves (NIP-95)
- [x] Custom Emoji (NIP-30)
- [x] Zap Forwarding (NIP-57)
- [x] Text Note References (NIP-27)
- [x] Audio Tracks (zapstr.live) (Kind:31337)
- [x] Push Notifications (Zaps and Messages)
- [x] Generic Tags (NIP-12)
- [x] Sensitive Content (NIP-36)
- [x] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
- [x] Recommended Application Handlers (NIP-89)
- [x] Events with a Subject (NIP-14)
- [x] Generic Reposts (kind:16)
- [x] Live Activities & Live Chats (NIP-53)
- [x] Relay Pages (NIP-11)
- [x] HTTP Auth (NIP-98)
- [x] Zapraiser (NIP-TBD)
- [x] Moderated Communities (NIP-72)
- [x] Emoji Packs (Kind:30030)
- [x] Personal Emoji Lists (Kind:10030)
- [x] Classifieds (Kind:30403)
- [x] Private Messages and Small Groups (NIP-24)
- [x] Gift Wraps & Seals (NIP-59)
- [x] Versioned Encrypted Payloads (NIP-44)
- [x] Expiration Support (NIP-40)
- [x] Status Event (NIP-38)
- [ ] Marketplace (NIP-15)
- [x] Medical Data (NIP-xx/Draft)
- [x] Embed events (NIP-xx/Draft)
- [x] Draft Events (NIP-xx/Draft)
- [ ] Event Sets (NIP-xx/Draft)
- [ ] Topical Notes (NIP-xx/Draft)
- [ ] Relationship Status (NIP-xx/Draft)
- [ ] Signed Filters (NIP-xx/Draft)
- [ ] Key Migration (NIP-xx/Draft)
- [ ] Time-based Sync (NIP-xx/Draft)
- [ ] Image/Video Capture in the app
- [ ] Local Database
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
- [ ] Workspaces
- [ ] Infinity Scroll
- [ ] Relay List Metadata (NIP-65)
- [ ] Signing Requests (NIP-46)
- [ ] Delegated Event Signing (NIP-26)
- [ ] Account Creation / Backup Guidance
- [ ] Mnemonic seed phrase (NIP-06)
- [ ] Message Sent feedback (NIP-20)
- [ ] OpenTimestamps Attestations (NIP-03)
## Privacy and Information Permanence
Relays know your IP address, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) except for Private Zaps and Private DMs. While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN or Tor) from trackers online.
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
Information shared on Nostr can be re-broadcasted to other servers and should be assumed permanent for privacy purposes. There is no way to guarantee the deletion of any content once posted.
# Development Overview
## Overall Architecture
This repository is split between Amethyst and Quartz:
- Amethyst is a native Android app made with Kotlin and Jetpack Compose.
- Quartz is our own Nostr-commons library to host classes that are of interest to other Nostr Clients.
This is a native Android app made with Kotlin and Jetpack Compose.
The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays.
The overall architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
The app architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to
allow the UI and other parts of the app to subscribe to each individual Note/User and receive updates when they happen.
They are also responsible for updating viewModels when needed. Filters react to changes in the screen. As the user
sees different Events, the Datasource classes are used to receive more information about those particular Events.
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData and Flow objects to
allow the UI and other parts of the app to subscribe to each Note/User and receive updates when they happen.
They are also responsible for updating viewModels when needed. As the user scrolls through Events, the Datasource classes
are updated to receive more information about those particular Events.
Most of the UI is reactive to changes in the repository classes. The service layer assembles Nostr filters for each need of the app,
receives the data from the Relay, and sends it to the repository. Connection with relays is never closed during the use of the app.
The UI receives a notification that objects were updated. Instances of User and Notes are mutable directly.
The UI receives a notification that objects have been updated. Instances of User and Notes are mutable directly.
There will never be two Notes with the same ID or two User instances with the same pubkey.
Lastly, the user's account information (priv key/pub key) is stored in the Android KeyStore for security.
Lastly, the user's account information (private key/pub key) is stored in the Android KeyStore for security.
## Setup
Make sure to have the following pre-requisites installed:
1. Java 17
1. Java 17+
2. Android Studio
3. Android 8.0+ Phone or Emulation setup
Fork and clone this repository and import into Android Studio
Fork and clone this repository and import it into Android Studio
```bash
git clone https://github.com/vitorpamplona/amethyst.git
```
Use one of the Android Studio builds to install and run the app in your device or a simulator.
Use an Android Studio build action to install and run the app on your device or a simulator.
## Building
Build the app:
@@ -149,8 +198,8 @@ Build the app:
## Linting
```bash
./gradlew ktlintCheck
./gradlew ktlintFormat
./gradlew spotlessCheck
./gradlew spotlessApply
```
## Installing on device
@@ -165,7 +214,7 @@ For the Play build:
./gradlew installPlayDebug
```
## How to Deploy
## Deploying
1. Generate a new signing key
```
@@ -177,42 +226,52 @@ 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.
7. Add your CHANGE LOG to the description of the new release
8. Download the `aab` file and upload it to the` PlayStore.
8. Download the `aab` file and upload it to the PlayStore.
# Privacy on Relays & nostr
Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
## Using the Quartz library
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
Setup [JitPack.io](https://jitpack.io/#vitorpamplona/amethyst/v0.84.3) to your build file
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of Private Zaps and Private DMs.
Add `maven { url 'https://jitpack.io' }` to settings.gradle at the end of repositories:
# DM Privacy #
While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
```gradle
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
```
# Visibility & Permanence of Your Content on nostr
## Information Visibility ##
Content that you share can be shared to other relays.
Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
Add the dependency
## Information Permanence ##
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted.
```gradle
implementation('com.github.vitorpamplona.amethyst:quartz:v0.85.1')
```
## Contributing
Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst)
[GitHub issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also welcome. Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social)
You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac)
By contributing to this repository, you agree to license your work under the MIT license. Any work contributed where you are not the original author must contain its license header with the original author(s) and source.
# Screenshots
| Home Feed | Messages | Live Streams | Notifications |
| FollowFeeds | ChatsGroup | LiveStreams | Notifications |
|-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------|
| ![Home Feed](./docs/screenshots/home.png) | ![Messages](./docs/screenshots/messages.png) | ![Live Streams](./docs/screenshots/replies.png) | ![Notifications](./docs/screenshots/notifications.png) |
# Contributing
[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) are very welcome.
## Contributors
# Contributors
<a align="center" href="https://github.com/vitorpamplona/amethyst/graphs/contributors">
<img src="https://contrib.rocks/image?repo=vitorpamplona/amethyst" />
@@ -220,6 +279,7 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th
# MIT License
<pre>
Copyright (c) 2023 Vitor Pamplona
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -239,3 +299,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</pre>

297
amethyst/build.gradle Normal file
View File

@@ -0,0 +1,297 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.googleServices)
alias(libs.plugins.jetbrainsComposeCompiler)
}
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 393
versionName "0.89.7"
buildConfigField "String", "RELEASE_NOTES_ID", "\"1130badb252bdf62aaf93cb65bfa2bb09e7600c7d6764894f2954555b3b73018\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
resourceConfigurations += [
'ar',
'bn-rBD',
'cs',
'cy-rGB',
'da-rDK',
'de',
'el-rGR',
'en-rGB',
'eo',
'es',
'es-rES',
'es-rMX',
'es-rUS',
'et-rEE',
'fa',
'fi-rFI',
'fo-rFO',
'fr',
'fr-rCA',
'gu-rIN',
'hi-rIN',
'hr-rHR',
'hu',
'in',
'in-rID',
'it-rIT',
'iw-rIL',
'ja',
'kk-rKZ',
'ko-rKR',
'ks-rIN',
'ku-rTR',
'lt-rLT',
'ne-rNP',
'night',
'nl',
'nl-rBE',
'pcm-rNG',
'pl-rPL',
'pt-rBR',
'pt-rPT',
'ru',
'ru-rUA',
'sa-rIN',
'sl-rSI',
'so-rSO',
'sr-rSP',
'ss-rZA',
'sv-rSE',
'sw-rKE',
'sw-rTZ',
'ta',
'th',
'tr',
'uk',
'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
resValue "string", "app_name", "@string/app_name_release"
}
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
resValue "string", "app_name", "@string/app_name_debug"
}
create("benchmark") {
initWith(getByName("release"))
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_17
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
compose true
buildConfig true
}
packagingOptions {
resources {
excludes += ['/META-INF/{AL2.0,LGPL2.1}', '**/libscrypt.dylib']
}
}
lint {
disable 'MissingTranslation'
}
testOptions {
unitTests.returnDefaultValues = true
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
composeCompiler {
reportsDestination = 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
// Observe Live data as State
implementation libs.androidx.runtime.livedata
// 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
implementation libs.androidx.lifecycle.livedata.ktx
// Zoomable images
implementation libs.zoomable
// Biometrics
implementation libs.androidx.biometric.ktx
// Websockets API
implementation libs.okhttp
// Encrypted Key Storage
implementation libs.androidx.security.crypto.ktx
// 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
// create blurhash
implementation libs.trbl.blurhash
// 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
// 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
testImplementation libs.junit
testImplementation libs.mockk
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
}

View File

@@ -33,6 +33,11 @@
# JNA For Libsodium
-keep class com.goterl.lazysodium.** { *; }
# libscrypt
-keep class com.lambdaworks.codec.** { *; }
-keep class com.lambdaworks.crypto.** { *; }
-keep class com.lambdaworks.jni.** { *; }
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
-keep class com.sun.jna.ToNativeConverter { *; }
-keep class com.sun.jna.NativeMapped { *; }
@@ -58,3 +63,5 @@
-keep class com.vitorpamplona.quartz.events.** { *; }
-keep class com.vitorpamplona.amethyst.model.** { *; }
-keep class com.vitorpamplona.amethyst.service.** { *; }
-keep class com.vitorpamplona.ammolite.service.** { *; }
-keep class com.vitorpamplona.ammolite.relays.** { *; }

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import android.graphics.Bitmap
import android.graphics.Color
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.Nip96Retriever
import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
import com.vitorpamplona.quartz.crypto.KeyPair
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.fail
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import java.io.ByteArrayOutputStream
import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
class ImageUploadTesting {
private suspend fun testBase(server: Nip96MediaServers.ServerName) {
val serverInfo =
Nip96Retriever()
.loadInfo(
server.baseUrl,
)
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)
val bytes = baos.toByteArray()
val inputStream = bytes.inputStream()
val account = Account(KeyPair())
val result =
Nip96Uploader(account)
.uploadImage(
inputStream,
bytes.size.toLong(),
"image/png",
alt = null,
sensitiveContent = null,
serverInfo,
onProgress = {},
)
val url = result.tags!!.first { it[0] == "url" }.get(1)
val size =
result.tags!!
.firstOrNull { it[0] == "size" }
?.get(1)
?.ifBlank { null }
val dim =
result.tags!!
.firstOrNull { it[0] == "dim" }
?.get(1)
?.ifBlank { null }
val hash =
result.tags!!
.firstOrNull { it[0] == "x" }
?.get(1)
?.ifBlank { null }
val contentType = result.tags!!.first { it[0] == "m" }.get(1)
val ox = result.tags!!.first { it[0] == "ox" }.get(1)
Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http"))
val imageData: ByteArray =
ImageDownloader().waitAndGetImage(url)
?: run {
fail("${server.name}: Should not be null")
return
}
FileHeader.prepare(
imageData,
"image/png",
null,
onReady = {
if (dim != null) {
// assertEquals("${server.name}: Invalid dimensions", it.dim, dim)
}
if (size != null) {
// assertEquals("${server.name}: Invalid size", it.size.toString(), size)
}
if (hash != null) {
assertEquals("${server.name}: Invalid hash", it.hash, hash)
}
},
onError = { fail("${server.name}: It should not fail") },
)
// delay(1000)
// assertTrue(Nip96Uploader(account).delete(ox, contentType, serverInfo))
}
@Test
fun runTestOnDefaultServers() =
runBlocking {
Nip96MediaServers.DEFAULT.forEach {
testBase(it)
}
}
@Test()
fun testNostrCheck() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostrcheck.me", "https://nostrcheck.me"))
}
@Test()
@Ignore("Not Working anymore")
fun testNostrage() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostrage", "https://nostrage.com"))
}
@Test()
@Ignore("Not Working anymore")
fun testSove() =
runBlocking {
testBase(Nip96MediaServers.ServerName("sove", "https://sove.rent"))
}
@Test()
fun testNostrBuild() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostr.build", "https://nostr.build"))
}
@Test()
@Ignore("Not Working anymore")
fun testSovbit() =
runBlocking {
testBase(Nip96MediaServers.ServerName("sovbit", "https://files.sovbit.host"))
}
@Test()
fun testVoidCat() =
runBlocking {
testBase(Nip96MediaServers.ServerName("void.cat", "https://void.cat"))
}
@Test()
fun testNostrPic() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostpic.com", "https://nostpic.com"))
}
@Test()
@Ignore("Not Working anymore")
fun testNostrOnch() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostr.onch.services", "https://nostr.onch.services"))
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.ots.OpenTimestamps
import com.vitorpamplona.quartz.signers.NostrSignerInternal
import junit.framework.TestCase.assertEquals
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class OkHttpOtsTest {
val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}"
val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}"
val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}"
val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6"
@Before
fun setup() {
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
}
@Test
fun verifyNostrEvent() {
val ots = Event.fromJson(otsEvent) as OtsEvent
println(ots.info())
assertEquals(1707688818L, ots.verify())
}
@Test
fun verifyNostrEvent2() {
val ots = Event.fromJson(otsEvent2) as OtsEvent
println(ots.info())
assertEquals(1706322179L, ots.verify())
}
@Test
fun verifyNostrPendingEvent() {
val ots = Event.fromJson(otsPendingEvent) as OtsEvent
println(ots.info())
assertEquals(null, ots.verify())
}
@Test
fun createOTSEventAndVerify() {
val signer = NostrSignerInternal(KeyPair())
var ots: OtsEvent? = null
val countDownLatch = CountDownLatch(1)
val otsFile = OtsEvent.stamp(otsEvent2Digest)
OtsEvent.create(otsEvent2Digest, otsFile, signer) {
ots = it
countDownLatch.countDown()
}
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
println(ots!!.toJson())
println(ots!!.info())
// Should not be valid because we need to wait for confirmations
assertEquals(null, ots!!.verify())
}
}

View File

@@ -0,0 +1,262 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import androidx.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.encoders.decodePublicKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.UserMetadata
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

@@ -1,3 +1,23 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -10,17 +30,27 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TranslationsTest {
fun translateTo(text: String, translateTo: String): String? {
fun translateTo(
text: String,
translateTo: String,
): String? {
val task = LanguageTranslatorService.autoTranslate(text, emptySet(), translateTo)
return Tasks.await(task).result
}
fun assertTranslate(expected: String, input: String, translateTo: String) {
fun assertTranslate(
expected: String,
input: String,
translateTo: String,
) {
assertEquals(null, expected, translateTo(input, translateTo))
}
fun assertTranslateContains(expected: String, input: String, translateTo: String) {
fun assertTranslateContains(
expected: String,
input: String,
translateTo: String,
) {
val translated = translateTo(input, translateTo)!!
assertTrue("'$translated' does not contain '$expected'", translated.contains(expected))
}
@@ -44,23 +74,47 @@ class TranslationsTest {
fun testTranslationUrl() {
assertTranslateContains("https://t.me/mygroup", "Have you seen this https://t.me/mygroup", "pt")
assertTranslateContains("http://bananas.com", "Have you seen this http://bananas.com", "pt")
assertTranslateContains("http://bananas.com/myimage.jpg", "Have you seen this http://bananas.com/myimage.jpg", "pt")
assertTranslateContains("http://bananas.com?search=true&image=myimage.jpg", "Have you seen this http://bananas.com?search=true&image=myimage.jpg", "pt")
assertTranslateContains(
"http://bananas.com/myimage.jpg",
"Have you seen this http://bananas.com/myimage.jpg",
"pt",
)
assertTranslateContains(
"http://bananas.com?search=true&image=myimage.jpg",
"Have you seen this http://bananas.com?search=true&image=myimage.jpg",
"pt",
)
assertTranslate("https://i.imgur.com/EZ3QPsw.jpg", "https://i.imgur.com/EZ3QPsw.jpg", "pt")
assertTranslate("https://HaveYouSeenThis.com", "https://HaveYouSeenThis.com", "pt")
assertTranslate("https://haveyouseenthis.com", "https://haveyouseenthis.com", "pt")
assertTranslate("https://i.imgur.com/asdEZ3QPsw.jpg", "https://i.imgur.com/asdEZ3QPsw.jpg", "pt")
assertTranslateContains("https://i.imgur.com/asdEZ3QPswadfj2389rioasdjf9834riofaj9834aKLL.jpg", "Hi there! \n How are you doing? \n https://i.imgur.com/asdEZ3QPswadfj2389rioasdjf9834riofaj9834aKLL.jpg", "pt")
assertTranslate(
"https://i.imgur.com/asdEZ3QPsw.jpg",
"https://i.imgur.com/asdEZ3QPsw.jpg",
"pt",
)
assertTranslateContains(
"https://i.imgur.com/asdEZ3QPswadfj2389rioasdjf9834riofaj9834aKLL.jpg",
"Hi there! \n How are you doing? \n https://i.imgur.com/asdEZ3QPswadfj2389rioasdjf9834riofaj9834aKLL.jpg",
"pt",
)
}
@Test
fun testChineseWithUrlDetector() {
assertTranslate("I entered your home page is very carton, perhaps your attention or other data is too much, and the homepage of others is not so carton. From aMethyst client", "我进入你的主页很卡顿也许是你的关注人数或者其他数据太多了其他人主页没有这么卡顿。来自amethyst客户端", "en")
assertTranslate(
"I entered your home page is very carton, perhaps your attention or other data is too much, and the homepage of others is not so carton. From aMethyst client",
"我进入你的主页很卡顿也许是你的关注人数或者其他数据太多了其他人主页没有这么卡顿。来自amethyst客户端",
"en",
)
}
@Test
fun testTranslationEmail() {
assertTranslateContains("vitor@amethyst.social", "Have you seen this vitor@amethyst.social", "pt")
assertTranslateContains(
"vitor@amethyst.social",
"Have you seen this vitor@amethyst.social",
"pt",
)
}
@Test
@@ -68,13 +122,13 @@ class TranslationsTest {
assertTranslateContains(
"lnbc12u1p3lvjeupp5a5ecgp45k6pa8tu7rnkgzfuwdy3l5ylv3k5tdzrg4cr8rj2f364sdq5g9kxy7fqd9h8vmmfvdjscqzpgxqyz5vqsp5zuzyetf33aphetf0e80w7tztw6dfsjs4lmvya4cyk8umfsx00qts9qyyssqke9hphcr36zvcav8wr502g0mhfhxpy8m9tt36zttg8vldm2qxw039ulccr8nwy3hjg2sw5vk65e99lwuhrhw0nuya2u57qszltvx7egp74jydn",
"Have you seen this: lnbc12u1p3lvjeupp5a5ecgp45k6pa8tu7rnkgzfuwdy3l5ylv3k5tdzrg4cr8rj2f364sdq5g9kxy7fqd9h8vmmfvdjscqzpgxqyz5vqsp5zuzyetf33aphetf0e80w7tztw6dfsjs4lmvya4cyk8umfsx00qts9qyyssqke9hphcr36zvcav8wr502g0mhfhxpy8m9tt36zttg8vldm2qxw039ulccr8nwy3hjg2sw5vk65e99lwuhrhw0nuya2u57qszltvx7egp74jydn I think I have to pay",
"pt"
"pt",
)
assertTranslateContains(
"lnbc10u1p3l0wg0pp5y5y3vxt3429m28uuq56uqhwxadftn67yaarq06h3y9nqapz72n6sdqqxqyjw5q9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqsp5y2tazp42xde3c0tdsz30zqcekrt0lzrneszdtagy2qn7vs0d3p5qrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glcll7jdvcln4lhw5qqqqlgqqqqqeqqjqdau9jzseecmvmh03h88xyf5f980xx45fmn0cej654v5jr79ye36pww90jwdda38damlmgt54v8rn6q9kywtw057rh4v3wwrmn8fajagqnssr7v",
"Test lnbc10u1p3l0wg0pp5y5y3vxt3429m28uuq56uqhwxadftn67yaarq06h3y9nqapz72n6sdqqxqyjw5q9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqsp5y2tazp42xde3c0tdsz30zqcekrt0lzrneszdtagy2qn7vs0d3p5qrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glcll7jdvcln4lhw5qqqqlgqqqqqeqqjqdau9jzseecmvmh03h88xyf5f980xx45fmn0cej654v5jr79ye36pww90jwdda38damlmgt54v8rn6q9kywtw057rh4v3wwrmn8fajagqnssr7v",
"pt"
"pt",
)
}
@@ -83,7 +137,7 @@ class TranslationsTest {
assertTranslateContains(
"nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy",
"sure, nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy",
"en"
"en",
)
}
@@ -96,7 +150,7 @@ class TranslationsTest {
"https://youtu.be/wMYFmCDy_Eg\n" +
"\n" +
"先輩がうざい後輩の話と似たような話かと思ったけど、もっとオタクの妄想あるある的なものを詰め込んだやつだ。ワードとかシチュエーションとか、ヒロインのサイズ感とか。知らんけど",
"en"
"en",
)
}
@@ -105,7 +159,7 @@ class TranslationsTest {
assertTranslateContains(
"https://cdn.nostr.build/i/df3783dcdf7dd289ba02ba538dc039c8fe1d4db055e580b81604ed88c6af4ee0.jpg",
"\uD83E\uDD23 https://cdn.nostr.build/i/df3783dcdf7dd289ba02ba538dc039c8fe1d4db055e580b81604ed88c6af4ee0.jpg ",
"pt"
"pt",
)
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.lang
object LanguageTranslatorService {
fun clear() {}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.notifications
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import com.vitorpamplona.amethyst.Amethyst
import org.unifiedpush.android.connector.UnifiedPush
interface PushDistributorActions {
fun getSavedDistributor(): String
fun getInstalledDistributors(): List<String>
fun saveDistributor(distributor: String)
fun removeSavedDistributor()
}
object PushDistributorHandler : PushDistributorActions {
private val appContext = Amethyst.instance.applicationContext
private val unifiedPush: UnifiedPush = UnifiedPush
private var endpointInternal = ""
val endpoint = endpointInternal
fun getSavedEndpoint() = endpoint
fun setEndpoint(newEndpoint: String) {
endpointInternal = newEndpoint
Log.d("PushHandler", "New endpoint saved : $endpointInternal")
}
fun removeEndpoint() {
endpointInternal = ""
}
override fun getSavedDistributor(): String = unifiedPush.getSavedDistributor(appContext) ?: ""
fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty()
override fun getInstalledDistributors(): List<String> = unifiedPush.getDistributors(appContext)
fun formattedDistributorNames(): List<String> {
val distributorsArray = getInstalledDistributors().toTypedArray()
val distributorsNameArray =
distributorsArray
.map {
try {
val ai =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
appContext.packageManager.getApplicationInfo(
it,
PackageManager.ApplicationInfoFlags.of(
PackageManager.GET_META_DATA.toLong(),
),
)
} else {
appContext.packageManager.getApplicationInfo(it, 0)
}
appContext.packageManager.getApplicationLabel(ai)
} catch (e: PackageManager.NameNotFoundException) {
it
}
as String
}.toTypedArray()
return distributorsNameArray.toList()
}
override fun saveDistributor(distributor: String) {
unifiedPush.saveDistributor(appContext, distributor)
unifiedPush.registerApp(appContext)
}
override fun removeSavedDistributor() {
unifiedPush.safeRemoveDistributor(appContext)
}
fun forceRemoveDistributor(context: Context) {
unifiedPush.forceRemoveDistributor(context)
}
}

View File

@@ -1,3 +1,23 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.notifications
import android.app.NotificationManager
@@ -12,6 +32,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrC
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -20,21 +41,27 @@ import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.MessagingReceiver
class PushMessageReceiver : MessagingReceiver() {
private val TAG = "Amethyst-OSSPushReceiver"
companion object {
private val TAG = "Amethyst-OSSPushReceiver"
}
private val appContext = Amethyst.instance.applicationContext
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val eventCache = LruCache<String, String>(100)
private val pushHandler = PushDistributorHandler
override fun onMessage(context: Context, message: ByteArray, instance: String) {
override fun onMessage(
context: Context,
message: ByteArray,
instance: String,
) {
val messageStr = String(message)
Log.d(TAG, "New message ${message.decodeToString()} for Instance: $instance")
scope.launch {
try {
parseMessage(messageStr)?.let {
receiveIfNew(it)
}
parseMessage(messageStr)?.let { receiveIfNew(it) }
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.d(TAG, "Message could not be parsed: ${e.message}")
}
}
@@ -54,7 +81,11 @@ class PushMessageReceiver : MessagingReceiver() {
}
}
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
override fun onNewEndpoint(
context: Context,
endpoint: String,
instance: String,
) {
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance")
val sanitizedEndpoint = endpoint.dropLast(5)
pushHandler.setEndpoint(sanitizedEndpoint)
@@ -65,20 +96,29 @@ class PushMessageReceiver : MessagingReceiver() {
}
}
override fun onReceive(context: Context, intent: Intent) {
override fun onReceive(
context: Context,
intent: Intent,
) {
val intentData = intent.dataString
val intentAction = intent.action.toString()
Log.d(TAG, "Intent Data:- $intentData Intent Action: $intentAction")
super.onReceive(context, intent)
}
override fun onRegistrationFailed(context: Context, instance: String) {
override fun onRegistrationFailed(
context: Context,
instance: String,
) {
Log.d(TAG, "Registration failed for Instance: $instance")
scope.cancel()
pushHandler.forceRemoveDistributor(context)
}
override fun onUnregistered(context: Context, instance: String) {
override fun onUnregistered(
context: Context,
instance: String,
) {
val removedEndpoint = pushHandler.endpoint
Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance")
Log.d(TAG, "App is unregistered. ")
@@ -86,7 +126,7 @@ class PushMessageReceiver : MessagingReceiver() {
pushHandler.removeEndpoint()
}
fun notificationManager(): NotificationManager {
return ContextCompat.getSystemService(appContext, NotificationManager::class.java) as NotificationManager
}
fun notificationManager(): NotificationManager =
ContextCompat.getSystemService(appContext, NotificationManager::class.java)
as NotificationManager
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.notifications
import android.util.Log
import com.vitorpamplona.amethyst.AccountInfo
import kotlinx.coroutines.CancellationException
object PushNotificationUtils {
var hasInit: Boolean = false
private val pushHandler = PushDistributorHandler
suspend fun init(accounts: List<AccountInfo>) {
if (hasInit || pushHandler.savedDistributorExists()) {
return
}
try {
if (pushHandler.savedDistributorExists()) {
RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint())
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.")
}
}
}

View File

@@ -1,3 +1,23 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import android.util.Log
@@ -23,13 +43,14 @@ 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.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.halilibo.richtext.markdown.Markdown
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.Material3RichText
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
@@ -38,13 +59,15 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.CheckifItNeedsToRequestNoti
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingsRow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
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)
val notificationPermissionState =
CheckifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel)
if (notificationPermissionState.status.isGranted) {
if (!sharedPreferencesViewModel.sharedPrefs.dontShowPushNotificationSelector) {
@@ -53,10 +76,10 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
mutableStateOf(PushDistributorHandler.savedDistributorExists())
}
if (!distributorPresent) {
LoadDistributors() { currentDistributor, list, readableListWithExplainer ->
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
if (readableListWithExplainer.size > 1) {
SpinnerSelectionDialog(
title = stringResource(id = R.string.select_push_server),
title = stringRes(id = R.string.select_push_server),
options = readableListWithExplainer,
onSelect = { index ->
if (list[index] == "None") {
@@ -73,54 +96,57 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
onDismiss = {
distributorPresent = true
Log.d("Amethyst", "NotificationScreen: Distributor dialog dismissed.")
}
},
)
} else {
AlertDialog(
onDismissRequest = {
distributorPresent = true
},
title = {
Text(stringResource(R.string.push_server_install_app))
},
onDismissRequest = { distributorPresent = true },
title = { Text(stringRes(R.string.push_server_install_app)) },
text = {
Material3RichText(
style = RichTextStyle().resolveDefaults()
val content = stringRes(R.string.push_server_install_app_description)
val astNode =
remember {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
}
RichText(
style = RichTextStyle().resolveDefaults(),
renderer = null,
) {
Markdown(
content = stringResource(R.string.push_server_install_app_description)
)
BasicMarkdown(astNode)
}
},
confirmButton = {
Row(
modifier = Modifier
.padding(all = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
TextButton(onClick = {
distributorPresent = true
sharedPreferencesViewModel.dontShowPushNotificationSelector()
}) {
Text(stringResource(R.string.quick_action_dont_show_again_button))
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)) {
Button(
onClick = { distributorPresent = true },
contentPadding = PaddingValues(horizontal = 16.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null
contentDescription = null,
)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.error_dialog_button_ok))
Text(stringRes(R.string.error_dialog_button_ok))
}
}
}
}
},
)
}
}
@@ -133,35 +159,33 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
}
@Composable
fun LoadDistributors(
onInner: @Composable (String, ImmutableList<String>, ImmutableList<TitleExplainer>) -> Unit
) {
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 list =
remember {
PushDistributorHandler.getInstalledDistributors().plus("None").toImmutableList()
}
val readableListWithExplainer =
PushDistributorHandler.formattedDistributorNames()
PushDistributorHandler
.formattedDistributorNames()
.mapIndexed { index, name ->
TitleExplainer(
name,
stringResource(id = R.string.push_server_uses_app_explainer, list[index])
stringRes(id = R.string.push_server_uses_app_explainer, list[index]),
)
}
.plus(
}.plus(
TitleExplainer(
stringResource(id = R.string.push_server_none),
stringResource(id = R.string.push_server_none_explainer)
)
)
.toImmutableList()
stringRes(id = R.string.push_server_none),
stringRes(id = R.string.push_server_none_explainer),
),
).toImmutableList()
onInner(
currentDistributor,
list,
readableListWithExplainer
readableListWithExplainer,
)
}
@@ -169,12 +193,12 @@ fun LoadDistributors(
fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) {
val context = LocalContext.current
LoadDistributors() { currentDistributor, list, readableListWithExplainer ->
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
SettingsRow(
R.string.push_server_title,
R.string.push_server_explainer,
selectedItens = readableListWithExplainer,
selectedIndex = list.indexOf(currentDistributor)
selectedIndex = list.indexOf(currentDistributor),
) { index ->
if (list[index] == "None") {
sharedPreferencesViewModel.dontAskForNotificationPermissions()

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.events.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: (String) -> Unit,
) = ExpandableRichTextViewer(
content,
canPreview,
quotesLeft,
modifier,
tags,
backgroundColor,
id,
callbackUri,
accountViewModel,
nav,
)

View File

@@ -3,8 +3,11 @@
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="org.torproject.android"/>
<package android:name="com.greenart7c3.nostrsigner"/>
<package android:name="com.greenart7c3.nostrsigner.debug"/>
<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 -->

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
import androidx.compose.ui.unit.Velocity
/**
* A nested scroll modifier that provides scroll events to [state].
*
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
* order to receive scroll events. For example:
*
* @param state The [PullRefreshState] associated with this pull-to-refresh component. The state
* will be updated by this modifier.
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored.
* @sample androidx.compose.material.samples.PullRefreshSample
*/
fun Modifier.pullRefresh(
state: PullRefreshState,
enabled: Boolean = true,
) = inspectable(
inspectorInfo =
debugInspectorInfo {
name = "pullRefresh"
properties["state"] = state
properties["enabled"] = enabled
},
) {
Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
}
/**
* A nested scroll modifier that provides [onPull] and [onRelease] callbacks to aid building custom
* pull refresh components.
*
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
* order to receive scroll events. For example:
*
* @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument.
* Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling
* down despite being at the top of a scrollable component), whereas negative delta (swiping up)
* is dispatched first (in case it is needed to push the indicator back up), and then the
* unconsumed delta is passed on to the child. The callback returns how much delta was consumed.
* @param onRelease Callback for when drag is released, takes float flingVelocity as argument. The
* callback returns how much velocity was consumed - in most cases this should only consume
* velocity if pull refresh has been dragged already and the velocity is positive (the fling is
* downwards), as an upwards fling should typically still scroll a scrollable component beneath
* the pullRefresh. This is invoked before any remaining velocity is passed to the child.
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither
* [onPull] nor [onRelease] will be invoked.
* @sample androidx.compose.material.samples.CustomPullRefreshSample
*/
fun Modifier.pullRefresh(
onPull: (pullDelta: Float) -> Float,
onRelease: suspend (flingVelocity: Float) -> Float,
enabled: Boolean = true,
) = inspectable(
inspectorInfo =
debugInspectorInfo {
name = "pullRefresh"
properties["onPull"] = onPull
properties["onRelease"] = onRelease
properties["enabled"] = enabled
},
) {
Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
}
private class PullRefreshNestedScrollConnection(
private val onPull: (pullDelta: Float) -> Float,
private val onRelease: suspend (flingVelocity: Float) -> Float,
private val enabled: Boolean,
) : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource,
): Offset =
when {
!enabled -> Offset.Zero
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
else -> Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset =
when {
!enabled -> Offset.Zero
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
else -> Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity = Velocity(0f, onRelease(available.y))
}

View File

@@ -1,19 +1,23 @@
/*
* Copyright 2022 The Android Open Source Project
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 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:
*
* http://www.apache.org/licenses/LICENSE-2.0
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* 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 androidx.compose.material3.pullrefresh
import androidx.compose.animation.Crossfade
@@ -56,17 +60,14 @@ import kotlin.math.pow
/**
* The default indicator for Compose pull-to-refresh, based on Android's SwipeRefreshLayout.
*
* @sample androidx.compose.material.samples.PullRefreshSample
*
* @param refreshing A boolean representing whether a refresh is occurring.
* @param state The [PullRefreshState] which controls where and how the indicator will be drawn.
* @param modifier Modifiers for the indicator.
* @param backgroundColor The color of the indicator's background.
* @param contentColor The color of the indicator's arc and arrow.
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
* @sample androidx.compose.material.samples.PullRefreshSample
*/
// TODO(b/244423199): Consider whether the state parameter should be replaced with lambdas to
// enable people to use this indicator with custom pull-to-refresh components.
@Composable
fun PullRefreshIndicator(
refreshing: Boolean,
@@ -74,27 +75,24 @@ fun PullRefreshIndicator(
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(backgroundColor),
scale: Boolean = false
scale: Boolean = false,
) {
val showElevation by remember(refreshing, state) {
derivedStateOf { refreshing || state.position > 0.5f }
}
val showElevation by
remember(refreshing, state) { derivedStateOf { refreshing || state.position > 0.5f } }
Surface(
modifier = modifier
.size(IndicatorSize)
.pullRefreshIndicatorTransform(state, scale),
modifier = modifier.size(IndicatorSize).pullRefreshIndicatorTransform(state, scale),
shape = SpinnerShape,
color = backgroundColor,
shadowElevation = if (showElevation) Elevation else 0.dp
shadowElevation = if (showElevation) Elevation else 0.dp,
) {
Crossfade(
targetState = refreshing,
animationSpec = tween(durationMillis = CrossfadeDurationMs)
animationSpec = tween(durationMillis = CROSSFADE_DURATION_MS),
) { refreshing ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
contentAlignment = Alignment.Center,
) {
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
@@ -102,7 +100,7 @@ fun PullRefreshIndicator(
CircularProgressIndicator(
color = contentColor,
strokeWidth = StrokeWidth,
modifier = Modifier.size(spinnerSize)
modifier = Modifier.size(spinnerSize),
)
} else {
CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize))
@@ -112,22 +110,17 @@ fun PullRefreshIndicator(
}
}
/**
* Modifier.size MUST be specified.
*/
/** Modifier.size MUST be specified. */
@Composable
private fun CircularArrowIndicator(
state: PullRefreshState,
color: Color,
modifier: Modifier
modifier: Modifier,
) {
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
val targetAlpha by remember(state) {
derivedStateOf {
if (state.progress >= 1f) MaxAlpha else MinAlpha
}
}
val targetAlpha by
remember(state) { derivedStateOf { if (state.progress >= 1f) MAX_ALPHA else MIN_ALPHA } }
val alphaState = animateFloatAsState(targetValue = targetAlpha, animationSpec = AlphaTween)
@@ -138,12 +131,13 @@ private fun CircularArrowIndicator(
rotate(degrees = values.rotation) {
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f
val arcBounds = Rect(
size.center.x - arcRadius,
size.center.y - arcRadius,
size.center.x + arcRadius,
size.center.y + arcRadius
)
val arcBounds =
Rect(
size.center.x - arcRadius,
size.center.y - arcRadius,
size.center.x + arcRadius,
size.center.y + arcRadius,
)
drawArc(
color = color,
alpha = alpha,
@@ -152,10 +146,11 @@ private fun CircularArrowIndicator(
useCenter = false,
topLeft = arcBounds.topLeft,
size = arcBounds.size,
style = Stroke(
width = StrokeWidth.toPx(),
cap = StrokeCap.Square
)
style =
Stroke(
width = StrokeWidth.toPx(),
cap = StrokeCap.Square,
),
)
drawArrow(path, arcBounds, color, alpha, values)
}
@@ -167,7 +162,7 @@ private class ArrowValues(
val rotation: Float,
val startAngle: Float,
val endAngle: Float,
val scale: Float
val scale: Float,
)
private fun ArrowValues(progress: Float): ArrowValues {
@@ -181,7 +176,7 @@ private fun ArrowValues(progress: Float): ArrowValues {
val tensionPercent = linearTension - linearTension.pow(2) / 4
// Calculations based on SwipeRefreshLayout specification.
val endTrim = adjustedPercent * MaxProgressArc
val endTrim = adjustedPercent * MAX_PROGRESS_ARC
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
val startAngle = rotation * 360
val endAngle = (rotation + endTrim) * 360
@@ -195,7 +190,7 @@ private fun DrawScope.drawArrow(
bounds: Rect,
color: Color,
alpha: Float,
values: ArrowValues
values: ArrowValues,
) {
arrow.reset()
arrow.moveTo(0f, 0f) // Move to left corner
@@ -204,7 +199,7 @@ private fun DrawScope.drawArrow(
// Line to tip of arrow
arrow.lineTo(
x = ArrowWidth.toPx() * values.scale / 2,
y = ArrowHeight.toPx() * values.scale
y = ArrowHeight.toPx() * values.scale,
)
val radius = min(bounds.width, bounds.height) / 2f
@@ -212,17 +207,15 @@ private fun DrawScope.drawArrow(
arrow.translate(
Offset(
x = radius + bounds.center.x - inset,
y = bounds.center.y + StrokeWidth.toPx() / 2f
)
y = bounds.center.y + StrokeWidth.toPx() / 2f,
),
)
arrow.close()
rotate(degrees = values.endAngle) {
drawPath(path = arrow, color = color, alpha = alpha)
}
rotate(degrees = values.endAngle) { drawPath(path = arrow, color = color, alpha = alpha) }
}
private const val CrossfadeDurationMs = 100
private const val MaxProgressArc = 0.8f
private const val CROSSFADE_DURATION_MS = 100
private const val MAX_PROGRESS_ARC = 0.8f
private val IndicatorSize = 40.dp
private val SpinnerShape = CircleShape
@@ -233,6 +226,6 @@ private val ArrowHeight = 5.dp
private val Elevation = 6.dp
// Values taken from SwipeRefreshLayout
private const val MinAlpha = 0.3f
private const val MaxAlpha = 1f
private const val MIN_ALPHA = 0.3f
private const val MAX_ALPHA = 1f
private val AlphaTween = tween<Float>(300, easing = LinearEasing)

View File

@@ -1,19 +1,23 @@
/*
* Copyright 2022 The Android Open Source Project
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 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:
*
* http://www.apache.org/licenses/LICENSE-2.0
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* 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 androidx.compose.material3.pullrefresh
import androidx.compose.animation.core.LinearOutSlowInEasing
@@ -25,24 +29,23 @@ import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
/**
* A modifier for translating the position and scaling the size of a pull-to-refresh indicator
* based on the given [PullRefreshState].
*
* @sample androidx.compose.material.samples.PullRefreshIndicatorTransformSample
* A modifier for translating the position and scaling the size of a pull-to-refresh indicator based
* on the given [PullRefreshState].
*
* @param state The [PullRefreshState] which determines the position of the indicator.
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
* @sample androidx.compose.material.samples.PullRefreshIndicatorTransformSample
*/
// TODO: Consider whether the state parameter should be replaced with lambdas.
fun Modifier.pullRefreshIndicatorTransform(
state: PullRefreshState,
scale: Boolean = false
scale: Boolean = false,
) = inspectable(
inspectorInfo = debugInspectorInfo {
name = "pullRefreshIndicatorTransform"
properties["state"] = state
properties["scale"] = scale
}
inspectorInfo =
debugInspectorInfo {
name = "pullRefreshIndicatorTransform"
properties["state"] = state
properties["scale"] = scale
},
) {
Modifier
// Essentially we only want to clip the at the top, so the indicator will not appear when
@@ -56,18 +59,16 @@ fun Modifier.pullRefreshIndicatorTransform(
top = 0f,
left = -Float.MAX_VALUE,
right = Float.MAX_VALUE,
bottom = Float.MAX_VALUE
bottom = Float.MAX_VALUE,
) {
this@drawWithContent.drawContent()
}
}
.graphicsLayer {
}.graphicsLayer {
translationY = state.position - size.height
if (scale && !state.refreshing) {
val scaleFraction = LinearOutSlowInEasing
.transform(state.position / state.threshold)
.coerceIn(0f, 1f)
val scaleFraction =
LinearOutSlowInEasing.transform(state.position / state.threshold).coerceIn(0f, 1f)
scaleX = scaleFraction
scaleY = scaleFraction
}

View File

@@ -0,0 +1,234 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.animation.core.animate
import androidx.compose.foundation.MutatorMutex
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.pow
/**
* Creates a [PullRefreshState] that is remembered across compositions.
*
* Changes to [refreshing] will result in [PullRefreshState] being updated.
*
* @param refreshing A boolean representing whether a refresh is currently occurring.
* @param onRefresh The function to be called to trigger a refresh.
* @param refreshThreshold The threshold below which, if a release occurs, [onRefresh] will be
* called.
* @param refreshingOffset The offset at which the indicator will be drawn while refreshing. This
* offset corresponds to the position of the bottom of the indicator.
* @sample androidx.compose.material.samples.PullRefreshSample
*/
@Composable
fun rememberPullRefreshState(
refreshing: Boolean,
onRefresh: () -> Unit,
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
): PullRefreshState {
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
val scope = rememberCoroutineScope()
val onRefreshState = rememberUpdatedState(onRefresh)
val thresholdPx: Float
val refreshingOffsetPx: Float
with(LocalDensity.current) {
thresholdPx = refreshThreshold.toPx()
refreshingOffsetPx = refreshingOffset.toPx()
}
val state =
remember(scope) { PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) }
SideEffect {
state.setRefreshing(refreshing)
state.setThreshold(thresholdPx)
state.setRefreshingOffset(refreshingOffsetPx)
}
return state
}
/**
* A state object that can be used in conjunction with [pullRefresh] to add pull-to-refresh
* behaviour to a scroll component. Based on Android's SwipeRefreshLayout.
*
* Provides [progress], a float representing how far the user has pulled as a percentage of the
* refreshThreshold. Values of one or less indicate that the user has not yet pulled past the
* threshold. Values greater than one indicate how far past the threshold the user has pulled.
*
* Can be used in conjunction with [pullRefreshIndicatorTransform] to implement Android-like
* pull-to-refresh behaviour with a custom indicator.
*
* Should be created using [rememberPullRefreshState].
*/
class PullRefreshState
internal constructor(
private val animationScope: CoroutineScope,
private val onRefreshState: State<() -> Unit>,
refreshingOffset: Float,
threshold: Float,
) {
/**
* A float representing how far the user has pulled as a percentage of the refreshThreshold.
*
* If the component has not been pulled at all, progress is zero. If the pull has reached halfway
* to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has gone beyond
* the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to two times the
* refreshThreshold.
*/
val progress
get() = adjustedDistancePulled / threshold
val refreshing
get() = _refreshing
val position
get() = _position
val threshold
get() = _threshold
private val adjustedDistancePulled by derivedStateOf { distancePulled * DRAG_MULTIPLIER }
private var _refreshing by mutableStateOf(false)
private var _position by mutableStateOf(0f)
private var distancePulled by mutableStateOf(0f)
private var _threshold by mutableStateOf(threshold)
private var refreshingOffsetState by mutableStateOf(refreshingOffset)
internal fun onPull(pullDelta: Float): Float {
if (_refreshing) return 0f // Already refreshing, do nothing.
val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f)
val dragConsumed = newOffset - distancePulled
distancePulled = newOffset
_position = calculateIndicatorPosition()
return dragConsumed
}
internal fun onRelease(velocity: Float): Float {
if (refreshing) return 0f // Already refreshing, do nothing
if (adjustedDistancePulled > threshold) {
onRefreshState.value()
}
animateIndicatorTo(0f)
val consumed =
when {
// We are flinging without having dragged the pull refresh (for example a fling inside
// a list) - don't consume
distancePulled == 0f -> 0f
// If the velocity is negative, the fling is upwards, and we don't want to prevent the
// the list from scrolling
velocity < 0f -> 0f
// We are showing the indicator, and the fling is downwards - consume everything
else -> velocity
}
distancePulled = 0f
return consumed
}
internal fun setRefreshing(refreshing: Boolean) {
if (_refreshing != refreshing) {
_refreshing = refreshing
distancePulled = 0f
animateIndicatorTo(if (refreshing) refreshingOffsetState else 0f)
}
}
internal fun setThreshold(threshold: Float) {
_threshold = threshold
}
internal fun setRefreshingOffset(refreshingOffset: Float) {
if (refreshingOffsetState != refreshingOffset) {
refreshingOffsetState = refreshingOffset
if (refreshing) animateIndicatorTo(refreshingOffset)
}
}
// Make sure to cancel any existing animations when we launch a new one. We use this instead of
// Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
// overhead of running through the animation pipeline instead of directly mutating the state.
private val mutatorMutex = MutatorMutex()
private fun animateIndicatorTo(offset: Float) =
animationScope.launch {
mutatorMutex.mutate {
animate(initialValue = _position, targetValue = offset) { value, _ -> _position = value }
}
}
private fun calculateIndicatorPosition(): Float =
when {
// If drag hasn't gone past the threshold, the position is the adjustedDistancePulled.
adjustedDistancePulled <= threshold -> adjustedDistancePulled
else -> {
// How far beyond the threshold pull has gone, as a percentage of the threshold.
val overshootPercent = abs(progress) - 1.0f
// Limit the overshoot to 200%. Linear between 0 and 200.
val linearTension = overshootPercent.coerceIn(0f, 2f)
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
val tensionPercent = linearTension - linearTension.pow(2) / 4
// The additional offset beyond the threshold.
val extraOffset = threshold * tensionPercent
threshold + extraOffset
}
}
}
/** Default parameter values for [rememberPullRefreshState]. */
object PullRefreshDefaults {
/**
* If the indicator is below this threshold offset when it is released, a refresh will be
* triggered.
*/
val RefreshThreshold = 80.dp
/** The offset at which the indicator should be rendered whilst a refresh is occurring. */
val RefreshingOffset = 56.dp
}
/**
* The distance pulled is multiplied by this value to give us the adjusted distance pulled, which is
* used in calculating the indicator position (when the adjusted distance pulled is less than the
* refresh threshold, it is the indicator position, otherwise the indicator position is derived from
* the progress).
*/
private const val DRAG_MULTIPLIER = 0.5f

View File

@@ -0,0 +1,131 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import android.app.Application
import android.content.Context
import android.os.Looper
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.amethyst.service.playback.VideoCache
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.ots.OpenTimestamps
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import kotlin.time.measureTimedValue
class Amethyst : Application() {
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun onTerminate() {
super.onTerminate()
applicationIOScope.cancel()
}
val videoCache: VideoCache by lazy {
val newCache = VideoCache()
runBlocking {
newCache.initFileCache(this@Amethyst)
}
newCache
}
val coilCache: DiskCache by lazy {
DiskCache
.Builder()
.directory(applicationContext.safeCacheDir.resolve("image_cache"))
.maxSizePercent(0.2)
.maximumMaxSizeBytes(1024 * 1024 * 1024) // 1GB
.build()
}
val coilMemCache: MemoryCache by lazy {
MemoryCache
.Builder(this)
.maxSizePercent(0.40) // memory heavy app due to profile pics and videos.
.build()
}
override fun onCreate() {
super.onCreate()
instance = this
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") {
StrictMode.setThreadPolicy(
ThreadPolicy
.Builder()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
VmPolicy
.Builder()
.detectAll()
.penaltyLog()
.build(),
)
Looper.getMainLooper().setMessageLogging(LogMonitor())
ChoreographerHelper.start()
}
GlobalScope.launch(Dispatchers.IO) {
val (value, elapsed) =
measureTimedValue {
// initializes the video cache in a thread
videoCache
}
Log.d("Rendering Metrics", "VideoCache initialized in $elapsed")
}
}
fun imageLoaderBuilder(): ImageLoader.Builder = ImageLoader.Builder(this).diskCache { coilCache }.memoryCache { coilMemCache }
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
companion object {
lateinit var instance: Amethyst
private set
}
}
internal val Context.safeCacheDir: File
get() {
val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" }
return cacheDir.apply { mkdirs() }
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class EncryptedStorage {
companion object {
private const val PREFERENCES_NAME = "secret_keeper"
// returns the preferences for each account or a global file if null.
fun prefsFileName(npub: String? = null): String = 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)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
applicationContext,
prefsFileName(npub),
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
) as EncryptedSharedPreferences
}
}
}

View File

@@ -0,0 +1,689 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.DefaultReactions
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal
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
import java.util.Locale
// 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,
)
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 RELAYS = "relays"
const val DONT_TRANSLATE_FROM = "dontTranslateFrom"
const val LOCAL_RELAY_SERVERS = "localRelayServers"
const val LANGUAGE_PREFS = "languagePreferences"
const val TRANSLATE_TO = "translateTo"
const val ZAP_AMOUNTS = "zapAmounts"
const val REACTION_CHOICES = "reactionChoices"
const val DEFAULT_ZAPTYPE = "defaultZapType"
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_CONTACT_LIST = "latestContactList"
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
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 USE_PROXY = "use_proxy"
const val PROXY_PORT = "proxy_port"
const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content"
const val WARN_ABOUT_REPORTS = "warn_about_reports"
const val FILTER_SPAM_FROM_STRANGERS = "filter_spam_from_strangers"
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 var savedAccounts: List<AccountInfo>? = null
private var cachedAccounts: MutableMap<String, Account?> = mutableMapOf()
suspend fun currentAccount(): String? {
if (currentAccount == null) {
currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
}
return currentAccount
}
private fun updateCurrentAccount(npub: String?) {
if (npub == null) {
currentAccount = null
encryptedPreferences().edit().clear().apply()
} else if (currentAccount != npub) {
currentAccount = npub
encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply()
}
}
private fun savedAccounts(): List<AccountInfo> {
if (savedAccounts == null) {
with(encryptedPreferences()) {
val newSystemOfAccounts =
getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let {
Event.mapper.readValue<List<AccountInfo>>(it)
}
if (!newSystemOfAccounts.isNullOrEmpty()) {
savedAccounts = 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(),
)
}
savedAccounts = migrated
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply()
}
}
}
return savedAccounts!!
}
private suspend fun updateSavedAccounts(accounts: List<AccountInfo>) =
withContext(Dispatchers.IO) {
if (savedAccounts != accounts) {
savedAccounts = accounts
encryptedPreferences()
.edit()
.apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(accounts)) }
.apply()
}
}
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(account: Account) =
withContext(Dispatchers.IO) {
val npub = account.userProfile().pubkeyNpub()
val accInfo =
AccountInfo(
npub,
account.isWriteable(),
account.signer is NostrSignerExternal,
)
updateCurrentAccount(npub)
addAccount(accInfo)
}
suspend fun switchToAccount(accountInfo: AccountInfo) = withContext(Dispatchers.IO) { updateCurrentAccount(accountInfo.npub) }
/** Removes the account from the app level shared preferences */
private suspend fun removeAccount(accountInfo: AccountInfo) {
val accounts = savedAccounts().filter { it.npub != accountInfo.npub }
updateSavedAccounts(accounts)
}
/** Deletes the npub-specific shared preference file */
private fun deleteUserPreferenceFile(npub: String) {
checkNotInMainThread()
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 {
return 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 updatePrefsForLogout(accountInfo: AccountInfo) {
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout")
withContext(Dispatchers.IO) {
val userPrefs = encryptedPreferences(accountInfo.npub)
userPrefs.edit().clear().commit()
removeAccount(accountInfo)
deleteUserPreferenceFile(accountInfo.npub)
if (savedAccounts().isEmpty()) {
updateCurrentAccount(null)
} else if (currentAccount() == accountInfo.npub) {
updateCurrentAccount(savedAccounts().elementAt(0).npub)
}
}
}
suspend fun updatePrefsForLogin(account: Account) {
setCurrentAccount(account)
saveToEncryptedStorage(account)
}
fun allSavedAccounts(): List<AccountInfo> = savedAccounts()
suspend fun saveToEncryptedStorage(account: Account) {
Log.d("LocalPreferences", "Saving to encrypted storage")
withContext(Dispatchers.IO) {
checkNotInMainThread()
val prefs = encryptedPreferences(account.userProfile().pubkeyNpub())
prefs
.edit()
.apply {
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.signer is NostrSignerExternal)
if (account.signer is NostrSignerExternal) {
remove(PrefKeys.NOSTR_PRIVKEY)
putString(PrefKeys.SIGNER_PACKAGE_NAME, account.signer.launcher.signerPackageName)
} else {
account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
}
account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(account.localRelays))
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, account.localRelayServers)
putString(
PrefKeys.LANGUAGE_PREFS,
Event.mapper.writeValueAsString(account.languagePreferences),
)
putString(PrefKeys.TRANSLATE_TO, account.translateTo)
putString(PrefKeys.ZAP_AMOUNTS, Event.mapper.writeValueAsString(account.zapAmountChoices))
putString(
PrefKeys.REACTION_CHOICES,
Event.mapper.writeValueAsString(account.reactionChoices),
)
putString(PrefKeys.DEFAULT_ZAPTYPE, account.defaultZapType.name)
putString(
PrefKeys.DEFAULT_FILE_SERVER,
Event.mapper.writeValueAsString(account.defaultFileServer),
)
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList.value)
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList.value)
putString(
PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST,
account.defaultNotificationFollowList.value,
)
putString(
PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST,
account.defaultDiscoveryFollowList.value,
)
putString(
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
Event.mapper.writeValueAsString(account.zapPaymentRequest),
)
if (account.backupContactList != null) {
putString(
PrefKeys.LATEST_CONTACT_LIST,
Event.mapper.writeValueAsString(account.backupContactList),
)
} else {
remove(PrefKeys.LATEST_CONTACT_LIST)
}
if (account.backupDMRelayList != null) {
putString(
PrefKeys.LATEST_DM_RELAY_LIST,
Event.mapper.writeValueAsString(account.backupDMRelayList),
)
} else {
remove(PrefKeys.LATEST_DM_RELAY_LIST)
}
if (account.backupNIP65RelayList != null) {
putString(
PrefKeys.LATEST_NIP65_RELAY_LIST,
Event.mapper.writeValueAsString(account.backupNIP65RelayList),
)
} else {
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
}
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, account.hideNIP17WarningDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
putBoolean(PrefKeys.USE_PROXY, account.proxy != null)
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, account.warnAboutPostsWithReports)
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, account.filterSpamFromStrangers)
putString(
PrefKeys.LAST_READ_PER_ROUTE,
Event.mapper.writeValueAsString(account.lastReadPerRoute),
)
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
if (account.showSensitiveContent.value == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent.value!!)
}
putString(
PrefKeys.PENDING_ATTESTATIONS,
Event.mapper.writeValueAsString(account.pendingAttestations.value),
)
}.apply()
}
}
suspend fun loadCurrentAccountFromEncryptedStorage(): Account? = currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
suspend fun saveSharedSettings(
sharedSettings: Settings,
prefs: SharedPreferences = encryptedPreferences(),
) {
Log.d("LocalPreferences", "Saving to shared settings")
with(prefs.edit()) {
putString(PrefKeys.SHARED_SETTINGS, Event.mapper.writeValueAsString(sharedSettings))
apply()
}
}
suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? {
Log.d("LocalPreferences", "Load shared settings")
with(prefs) {
return try {
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { Event.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,
)
e.printStackTrace()
null
}
}
}
val mutex = Mutex()
suspend fun loadCurrentAccountFromEncryptedStorage(npub: String): Account? =
withContext(Dispatchers.IO) {
mutex.withLock {
if (cachedAccounts.containsKey(npub)) {
return@withContext cachedAccounts.get(npub)
}
val account = innerLoadCurrentAccountFromEncryptedStorage(npub)
account?.registerObservers()
cachedAccounts.put(npub, account)
return@withContext account
}
}
suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): Account? {
Log.d("LocalPreferences", "Load account from file")
return withContext(Dispatchers.IO) {
checkNotInMainThread()
return@withContext with(encryptedPreferences(npub)) {
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
val loginWithExternalSigner = getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
val privKey = if (loginWithExternalSigner) null else getString(PrefKeys.NOSTR_PRIVKEY, null)
val localRelays =
getString(PrefKeys.RELAYS, "[]")?.let {
println("LocalRelays: $it")
Event.mapper.readValue<Set<RelaySetupInfo>?>(it)
}
?: setOf<RelaySetupInfo>()
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
val defaultHomeFollowList =
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_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 zapAmountChoices =
getString(PrefKeys.ZAP_AMOUNTS, "[]")
?.let { Event.mapper.readValue<List<Long>?>(it) }
?.ifEmpty { DefaultZapAmounts }
?: DefaultZapAmounts
val reactionChoices =
getString(PrefKeys.REACTION_CHOICES, "[]")
?.let { Event.mapper.readValue<List<String>?>(it) }
?.ifEmpty { DefaultReactions }
?: DefaultReactions
val defaultZapType =
getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
LnZapEvent.ZapType.values().firstOrNull { it.name == serverName }
}
?: LnZapEvent.ZapType.PUBLIC
val defaultFileServer =
try {
getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
Event.mapper.readValue<Nip96MediaServers.ServerName>(serverName)
}
?: Nip96MediaServers.DEFAULT[0]
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Failed to decode saved File Server", e)
e.printStackTrace()
Nip96MediaServers.DEFAULT[0]
}
val zapPaymentRequestServer =
try {
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
Event.mapper.readValue<Nip47WalletConnect.Nip47URI?>(it)
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}",
e,
)
e.printStackTrace()
null
}
val latestContactList =
try {
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
if (it != "null") {
println("Decoding Contact List: $it")
Event.fromJson(it) as ContactListEvent?
} else {
null
}
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}",
e,
)
null
}
val latestDmRelayList =
try {
getString(PrefKeys.LATEST_DM_RELAY_LIST, null)?.let {
if (it != "null") {
println("Decoding DM Relay List: $it")
Event.fromJson(it) as ChatMessageRelayListEvent?
} else {
null
}
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding DM Relay List ${getString(PrefKeys.LATEST_DM_RELAY_LIST, null)}",
e,
)
null
}
val latestNip65RelayList =
try {
getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)?.let {
if (it != "null") {
println("Decoding NIP65 Relay List: $it")
Event.fromJson(it) as AdvertisedRelayListEvent?
} else {
null
}
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding NIP65 Relay List ${getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)}",
e,
)
null
}
val pendingAttestations =
try {
getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let {
println("Decoding Attestation List: " + it)
if (it != null) {
Event.mapper.readValue<Map<HexKey, String>>(it)
} else {
null
}
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding Contact List ${getString(PrefKeys.PENDING_ATTESTATIONS, null)}",
e,
)
null
}
val languagePreferences =
try {
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
Event.mapper.readValue<Map<String, String>?>(it)
}
?: mapOf()
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}",
e,
)
e.printStackTrace()
mapOf()
}
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 useProxy = getBoolean(PrefKeys.USE_PROXY, false)
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort)
val showSensitiveContent =
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
} else {
null
}
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
val lastReadPerRoute =
try {
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
Event.mapper.readValue<Map<String, Long>?>(it)
}
?: mapOf()
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}",
e,
)
e.printStackTrace()
mapOf()
}
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
val signer =
if (loginWithExternalSigner) {
val packageName =
getString(PrefKeys.SIGNER_PACKAGE_NAME, null) ?: "com.greenart7c3.nostrsigner"
NostrSignerExternal(
pubKey,
ExternalSignerLauncher(pubKey.hexToByteArray().toNpub(), packageName),
)
} else {
NostrSignerInternal(keyPair)
}
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
val account =
Account(
keyPair = keyPair,
signer = signer,
localRelays = localRelays,
localRelayServers = localRelayServers,
dontTranslateFrom = dontTranslateFrom,
languagePreferences = languagePreferences,
translateTo = translateTo,
zapAmountChoices = zapAmountChoices,
reactionChoices = reactionChoices,
defaultZapType = defaultZapType,
defaultFileServer = defaultFileServer,
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
zapPaymentRequest = zapPaymentRequestServer,
hideDeleteRequestDialog = hideDeleteRequestDialog,
hideBlockAlertDialog = hideBlockAlertDialog,
hideNIP17WarningDialog = hideNIP17WarningDialog,
backupContactList = latestContactList,
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
proxy = proxy,
proxyPort = proxyPort,
showSensitiveContent = MutableStateFlow(showSensitiveContent),
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,
hasDonatedInVersion = hasDonatedInVersion,
pendingAttestations = MutableStateFlow(pendingAttestations ?: emptyMap()),
)
// Loads from DB
account.userProfile()
withContext(Dispatchers.Main) {
// Loads Live Objects
account.userProfile().live()
}
return@with account
}
}
}
}

View File

@@ -0,0 +1,198 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.util.Log
import android.util.Printer
import android.view.Choreographer
import java.text.SimpleDateFormat
import java.util.concurrent.atomic.AtomicBoolean
class LogMonitor : Printer {
private val mStackSampler: StackSampler
private var mPrintingStarted = false
private var mStartTimestamp: Long = 0
// threshold
private val mBlockThresholdMillis: Long = 16
// Sampling frequency
private val mSampleInterval: Long = 1000
private val mLogHandler: Handler
init {
mStackSampler = StackSampler(mSampleInterval)
val handlerThread = HandlerThread("block-canary-io")
handlerThread.start()
mLogHandler = Handler(handlerThread.getLooper())
}
override fun println(x: String) {
// From if to Else, execute DispatchMessage. If the execution takes more than the threshold, the output stuck information
if (!mPrintingStarted) {
// Record start time
mStartTimestamp = System.currentTimeMillis()
mPrintingStarted = true
mStackSampler.startDump()
} else {
val endTime = System.currentTimeMillis()
if (x.indexOf("com.vitorpamplona.amethyst") > 0) {
Log.d("block-canary", "Looper ${endTime - mStartTimestamp}ms for $x")
}
mPrintingStarted = false
//
if (isBlock(endTime)) {
notifyBlockEvent(endTime)
}
mStackSampler.stopDump()
}
}
private fun notifyBlockEvent(endTime: Long) {
mLogHandler.post {
// Obtain the stack of the main thread stack
val stacks: List<String> = mStackSampler.getStacks(mStartTimestamp, endTime)
for (stack in stacks) {
Log.e("block-canary", stack)
}
}
}
private fun isBlock(endTime: Long): Boolean = endTime - mStartTimestamp > mBlockThresholdMillis
}
class StackSampler(
private val mSampleInterval: Long,
) {
private val mHandler: Handler
private val mStackMap: MutableMap<Long, String> = LinkedHashMap()
private val mMaxCount = 100
// Whether to sample
var mShouldSample: AtomicBoolean = AtomicBoolean(false)
/**
* Start sampling and execute stack
*/
fun startDump() {
// Avoid repeating start
if (mShouldSample.get()) {
return
}
mShouldSample.set(true)
mHandler.removeCallbacks(mRunnable)
mHandler.postDelayed(mRunnable, mSampleInterval)
}
fun stopDump() {
if (!mShouldSample.get()) {
return
}
mShouldSample.set(false)
mHandler.removeCallbacks(mRunnable)
}
fun getStacks(
startTime: Long,
endTime: Long,
): List<String> {
val result = mutableListOf<String>()
synchronized(mStackMap) {
for (entryTime in mStackMap.keys) {
if (startTime < entryTime && entryTime < endTime) {
result.add(
TIME_FORMATTER.format(entryTime) +
SEPARATOR +
SEPARATOR +
mStackMap[entryTime],
)
}
}
}
return result
}
init {
val handlerThread = HandlerThread("block-canary-sampler")
handlerThread.start()
mHandler = Handler(handlerThread.looper)
}
private val mRunnable: Runnable =
object : Runnable {
override fun run() {
val sb = StringBuilder()
val stackTrace = Looper.getMainLooper().thread.stackTrace
for (s in stackTrace) {
sb.append(s.toString()).append("\n")
}
synchronized(mStackMap) {
// Save up to 100 stack information
if (mStackMap.size == mMaxCount) {
mStackMap.remove(mStackMap.keys.iterator().next())
}
mStackMap.put(System.currentTimeMillis(), sb.toString())
}
if (mShouldSample.get()) {
mHandler.postDelayed(this, mSampleInterval)
}
}
}
companion object {
const val SEPARATOR: String = "\r\n"
val TIME_FORMATTER: SimpleDateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS")
}
}
object ChoreographerHelper {
var lastFrameTimeNanos: Long = 0
fun start() {
Choreographer.getInstance().postFrameCallback(
object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
// Last callback time
if (lastFrameTimeNanos == 0L) {
lastFrameTimeNanos = frameTimeNanos
Choreographer.getInstance().postFrameCallback(this)
return
}
val diff = (frameTimeNanos - lastFrameTimeNanos) / 1000000
// only report after 30ms because videos play at 30fps
if (diff > 35) {
// Follow the frame number
val droppedCount = (diff / 16.6).toInt()
Log.w("block-canary", "Dropped $droppedCount frames. Skipped $diff ms")
}
lastFrameTimeNanos = frameTimeNanos
Choreographer.getInstance().postFrameCallback(this)
}
},
)
}
}

View File

@@ -0,0 +1,252 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import android.os.Build
import android.util.Log
import androidx.compose.runtime.Stable
import coil.Coil
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.size.Precision
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@Stable
class ServiceManager {
private var isStarted: Boolean =
false // to not open amber in a loop trying to use auth relays and registering for notifications
private var account: Account? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var collectorJob: Job? = null
private fun start(account: Account) {
this.account = account
start()
}
private fun start() {
Log.d("ServiceManager", "Pre Starting Relay Services $isStarted $account")
if (isStarted && account != null) {
return
}
Log.d("ServiceManager", "Starting Relay Services")
val myAccount = account
// Resets Proxy Use
HttpClientManager.setDefaultProxy(account?.proxy)
HttpClientManager.setDefaultUserAgent("Amethyst/${BuildConfig.VERSION_NAME}")
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
Coil.setImageLoader {
Amethyst.instance
.imageLoaderBuilder()
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(SvgDecoder.Factory())
} // .logger(DebugLogger())
.okHttpClient { HttpClientManager.getHttpClient() }
.precision(Precision.INEXACT)
.respectCacheHeaders(false)
.build()
}
if (myAccount != null) {
val relaySet = myAccount.connectToRelays.value
Log.d("Relay", "Service Manager Connect Connecting ${relaySet.size}")
Client.reconnect(relaySet)
collectorJob?.cancel()
collectorJob = null
collectorJob =
scope.launch {
myAccount.connectToRelaysFlow.collectLatest {
delay(500)
if (isStarted) {
Client.reconnect(it, onlyIfChanged = true)
}
}
}
// start services
NostrAccountDataSource.account = myAccount
NostrAccountDataSource.otherAccounts =
LocalPreferences.allSavedAccounts().mapNotNull {
try {
it.npub.bechToBytes().toHexKey()
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
}
NostrHomeDataSource.account = myAccount
NostrChatroomListDataSource.account = myAccount
NostrVideoDataSource.account = myAccount
NostrDiscoveryDataSource.account = myAccount
// Notification Elements
NostrHomeDataSource.start()
NostrAccountDataSource.start()
GlobalScope.launch(Dispatchers.IO) {
delay(3000)
NostrChatroomListDataSource.start()
NostrDiscoveryDataSource.start()
NostrVideoDataSource.start()
}
// More Info Data Sources
NostrSingleEventDataSource.start()
NostrSingleChannelDataSource.start()
NostrSingleUserDataSource.start()
isStarted = true
}
}
private fun pause() {
Log.d("ServiceManager", "Pausing Relay Services")
collectorJob?.cancel()
collectorJob = null
NostrAccountDataSource.stopSync()
NostrHomeDataSource.stopSync()
NostrChannelDataSource.stopSync()
NostrChatroomDataSource.stopSync()
NostrChatroomListDataSource.stopSync()
NostrDiscoveryDataSource.stopSync()
NostrCommunityDataSource.stopSync()
NostrHashtagDataSource.stopSync()
NostrGeohashDataSource.stopSync()
NostrSearchEventOrUserDataSource.stopSync()
NostrSingleChannelDataSource.stopSync()
NostrSingleEventDataSource.stopSync()
NostrSingleUserDataSource.stopSync()
NostrThreadDataSource.stopSync()
NostrUserProfileDataSource.stopSync()
NostrVideoDataSource.stopSync()
Client.reconnect(null)
isStarted = false
}
fun cleanObservers() {
LocalCache.cleanObservers()
}
fun trimMemory() {
LocalCache.cleanObservers()
val accounts =
LocalPreferences.allSavedAccounts().mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet()
account?.let {
LocalCache.pruneOldAndHiddenMessages(it)
NostrChatroomDataSource.clearEOSEs(it)
LocalCache.pruneHiddenMessages(it)
LocalCache.pruneContactLists(accounts)
LocalCache.pruneRepliesAndReactions(accounts)
LocalCache.prunePastVersionsOfReplaceables()
LocalCache.pruneExpiredEvents()
}
}
// This method keeps the pause/start in a Syncronized block to
// avoid concurrent pauses and starts.
@Synchronized
fun forceRestart(
account: Account? = null,
start: Boolean = true,
pause: Boolean = true,
) {
if (pause) {
pause()
}
if (start) {
if (account != null) {
start(account)
} else {
start()
}
}
}
fun restartIfDifferentAccount(account: Account) {
if (this.account != account) {
forceRestart(account, true, true)
}
}
fun forceRestart() {
forceRestart(null, true, true)
}
fun justStart() {
forceRestart(null, true, false)
}
fun pauseForGood() {
forceRestart(null, false, true)
}
fun pauseForGoodAndClearAccount() {
account = null
forceRestart(null, false, true)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayStats
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.events.Event
import kotlinx.coroutines.Dispatchers
data class Spammer(
val pubkeyHex: HexKey,
var duplicatedMessages: Set<HexKey>,
)
class AntiSpamFilter {
val recentMessages = LruCache<Int, String>(1000)
val spamMessages = LruCache<Int, Spammer>(1000)
var active: Boolean = true
fun isSpam(
event: Event,
relay: Relay?,
): Boolean {
checkNotInMainThread()
if (!active) return false
val idHex = event.id
// if short message, ok
// The idea here is to avoid considering repeated "GM" messages spam.
if (event.content.length < 50) 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 && Nip19Bech32.nip19regex.matcher(event.content).find()) return false
// double list strategy:
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
// Considers tags so that same replies to different people don't count.
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
if (
(recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null
) {
Log.w(
"Potential SPAM Message",
"${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}",
)
// Log down offenders
logOffender(hash, event)
if (relay != null) {
RelayStats.newSpam(relay.url, "https://njump.me/${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, relay.url)}")
}
liveSpam.invalidateData()
return true
}
recentMessages.put(hash, idHex)
return false
}
@Synchronized
private fun logOffender(
hashCode: Int,
event: Event,
) {
if (spamMessages.get(hashCode) == null) {
spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id)))
} else {
val spammer = spamMessages.get(hashCode)
spammer.duplicatedMessages += event.id
}
}
val liveSpam: AntiSpamLiveData = AntiSpamLiveData(this)
}
@Stable
class AntiSpamLiveData(
val cache: AntiSpamFilter,
) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateData() {
checkNotInMainThread()
bundler.invalidate {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(AntiSpamState(cache))
}
}
}
}
class AntiSpamState(
val cache: AntiSpamFilter,
)

View File

@@ -0,0 +1,200 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.commons.data.LargeCache
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import kotlinx.coroutines.Dispatchers
@Stable
class PublicChatChannel(
idHex: String,
) : Channel(idHex) {
var info = ChannelCreateEvent.ChannelData(null, null, null)
fun updateChannelInfo(
creator: User,
channelInfo: ChannelCreateEvent.ChannelData,
updatedAt: Long,
) {
this.info = channelInfo
super.updateChannelInfo(creator, updatedAt)
}
override fun toBestDisplayName(): String = info.name ?: super.toBestDisplayName()
override fun summary(): String? = info.about
override fun profilePicture(): String? {
if (info.picture.isNullOrBlank()) return super.profilePicture()
return info.picture ?: super.profilePicture()
}
override fun anyNameStartsWith(prefix: String): Boolean = listOfNotNull(info.name, info.about).filter { it.contains(prefix, true) }.isNotEmpty()
}
@Stable
class LiveActivitiesChannel(
val address: ATag,
) : Channel(address.toTag()) {
var info: LiveActivitiesEvent? = null
override fun idNote() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
fun address() = address
fun updateChannelInfo(
creator: User,
channelInfo: LiveActivitiesEvent,
updatedAt: Long,
) {
this.info = channelInfo
super.updateChannelInfo(creator, updatedAt)
}
override fun toBestDisplayName(): String = info?.title() ?: super.toBestDisplayName()
override fun summary(): String? = info?.summary()
override fun profilePicture(): String? = info?.image()?.ifBlank { null }
override fun anyNameStartsWith(prefix: String): Boolean =
listOfNotNull(info?.title(), info?.summary())
.filter { it.contains(prefix, true) }
.isNotEmpty()
}
@Stable
abstract class Channel(
val idHex: String,
) {
var creator: User? = null
var updatedMetadataAt: Long = 0
val notes = LargeCache<HexKey, Note>()
var lastNoteCreatedAt: Long = 0
open fun id() = Hex.decode(idHex)
open fun idNote() = id().toNote()
open fun idDisplayNote() = idNote().toShortenHex()
open fun toBestDisplayName(): String = idDisplayNote()
open fun summary(): String? = null
open fun creatorName(): String? = creator?.toBestDisplayName()
open fun profilePicture(): String? = creator?.info?.banner
open fun updateChannelInfo(
creator: User,
updatedAt: Long,
) {
this.creator = creator
this.updatedMetadataAt = updatedAt
live.invalidateData()
}
fun addNote(note: Note) {
notes.put(note.idHex, note)
if ((note.createdAt() ?: 0) > lastNoteCreatedAt) {
lastNoteCreatedAt = note.createdAt() ?: 0
}
}
fun removeNote(note: Note) {
notes.remove(note.idHex)
}
fun removeNote(noteHex: String) {
notes.remove(noteHex)
}
abstract fun anyNameStartsWith(prefix: String): Boolean
// Observers line up here.
val live: ChannelLiveData = ChannelLiveData(this)
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
val important =
notes
.filter { key, it ->
it.author?.let { author -> account.isHidden(author) } == false
}.sortedWith(DefaultFeedOrder)
.take(500)
.toSet()
val toBeRemoved = notes.filter { key, it -> it !in important }
toBeRemoved.forEach { notes.remove(it.idHex) }
return toBeRemoved.toSet()
}
}
class ChannelLiveData(
val channel: Channel,
) : LiveData<ChannelState>(ChannelState(channel)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateData() {
checkNotInMainThread()
bundler.invalidate {
checkNotInMainThread()
if (hasActiveObservers()) {
postValue(ChannelState(channel))
}
}
}
override fun onActive() {
super.onActive()
NostrSingleChannelDataSource.add(channel)
}
override fun onInactive() {
super.onInactive()
NostrSingleChannelDataSource.remove(channel)
}
}
class ChannelState(
val channel: Channel,
)

View File

@@ -0,0 +1,93 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.utils.TimeUtils
@Stable
class Chatroom {
var authors: Set<User> = setOf()
var roomMessages: Set<Note> = setOf()
var subject: String? = null
var subjectCreatedAt: Long? = null
@Synchronized
fun addMessageSync(msg: Note) {
checkNotInMainThread()
if (msg !in roomMessages) {
roomMessages = roomMessages + msg
msg.author?.let { author ->
if (author !in authors) {
authors += author
}
}
val newSubject = msg.event?.subject()
if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) {
subject = newSubject
subjectCreatedAt = msg.createdAt()
}
}
}
@Synchronized
fun removeMessageSync(msg: Note) {
checkNotInMainThread()
if (msg in roomMessages) {
roomMessages = roomMessages - msg
roomMessages
.filter { it.event?.subject() != null }
.sortedBy { it.createdAt() }
.lastOrNull()
?.let {
subject = it.event?.subject()
subjectCreatedAt = it.createdAt()
}
}
}
fun senderIntersects(keySet: Set<HexKey>): Boolean = authors.any { it.pubkeyHex in keySet }
fun pruneMessagesToTheLatestOnly(): Set<Note> {
val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
val toKeep =
if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) {
// Recent messages, keep last 100
sorted.take(100).toSet()
} else {
// Old messages, keep the last one.
sorted.take(1).toSet()
} + sorted.filter { it.liveSet?.isInUse() ?: false }
val toRemove = roomMessages.minus(toKeep)
roomMessages = toKeep
return toRemove
}
}

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.hashtags.Amethyst
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.Footstr
import com.vitorpamplona.amethyst.commons.hashtags.Grownostr
import com.vitorpamplona.amethyst.commons.hashtags.Lightning
import com.vitorpamplona.amethyst.commons.hashtags.Mate
import com.vitorpamplona.amethyst.commons.hashtags.Nostr
import com.vitorpamplona.amethyst.commons.hashtags.Plebs
import com.vitorpamplona.amethyst.commons.hashtags.Skull
import com.vitorpamplona.amethyst.commons.hashtags.Tunestr
import com.vitorpamplona.amethyst.commons.hashtags.Weed
import com.vitorpamplona.amethyst.commons.hashtags.Zap
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.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.events.EmptyTagList
@Preview
@Composable
fun RenderHashTagIcons() {
val nav: (String) -> Unit = {}
ThemeComparisonColumn {
RenderRegular(
"Testing rendering of hashtags: #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate",
EmptyTagList,
) { word, state ->
when (word) {
is HashTagSegment -> HashTag(word, nav)
is RegularTextSegment -> Text(word.segmentText)
}
}
}
}
fun checkForHashtagWithIcon(tag: String): HashtagIcon? =
when (tag.lowercase()) {
"₿itcoin", "bitcoin", "btc", "timechain", "bitcoiner", "bitcoiners" -> bitcoin
"nostr", "nostrich", "nostriches", "thenostr" -> nostr
"lightning", "lightningnetwork" -> lightning
"zap", "zaps", "zapper", "zappers", "zapping", "zapped", "zapathon", "zapraiser", "zaplife", "zapchain" -> zap
"amethyst" -> amethyst
"cashu", "ecash", "nut", "nuts", "deeznuts" -> cashu
"plebs", "pleb", "plebchain" -> plebs
"coffee", "coffeechain", "cafe" -> coffee
"skullofsatoshi" -> skull
"grownostr", "gardening", "garden" -> growstr
"footstr" -> footstr
"tunestr", "music", "nowplaying" -> tunestr
"mate", "matechain", "matestr" -> matestr
"weed", "weedstr", "420", "cannabis", "marijuana" -> weed
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))
val lightning = HashtagIcon(CustomHashTagIcons.Lightning, "Lightning", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
val zap = HashtagIcon(CustomHashTagIcons.Zap, "Zap", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
val amethyst = HashtagIcon(CustomHashTagIcons.Amethyst, "Amethyst", Modifier.padding(start = 2.dp, bottom = 1.dp, top = 1.dp))
val cashu = HashtagIcon(CustomHashTagIcons.Cashu, "Cashu", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
val plebs = HashtagIcon(CustomHashTagIcons.Plebs, "Pleb", Modifier.padding(start = 2.dp, bottom = 1.dp, top = 1.dp))
val coffee = HashtagIcon(CustomHashTagIcons.Coffee, "Coffee", Modifier.padding(start = 3.dp, bottom = 1.dp, top = 1.dp))
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 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))
@Immutable
class HashtagIcon(
val icon: ImageVector,
val description: String,
val modifier: Modifier = Modifier,
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,159 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import android.util.Log
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "resourceType",
)
@JsonSubTypes(
JsonSubTypes.Type(value = Practitioner::class, name = "Practitioner"),
JsonSubTypes.Type(value = Patient::class, name = "Patient"),
JsonSubTypes.Type(value = Bundle::class, name = "Bundle"),
JsonSubTypes.Type(value = VisionPrescription::class, name = "VisionPrescription"),
)
open class Resource(
var resourceType: String? = null,
var id: String = "",
)
class Practitioner(
resourceType: String? = null,
id: String = "",
var active: Boolean? = null,
var name: ArrayList<HumanName> = arrayListOf(),
var gender: String? = null,
) : Resource(resourceType, id)
class Patient(
resourceType: String? = null,
id: String = "",
var active: Boolean? = null,
var name: ArrayList<HumanName> = arrayListOf(),
var gender: String? = null,
) : Resource(resourceType, id)
class HumanName(
var use: String? = null,
var family: String? = null,
var given: ArrayList<String> = arrayListOf(),
) {
fun assembleName(): String = given.joinToString(" ") + " " + family
}
class Bundle(
resourceType: String? = null,
id: String = "",
var type: String? = null,
var created: String? = null,
var entry: List<Resource> = arrayListOf(),
) : Resource(resourceType, id)
class VisionPrescription(
resourceType: String? = null,
id: String = "",
var status: String? = null,
var created: String? = null,
var patient: Reference? = Reference(),
var encounter: Reference? = Reference(),
var dateWritten: String? = null,
var prescriber: Reference? = Reference(),
var lensSpecification: List<LensSpecification> = arrayListOf(),
) : Resource(resourceType, id)
class LensSpecification(
var eye: String? = null,
var sphere: Double? = null,
var cylinder: Double? = null,
var axis: Double? = null,
var add: Double? = null,
var prism: List<Prism> = arrayListOf(),
// contact lenses
var power: Double? = null,
var backCurve: Double? = null,
var diameter: Double? = null,
var color: String? = null,
var brand: String? = null,
var note: String? = null,
)
class Prism(
var amount: Double? = null,
var base: String? = null,
)
class Reference(
var reference: String? = null,
)
data class FhirElementDatabase(
var baseResource: Resource? = null,
var localDb: ImmutableMap<String, Resource> = persistentMapOf(),
)
fun findReferenceInDb(
it: String,
db: Map<String, Resource>,
): Resource? {
val parts = it.split("/")
return if (parts.size == 2) {
db.get(parts[1].removePrefix("#"))
} else {
db.get(it.removePrefix("#"))
}
}
fun parseResourceBundleOrNull(json: String): FhirElementDatabase? {
val mapper =
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return try {
val resource = mapper.readValue(json, Resource::class.java)
val db =
when (resource) {
is Bundle -> {
resource.entry.associateBy { it.id }.toImmutableMap()
}
else -> {
persistentMapOf(resource.id to resource)
}
}
FhirElementDatabase(
localDb = db,
baseResource = resource,
)
} catch (e: Exception) {
Log.e("RenderEyeGlassesPrescription", "Parser error", e)
null
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.quartz.encoders.HexKey
class ParticipantListBuilder {
private fun addFollowsThatDirectlyParticipateOnToSet(
baseNote: Note,
followingSet: Set<HexKey>?,
set: MutableSet<User>,
) {
baseNote.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) {
set.add(author)
}
}
// Breaks these searchers down to avoid the memory use of creating multiple lists
baseNote.replies.forEach { reply ->
reply.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) {
set.add(author)
}
}
}
baseNote.boosts.forEach { boost ->
boost.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) {
set.add(author)
}
}
}
baseNote.zaps.forEach { zapPair ->
zapPair.key.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) {
set.add(author)
}
}
}
baseNote.reactions.forEach { reactionSet ->
reactionSet.value.forEach { reaction ->
reaction.author?.let { author ->
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) {
set.add(author)
}
}
}
}
}
fun followsThatParticipateOnDirect(
baseNote: Note?,
followingSet: Set<HexKey>?,
): Set<User> {
if (baseNote == null) return mutableSetOf()
val set = mutableSetOf<User>()
addFollowsThatDirectlyParticipateOnToSet(baseNote, followingSet, set)
return set
}
fun followsThatParticipateOn(
baseNote: Note?,
followingSet: Set<HexKey>?,
): Set<User> {
if (baseNote == null) return mutableSetOf()
val mySet = mutableSetOf<User>()
addFollowsThatDirectlyParticipateOnToSet(baseNote, followingSet, mySet)
baseNote.replies.forEach { addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) }
baseNote.boosts.forEach {
it.replyTo?.forEach { addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) }
}
LocalCache.getChannelIfExists(baseNote.idHex)?.notes?.forEach { key, it ->
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
}
return mySet
}
fun countFollowsThatParticipateOn(
baseNote: Note?,
followingSet: Set<HexKey>?,
): Int {
if (baseNote == null) return 0
val list = followsThatParticipateOn(baseNote, followingSet)
return list.size
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.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,
)
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),
}
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
}
}
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
}
}

View File

@@ -0,0 +1,182 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.RepostEvent
import kotlin.time.measureTimedValue
class ThreadAssembler {
private fun searchRoot(
note: Note,
testedNotes: MutableSet<Note> = mutableSetOf(),
): Note? {
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
if (note.event is RepostEvent || note.event is GenericRepostEvent) return note
testedNotes.add(note)
val markedAsRoot =
note.event
?.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?.reversed()?.firstOrNull { it.replyTo?.isEmpty() == true }
if (hasNoReplyTo != null) return hasNoReplyTo
// recursive
val roots =
note.replyTo
?.map {
if (it !in testedNotes) {
searchRoot(it, testedNotes)
} else {
null
}
}?.filterNotNull()
if (roots != null && roots.isNotEmpty()) {
return roots[0]
}
return null
}
fun findThreadFor(noteId: String): Set<Note> {
checkNotInMainThread()
val (result, elapsed) =
measureTimedValue {
val note = LocalCache.checkGetOrCreateNote(noteId) ?: return emptySet<Note>()
if (note.event != null) {
val thread = OnlyLatestVersionSet()
val threadRoot = searchRoot(note, thread) ?: note
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) }
thread
} else {
setOf(note)
}
}
println("Model Refresh: Thread loaded in $elapsed")
return result
}
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<ATag, 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: ATag,
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,128 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.RepostEvent
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
if (
note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()
) {
return LevelSignature(
signature = "/" + formattedDateTime(note.createdAt() ?: 0) + note.idHex.substring(0, 8) + ";",
createdAt = note.createdAt(),
author = note.author,
)
}
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 (parent?.author == note.author && note.createdAt() != null) {
// author of the thread first, in **ascending** order
"9" +
formattedDateTime((parent?.createdAt ?: 0) + (now - (note.createdAt() ?: 0))) +
note.idHex.substring(0, 8)
} else if (note.author?.pubkeyHex == account.pubkeyHex) {
"8" + formattedDateTime(note.createdAt() ?: 0) + note.idHex.substring(0, 8) // my replies
} else if (note.author?.pubkeyHex in accountFollowingSet) {
"7" + formattedDateTime(note.createdAt() ?: 0) + note.idHex.substring(0, 8) // my follows replies.
} else {
"0" + formattedDateTime(note.createdAt() ?: 0) + note.idHex.substring(0, 8) // everyone else.
}
val mySignature =
LevelSignature(
signature = parentSignature + "/" + threadOrder + ";",
createdAt = note.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()
) {
return 0
}
return replyTo.maxOf {
cachedLevels[it] ?: replyLevel(it, cachedLevels).apply { cachedLevels.put(it, this) }
} + 1
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.previews.BahaUrlPreview
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
@Stable
object UrlCachedPreviewer {
var cache = LruCache<String, UrlPreviewState>(100)
private set
suspend fun previewInfo(
url: String,
onReady: suspend (UrlPreviewState) -> Unit,
) {
cache[url]?.let {
onReady(it)
return
}
BahaUrlPreview(
url,
object : IUrlPreviewCallback {
override suspend fun onComplete(urlInfo: UrlInfoItem) {
cache[url]?.let {
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
onReady(it)
return
}
}
val state =
if (urlInfo.fetchComplete() && urlInfo.url == url) {
UrlPreviewState.Loaded(urlInfo)
} else {
UrlPreviewState.Empty
}
cache.put(url, state)
onReady(state)
}
override suspend fun onFailed(throwable: Throwable) {
cache[url]?.let {
onReady(it)
return
}
val state = UrlPreviewState.Error(throwable.message ?: "Error Loading url preview")
cache.put(url, state)
onReady(state)
}
},
).fetchUrlPreview()
}
}

View File

@@ -1,3 +1,23 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
@@ -7,14 +27,16 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.distinctUntilChanged
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ContactListEvent
@@ -25,12 +47,17 @@ import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import java.math.BigDecimal
@Stable
class User(val pubkeyHex: String) {
class User(
val pubkeyHex: String,
) {
var info: UserMetadata? = null
var latestMetadata: MetadataEvent? = null
var latestMetadataRelay: String? = null
var latestContactList: ContactListEvent? = null
var latestBookmarkList: BookmarkListEvent? = null
@@ -49,49 +76,45 @@ class User(val pubkeyHex: String) {
private set
fun pubkey() = Hex.decode(pubkeyHex)
fun pubkeyNpub() = pubkey().toNpub()
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
fun toNostrUri() = "nostr:${pubkeyNpub()}"
fun toNProfile(): String {
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)?.writeRelays()
return Nip19Bech32.createNProfile(
pubkeyHex,
relayList?.take(3) ?: listOfNotNull(latestMetadataRelay),
)
}
fun toNostrUri() = "nostr:${toNProfile()}"
override fun toString(): String = pubkeyHex
fun toBestShortFirstName(): String {
val fullName = bestDisplayName() ?: bestUsername() ?: return pubkeyDisplayHex()
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]
}
val firstName =
if (names[0].length <= 3) {
// too short. Remove Dr.
"${names[0]} ${names.getOrNull(1) ?: ""}"
} else {
names[0]
}
return firstName
}
fun toBestDisplayName(): String {
return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex()
}
fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex()
fun bestUsername(): String? {
return info?.name?.ifBlank { null } ?: info?.username?.ifBlank { null }
}
fun nip05(): String? = info?.nip05
fun bestDisplayName(): String? {
return info?.displayName?.ifBlank { null } ?: info?.display_name?.ifBlank { null }
}
fun nip05(): String? {
return info?.nip05?.ifBlank { null }
}
fun profilePicture(): String? {
if (info?.picture.isNullOrBlank()) info?.picture = null
return info?.picture
}
fun profilePicture(): String? = info?.picture
fun updateBookmark(event: BookmarkListEvent) {
if (event.id == latestBookmarkList?.id) return
@@ -112,27 +135,38 @@ class User(val pubkeyHex: String) {
// Update following of the current user
liveSet?.innerFollows?.invalidateData()
flowSet?.follows?.invalidateData()
// Update Followers of the past user list
// Update Followers of the new contact list
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
LocalCache
.getUserIfExists(it)
?.liveSet
?.innerFollowers
?.invalidateData()
}
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
LocalCache
.getUserIfExists(it)
?.liveSet
?.innerFollowers
?.invalidateData()
}
liveSet?.innerRelays?.invalidateData()
flowSet?.relays?.invalidateData()
}
fun addReport(note: Note) {
val author = note.author ?: return
if (author !in reports.keys) {
val reportsBy = reports[author]
if (reportsBy == null) {
reports = reports + Pair(author, setOf(note))
liveSet?.innerReports?.invalidateData()
} else if (reports[author]?.contains(note) == false) {
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
} else if (!reportsBy.contains(note)) {
reports = reports + Pair(author, reportsBy + note)
liveSet?.innerReports?.invalidateData()
}
}
@@ -140,7 +174,7 @@ class User(val pubkeyHex: String) {
fun removeReport(deleteNote: Note) {
val author = deleteNote.author ?: return
if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
if (reports[author]?.contains(deleteNote) == true) {
reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote))
liveSet?.innerReports?.invalidateData()
@@ -148,61 +182,62 @@ class User(val pubkeyHex: String) {
}
}
fun addZap(zapRequest: Note, zap: Note?) {
if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap)
liveSet?.innerZaps?.invalidateData()
} else if (zapRequest in zaps.keys && zaps[zapRequest] == null) {
fun addZap(
zapRequest: Note,
zap: Note?,
) {
if (zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap)
liveSet?.innerZaps?.invalidateData()
}
}
fun removeZap(zapRequestOrZapEvent: Note) {
if (zapRequestOrZapEvent in zaps.keys) {
if (zaps.containsKey(zapRequestOrZapEvent)) {
zaps = zaps.minus(zapRequestOrZapEvent)
liveSet?.innerZaps?.invalidateData()
} else if (zapRequestOrZapEvent in zaps.values) {
} else if (zaps.containsValue(zapRequestOrZapEvent)) {
zaps = zaps.filter { it.value != zapRequestOrZapEvent }
liveSet?.innerZaps?.invalidateData()
}
}
fun zappedAmount(): BigDecimal {
return zaps.mapNotNull { it.value?.event }
.filterIsInstance<LnZapEvent>()
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 {
it.amount
}.sumOf { it }
}
fun reportsBy(user: User): Set<Note> {
return reports[user] ?: emptySet()
}
fun reportAuthorsBy(users: Set<HexKey>): List<User> {
return reports.keys.filter { it.pubkeyHex in users }
}
fun countReportAuthorsBy(users: Set<HexKey>): Int {
return reports.keys.count { it.pubkeyHex in users }
}
fun reportsBy(users: Set<HexKey>): List<Note> {
return reportAuthorsBy(users).mapNotNull {
reports[it]
}.flatten()
}
if (it.key.pubkeyHex in users) {
it.value
} else {
null
}
}.flatten()
@Synchronized
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom {
checkNotInMainThread()
return privateChatrooms[key] ?: run {
val privateChatroom = Chatroom()
privateChatrooms = privateChatrooms + Pair(key, privateChatroom)
privateChatroom
}
return privateChatrooms[key]
?: run {
val privateChatroom = Chatroom()
privateChatrooms = privateChatrooms + Pair(key, privateChatroom)
privateChatroom
}
}
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
@@ -210,11 +245,12 @@ class User(val pubkeyHex: String) {
return getOrCreatePrivateChatroom(key)
}
private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom {
return privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
}
private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
fun addMessage(room: ChatroomKey, msg: Note) {
fun addMessage(
room: ChatroomKey,
msg: Note,
) {
val privateChatroom = getOrCreatePrivateChatroom(room)
if (msg !in privateChatroom.roomMessages) {
privateChatroom.addMessageSync(msg)
@@ -222,7 +258,10 @@ class User(val pubkeyHex: String) {
}
}
fun addMessage(user: User, msg: Note) {
fun addMessage(
user: User,
msg: Note,
) {
val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg !in privateChatroom.roomMessages) {
privateChatroom.addMessageSync(msg)
@@ -234,7 +273,10 @@ class User(val pubkeyHex: String) {
getOrCreatePrivateChatroom(withKey)
}
fun removeMessage(user: User, msg: Note) {
fun removeMessage(
user: User,
msg: Note,
) {
checkNotInMainThread()
val privateChatroom = getOrCreatePrivateChatroom(user)
@@ -244,7 +286,22 @@ class User(val pubkeyHex: String) {
}
}
fun addRelayBeingUsed(relay: Relay, eventTime: Long) {
fun removeMessage(
room: ChatroomKey,
msg: Note,
) {
checkNotInMainThread()
val privateChatroom = getOrCreatePrivateChatroom(room)
if (msg in privateChatroom.roomMessages) {
privateChatroom.removeMessageSync(msg)
liveSet?.innerMessages?.invalidateData()
}
}
fun addRelayBeingUsed(
relay: Relay,
eventTime: Long,
) {
val here = relaysBeingUsed[relay.url]
if (here == null) {
relaysBeingUsed = relaysBeingUsed + Pair(relay.url, RelayInfo(relay.url, eventTime, 1))
@@ -258,11 +315,13 @@ class User(val pubkeyHex: String) {
liveSet?.innerRelayInfo?.invalidateData()
}
fun updateUserInfo(newUserInfo: UserMetadata, latestMetadata: MetadataEvent) {
fun updateUserInfo(
newUserInfo: UserMetadata,
latestMetadata: MetadataEvent,
) {
info = newUserInfo
info?.latestMetadata = latestMetadata
info?.updatedMetadataAt = latestMetadata.createdAt
info?.tags = latestMetadata.tags.toImmutableListOfLists()
info?.cleanBlankNames()
if (newUserInfo.lud16.isNullOrBlank()) {
info?.lud06?.let {
@@ -275,105 +334,149 @@ class User(val pubkeyHex: String) {
liveSet?.innerMetadata?.invalidateData()
}
fun isFollowing(user: User): Boolean {
return latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
}
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
fun isFollowingHashtag(tag: String): Boolean {
return latestContactList?.isTaggedHash(tag) ?: false
}
fun isFollowingHashtag(tag: String): Boolean = latestContactList?.isTaggedHash(tag) ?: false
fun isFollowingHashtagCached(tag: String): Boolean {
return latestContactList?.verifiedFollowTagSet?.let {
return tag.lowercase() in it
} ?: false
}
?: false
}
fun isFollowingGeohashCached(geoTag: String): Boolean {
return latestContactList?.verifiedFollowGeohashSet?.let {
return geoTag.lowercase() in it
} ?: false
}
?: false
}
fun isFollowingCached(user: User): Boolean {
return latestContactList?.verifiedFollowKeySet?.let {
return user.pubkeyHex in it
} ?: false
}
?: false
}
fun isFollowingCached(userHex: String): Boolean {
return latestContactList?.verifiedFollowKeySet?.let {
return userHex in it
} ?: false
}
?: false
}
fun transientFollowCount(): Int? {
return latestContactList?.unverifiedFollowKeySet()?.size
}
fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size
suspend fun transientFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
fun cachedFollowingKeySet(): Set<HexKey> {
return latestContactList?.verifiedFollowKeySet ?: emptySet()
}
fun cachedFollowingKeySet(): Set<HexKey> = latestContactList?.verifiedFollowKeySet ?: emptySet()
fun cachedFollowingTagSet(): Set<String> {
return latestContactList?.verifiedFollowTagSet ?: emptySet()
}
fun cachedFollowingTagSet(): Set<String> = latestContactList?.verifiedFollowTagSet ?: emptySet()
fun cachedFollowingGeohashSet(): Set<HexKey> {
return latestContactList?.verifiedFollowGeohashSet ?: emptySet()
}
fun cachedFollowingGeohashSet(): Set<HexKey> = latestContactList?.verifiedFollowGeohashSet ?: emptySet()
fun cachedFollowingCommunitiesSet(): Set<HexKey> {
return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
}
fun cachedFollowingCommunitiesSet(): Set<HexKey> = latestContactList?.verifiedFollowCommunitySet ?: emptySet()
fun cachedFollowCount(): Int? {
return latestContactList?.verifiedFollowKeySet?.size
}
fun cachedFollowCount(): Int? = latestContactList?.verifiedFollowKeySet?.size
suspend fun cachedFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
suspend fun cachedFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
val messagesToUser = privateChatrooms[key] ?: return false
return messagesToUser.roomMessages.any { this.pubkeyHex == it.author?.pubkeyHex }
return messagesToUser.authors.any { this == it }
}
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
return reports[loggedIn]?.firstOrNull() {
it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
fun hasReport(
loggedIn: User,
type: ReportEvent.ReportType,
): Boolean =
reports[loggedIn]?.firstOrNull {
it.event is ReportEvent &&
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
} != null
}
fun anyNameStartsWith(username: String): Boolean {
return info?.anyNameStartsWith(username) ?: false
}
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
var liveSet: UserLiveSet? = null
var flowSet: UserFlowSet? = null
fun live(): UserLiveSet {
if (liveSet == null) {
liveSet = UserLiveSet(this)
createOrDestroyLiveSync(true)
}
return liveSet!!
}
fun clearLive() {
if (liveSet != null && liveSet?.isInUse() == false) {
liveSet?.destroy()
liveSet = null
createOrDestroyLiveSync(false)
}
}
@Synchronized
fun createOrDestroyLiveSync(create: Boolean) {
if (create) {
if (liveSet == null) {
liveSet = UserLiveSet(this)
}
} else {
if (liveSet != null && liveSet?.isInUse() == false) {
liveSet?.destroy()
liveSet = null
}
}
}
@Synchronized
fun createOrDestroyFlowSync(create: Boolean) {
if (create) {
if (flowSet == null) {
flowSet = UserFlowSet(this)
}
} else {
if (flowSet != null && flowSet?.isInUse() == false) {
flowSet?.destroy()
flowSet = null
}
}
}
fun flow(): UserFlowSet {
if (flowSet == null) {
createOrDestroyFlowSync(true)
}
return flowSet!!
}
fun clearFlow() {
if (flowSet != null && flowSet?.isInUse() == false) {
createOrDestroyFlowSync(false)
}
}
}
@Stable
class UserLiveSet(u: User) {
class UserFlowSet(
u: User,
) {
// Observers line up here.
val follows = UserBundledRefresherFlow(u)
val relays = UserBundledRefresherFlow(u)
fun isInUse(): Boolean = relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
fun destroy() {
relays.destroy()
follows.destroy()
}
}
@Stable
class UserLiveSet(
u: User,
) {
val innerMetadata = UserBundledRefresherLiveData(u)
// UI Observers line up here.
@@ -385,6 +488,7 @@ class UserLiveSet(u: User) {
val innerRelayInfo = UserBundledRefresherLiveData(u)
val innerZaps = UserBundledRefresherLiveData(u)
val innerBookmarks = UserBundledRefresherLiveData(u)
val innerGallery = UserBundledRefresherLiveData(u)
val innerStatuses = UserBundledRefresherLiveData(u)
// UI Observers line up here.
@@ -397,22 +501,17 @@ class UserLiveSet(u: User) {
val relayInfo = innerRelayInfo.map { it }
val zaps = innerZaps.map { it }
val bookmarks = innerBookmarks.map { it }
val gallery = innerGallery.map { it }
val statuses = innerStatuses.map { it }
val profilePictureChanges = innerMetadata.map {
it.user.profilePicture()
}.distinctUntilChanged()
val profilePictureChanges = innerMetadata.map { it.user.profilePicture() }.distinctUntilChanged()
val nip05Changes = innerMetadata.map {
it.user.nip05()
}.distinctUntilChanged()
val nip05Changes = innerMetadata.map { it.user.nip05() }.distinctUntilChanged()
val userMetadataInfo = innerMetadata.map {
it.user.info
}.distinctUntilChanged()
val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged()
fun isInUse(): Boolean {
return metadata.hasObservers() ||
fun isInUse(): Boolean =
metadata.hasObservers() ||
follows.hasObservers() ||
followers.hasObservers() ||
reports.hasObservers() ||
@@ -421,11 +520,11 @@ class UserLiveSet(u: User) {
relayInfo.hasObservers() ||
zaps.hasObservers() ||
bookmarks.hasObservers() ||
gallery.hasObservers() ||
statuses.hasObservers() ||
profilePictureChanges.hasObservers() ||
nip05Changes.hasObservers() ||
userMetadataInfo.hasObservers()
}
fun destroy() {
innerMetadata.destroy()
@@ -437,6 +536,7 @@ class UserLiveSet(u: User) {
innerRelayInfo.destroy()
innerZaps.destroy()
innerBookmarks.destroy()
innerGallery.destroy()
innerStatuses.destroy()
}
}
@@ -445,10 +545,12 @@ class UserLiveSet(u: User) {
data class RelayInfo(
val url: String,
var lastEvent: Long,
var counter: Long
var counter: Long,
)
class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
class UserBundledRefresherLiveData(
val user: User,
) : LiveData<UserState>(UserState(user)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
@@ -459,16 +561,14 @@ class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserSta
fun invalidateData() {
checkNotInMainThread()
bundler.invalidate() {
bundler.invalidate {
checkNotInMainThread()
postValue(UserState(user))
}
}
fun <Y> map(
transform: (UserState) -> Y
): UserLoadingLiveData<Y> {
fun <Y> map(transform: (UserState) -> Y): UserLoadingLiveData<Y> {
val initialValue = this.value?.let { transform(it) }
val result = UserLoadingLiveData(user, initialValue)
result.addSource(this) { x -> result.value = transform(x) }
@@ -476,7 +576,33 @@ class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserSta
}
}
class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
@Stable
class UserBundledRefresherFlow(
val user: User,
) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
val stateFlow = MutableStateFlow(UserState(user))
fun destroy() {
bundler.cancel()
}
fun invalidateData() {
checkNotInMainThread()
bundler.invalidate {
checkNotInMainThread()
stateFlow.emit(UserState(user))
}
}
}
class UserLoadingLiveData<Y>(
val user: User,
initialValue: Y?,
) : MediatorLiveData<Y>(initialValue) {
override fun onActive() {
super.onActive()
NostrSingleUserDataSource.add(user)
@@ -488,5 +614,6 @@ class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveDat
}
}
@Immutable
class UserState(val user: User)
@Immutable class UserState(
val user: User,
)

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.observables
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
object CreatedAtComparator : Comparator<Note> {
override fun compare(
first: Note?,
second: Note?,
): Int {
val firstEvent = first?.event
val secondEvent = second?.event
return if (firstEvent == null && secondEvent == null) {
0
} else if (firstEvent == null) {
1
} else if (secondEvent == null) {
-1
} else {
firstEvent.createdAt().compareTo(secondEvent.createdAt())
}
}
}
object CreatedAtComparatorAddresses : Comparator<AddressableNote> {
override fun compare(
first: AddressableNote?,
second: AddressableNote?,
): Int {
val firstEvent = first?.event
val secondEvent = second?.event
return if (firstEvent == null && secondEvent == null) {
0
} else if (firstEvent == null) {
1
} else if (secondEvent == null) {
-1
} else {
firstEvent.createdAt().compareTo(secondEvent.createdAt())
}
}
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.observables
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.Event
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class LatestByKindAndAuthor<T : Event>(
private val kind: Int,
private val pubkey: String,
) {
private val _latest = MutableStateFlow<T?>(null)
val latest = _latest.asStateFlow()
fun matches(event: T) = event.kind == kind && event.pubKey == pubkey
fun updateIfMatches(event: T) {
if (matches(event)) {
if (event.createdAt > (_latest.value?.createdAt ?: 0)) {
_latest.tryEmit(event)
}
}
}
fun canDelete(): Boolean = _latest.subscriptionCount.value == 0
fun restart() {
val latestNote =
if ((kind in 10000..19999) || (kind in 30000..39999)) {
LocalCache.addressables
.maxOrNullOf(
filter = { idHex: String, note: AddressableNote ->
note.event?.let {
it.kind() == kind && it.pubKey() == pubkey
} == true
},
comparator = CreatedAtComparatorAddresses,
)?.event as? T
} else {
LocalCache.notes
.maxOrNullOf(
filter = { idHex: String, note: Note ->
note.event?.let {
it.kind() == kind && it.pubKey() == pubkey
} == true
},
comparator = CreatedAtComparator,
)?.event as? T
}
if (_latest.value != latestNote) {
_latest.tryEmit(latestNote)
}
}
suspend fun init() {
restart()
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.observables
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.Event
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class LatestByKindWithETag<T : Event>(
private val kind: Int,
private val eTag: String,
) {
private val _latest = MutableStateFlow<T?>(null)
val latest = _latest.asStateFlow()
fun matches(event: T) = event.kind == kind && event.isTaggedEvent(eTag)
fun updateIfMatches(event: T) {
if (matches(event)) {
if (event.createdAt > (_latest.value?.createdAt ?: 0)) {
_latest.tryEmit(event)
}
}
}
fun canDelete(): Boolean = _latest.subscriptionCount.value == 0
fun restart() {
val latestNote =
LocalCache.notes
.maxOrNullOf(
filter = { idHex: String, note: Note ->
note.event?.let {
it.kind() == kind && it.isTaggedEvent(eTag)
} == true
},
comparator = CreatedAtComparator,
)?.event as? T
if (_latest.value != latestNote) {
_latest.tryEmit(latestNote)
}
}
suspend fun init() {
restart()
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.ammolite.relays.NostrDataSource
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.Event
abstract class AmethystNostrDataSource(
debugName: String,
) : NostrDataSource(debugName) {
override fun consume(
event: Event,
relay: Relay,
) {
LocalCache.verifyAndConsume(event, relay)
}
override fun markAsSeenOnRelay(
eventId: String,
relay: Relay,
) {
val note = LocalCache.getNoteIfExists(eventId)
val noteEvent = note?.event
if (noteEvent is AddressableEvent) {
LocalCache.getAddressableNoteIfExists(noteEvent.address().toTag())?.addRelay(relay)
} else {
note?.addRelay(relay)
}
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.Stable
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.DataSource
import coil.fetch.DrawableResult
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.request.ImageRequest
import coil.request.Options
import com.vitorpamplona.amethyst.commons.preview.BlurHashDecoder
import java.net.URLDecoder
import java.net.URLEncoder
@Stable
class BlurHashFetcher(
private val options: Options,
private val data: Uri,
) : Fetcher {
override suspend fun fetch(): FetchResult {
checkNotInMainThread()
val hash = URLDecoder.decode(data.toString().removePrefix("bluehash:"), "utf-8")
val bitmap = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: throw Exception("Unable to convert Bluehash $data")
return DrawableResult(
drawable = bitmap.toDrawable(options.context.resources),
isSampled = false,
dataSource = DataSource.MEMORY,
)
}
object Factory : Fetcher.Factory<Uri> {
override fun create(
data: Uri,
options: Options,
imageLoader: ImageLoader,
): Fetcher = BlurHashFetcher(options, data)
}
}
object BlurHashRequester {
fun imageRequest(
context: Context,
message: String,
): ImageRequest {
val encodedMessage = URLEncoder.encode(message, "utf-8")
return ImageRequest
.Builder(context)
.data("bluehash:$encodedMessage")
.fetcherFactory(BlurHashFetcher.Factory)
.build()
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.util.LruCache
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
import com.vitorpamplona.quartz.events.ImmutableListOfLists
object CachedRichTextParser {
val richTextCache = LruCache<String, RichTextViewerState>(50)
fun getCached(content: String): RichTextViewerState? = richTextCache[content]
fun parseText(
content: String,
tags: ImmutableListOfLists<String>,
callbackUri: String? = null,
): RichTextViewerState {
val cached = richTextCache[content]
return if (cached != null) {
cached
} else {
val newUrls = RichTextParser().parseText(content, tags, callbackUri)
richTextCache.put(content, newUrls)
newUrls
}
}
}

View File

@@ -0,0 +1,266 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.content.Context
import android.util.LruCache
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.events.Event
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Base64
import kotlin.coroutines.cancellation.CancellationException
@Immutable
data class CashuToken(
val token: String,
val mint: String,
val totalAmount: Long,
val proofs: JsonNode,
)
object CachedCashuProcessor {
val cashuCache = LruCache<String, GenericLoadable<CashuToken>>(20)
fun cached(token: String): GenericLoadable<CashuToken> = cashuCache[token] ?: GenericLoadable.Loading()
fun parse(token: String): GenericLoadable<CashuToken> {
if (cashuCache[token] !is GenericLoadable.Loaded) {
val newCachuData = CashuProcessor().parse(token)
cashuCache.put(token, newCachuData)
}
return cashuCache[token]
}
}
class CashuProcessor {
fun parse(cashuToken: String): GenericLoadable<CashuToken> {
checkNotInMainThread()
try {
val base64token = cashuToken.replace("cashuA", "")
val cashu = jacksonObjectMapper().readTree(String(Base64.getDecoder().decode(base64token)))
val token = cashu.get("token").get(0)
val proofs = token.get("proofs")
val mint = token.get("mint").asText()
var totalAmount = 0L
for (proof in proofs) {
totalAmount += proof.get("amount").asLong()
}
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, proofs))
} catch (e: Exception) {
if (e is CancellationException) throw e
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
}
}
suspend fun melt(
token: CashuToken,
lud16: String,
onSuccess: (String, String) -> Unit,
onError: (String, String) -> Unit,
context: Context,
) {
checkNotInMainThread()
runCatching {
LightningAddressResolver()
.lnAddressInvoice(
lnaddress = lud16,
// Make invoice and leave room for fees
milliSats = token.totalAmount * 1000,
message = "Calculate Fees for Cashu",
onSuccess = { baseInvoice ->
feeCalculator(
token.mint,
baseInvoice,
onSuccess = { fees ->
LightningAddressResolver()
.lnAddressInvoice(
lnaddress = lud16,
// Make invoice and leave room for fees
milliSats = (token.totalAmount - fees) * 1000,
message = "Redeem Cashu",
onSuccess = { invoice ->
meltInvoice(token, invoice, fees, onSuccess, onError, context)
},
onProgress = {},
onError = onError,
context = context,
)
},
onError = onError,
context,
)
},
onProgress = {},
onError = onError,
context = context,
)
}
}
fun feeCalculator(
mintAddress: String,
invoice: String,
onSuccess: (Int) -> Unit,
onError: (String, String) -> Unit,
context: Context,
) {
checkNotInMainThread()
try {
val client = HttpClientManager.getHttpClient()
val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint
val factory = Event.mapper.nodeFactory
val jsonObject = factory.objectNode()
jsonObject.put("pr", invoice)
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonObject.toString().toRequestBody(mediaType)
val request =
Request
.Builder()
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
val body = it.body.string()
val tree = jacksonObjectMapper().readTree(body)
val feeCost = tree?.get("fee")?.asInt()
if (feeCost != null) {
onSuccess(
feeCost,
)
} else {
val msg =
tree
?.get("detail")
?.asText()
?.split('.')
?.getOrNull(0)
?.ifBlank { null }
onError(
stringRes(context, R.string.cashu_failed_redemption),
if (msg != null) {
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg)
} else {
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg)
},
)
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
onError(
stringRes(context, R.string.cashu_successful_redemption),
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message),
)
}
}
private fun meltInvoice(
token: CashuToken,
invoice: String,
fees: Int,
onSuccess: (String, String) -> Unit,
onError: (String, String) -> Unit,
context: Context,
) {
try {
val client = HttpClientManager.getHttpClient()
val url = token.mint + "/melt" // Melt cashu tokens at Mint
val factory = Event.mapper.nodeFactory
val jsonObject = factory.objectNode()
jsonObject.put("proofs", token.proofs)
jsonObject.put("pr", invoice)
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonObject.toString().toRequestBody(mediaType)
val request =
Request
.Builder()
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
val body = it.body.string()
val tree = jacksonObjectMapper().readTree(body)
val successful = tree?.get("paid")?.asText() == "true"
if (successful) {
onSuccess(
stringRes(context, R.string.cashu_successful_redemption),
stringRes(
context,
R.string.cashu_successful_redemption_explainer,
token.totalAmount.toString(),
fees.toString(),
),
)
} else {
val msg =
tree
?.get("detail")
?.asText()
?.split('.')
?.getOrNull(0)
?.ifBlank { null }
onError(
stringRes(context, R.string.cashu_failed_redemption),
if (msg != null) {
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg)
} else {
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg)
},
)
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
onError(
stringRes(context, R.string.cashu_successful_redemption),
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message),
)
}
}
}

View File

@@ -1,15 +1,37 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.quartz.events.ImmutableListOfLists
fun String.isUTF16Char(pos: Int): Boolean {
return Character.charCount(this.codePointAt(pos)) == 2
}
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
fun String.firstFullCharOld(): String {
return when (this.length) {
0, 1 -> return this
2, 3 -> return if (isUTF16Char(0)) this.take(2) else this.take(1)
0,
1,
-> return this
2,
3,
-> return if (isUTF16Char(0)) this.take(2) else this.take(1)
else -> {
val first = isUTF16Char(0)
val second = isUTF16Char(2)
@@ -96,7 +118,7 @@ fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
val emojiParts = this.split(":", limit = 3)
if (emojiParts.size >= 2) {
val emojiName = emojiParts[1]
val emojiUrl = tags.lists.firstOrNull() { it.size > 1 && it[1] == emojiName }?.getOrNull(2)
val emojiUrl = tags.lists.firstOrNull { it.size > 1 && it[1] == emojiName }?.getOrNull(2)
if (emojiUrl != null) {
return ":$emojiName:$emojiUrl"
}

View File

@@ -0,0 +1,268 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.media.MediaDataSource
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.BitmapParams
import android.os.Build
import android.util.Log
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import io.trbl.blurhash.BlurHash
import kotlinx.coroutines.CancellationException
import java.io.IOException
import kotlin.math.roundToInt
class FileHeader(
val mimeType: String?,
val hash: String,
val size: Int,
val dim: String?,
val blurHash: String?,
) {
companion object {
suspend fun prepare(
fileUrl: String,
mimeType: String?,
dimPrecomputed: String?,
onReady: (FileHeader) -> Unit,
onError: (String?) -> Unit,
) {
try {
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
if (imageData != null) {
prepare(imageData, mimeType, dimPrecomputed, onReady, onError)
} else {
onError(null)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
onError(e.message)
}
}
fun prepare(
data: ByteArray,
mimeType: String?,
dimPrecomputed: String?,
onReady: (FileHeader) -> Unit,
onError: (String?) -> Unit,
) {
try {
val hash = CryptoUtils.sha256(data).toHexKey()
val size = data.size
val (blurHash, dim) =
if (mimeType?.startsWith("image/") == true) {
val opt = BitmapFactory.Options()
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
val intArray = IntArray(mBitmap.width * mBitmap.height)
mBitmap.getPixels(
intArray,
0,
mBitmap.width,
0,
0,
mBitmap.width,
mBitmap.height,
)
val dim = "${mBitmap.width}x${mBitmap.height}"
val aspectRatio = (mBitmap.width).toFloat() / (mBitmap.height).toFloat()
if (aspectRatio > 1) {
Pair(
BlurHash.encode(
intArray,
mBitmap.width,
mBitmap.height,
9,
(9 * (1 / aspectRatio)).roundToInt(),
),
dim,
)
} else if (aspectRatio < 1) {
Pair(
BlurHash.encode(
intArray,
mBitmap.width,
mBitmap.height,
(9 * aspectRatio).roundToInt(),
9,
),
dim,
)
} else {
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4), dim)
}
} else if (mimeType?.startsWith("video/") == true) {
val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(ByteArrayMediaDataSource(data))
val newDim = mediaMetadataRetriever.prepareDimFromVideo() ?: dimPrecomputed
val blurhash =
mediaMetadataRetriever.getThumbnail()?.let { thumbnail ->
val aspectRatio = (thumbnail.width).toFloat() / (thumbnail.height).toFloat()
val intArray = IntArray(thumbnail.width * thumbnail.height)
thumbnail.getPixels(
intArray,
0,
thumbnail.width,
0,
0,
thumbnail.width,
thumbnail.height,
)
if (aspectRatio > 1) {
BlurHash.encode(
intArray,
thumbnail.width,
thumbnail.height,
9,
(9 * (1 / aspectRatio)).roundToInt(),
)
} else if (aspectRatio < 1) {
BlurHash.encode(
intArray,
thumbnail.width,
thumbnail.height,
(9 * aspectRatio).roundToInt(),
9,
)
} else {
BlurHash.encode(intArray, thumbnail.width, thumbnail.height, 4, 4)
}
}
if (newDim != "0x0") {
Pair(blurhash, newDim)
} else {
Pair(blurhash, null)
}
} else {
Pair(null, null)
}
onReady(FileHeader(mimeType, hash, size, dim, blurHash))
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
onError(e.message)
}
}
}
}
fun MediaMetadataRetriever.getThumbnail(): Bitmap? {
val raw: ByteArray? = getEmbeddedPicture()
if (raw != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return ImageDecoder.decodeBitmap(ImageDecoder.createSource(raw))
}
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val params = BitmapParams()
params.preferredConfig = Bitmap.Config.ARGB_8888
// Fall back to middle of video
// Note: METADATA_KEY_DURATION unit is in ms, not us.
val thumbnailTimeUs: Long =
(extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0) * 1000 / 2
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getFrameAtTime(thumbnailTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, params)
} else {
null
}
} else {
null
}
}
fun MediaMetadataRetriever.prepareDimFromVideo(): String? {
val width = prepareVideoWidth() ?: return null
val height = prepareVideoHeight() ?: return null
return "${width}x$height"
}
fun MediaMetadataRetriever.prepareVideoWidth(): Int? {
val widthData = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
return if (widthData.isNullOrEmpty()) {
null
} else {
widthData.toInt()
}
}
fun MediaMetadataRetriever.prepareVideoHeight(): Int? {
val heightData = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
return if (heightData.isNullOrEmpty()) {
null
} else {
heightData.toInt()
}
}
class ByteArrayMediaDataSource(
var imageData: ByteArray,
) : MediaDataSource() {
override fun getSize(): Long = imageData.size.toLong()
@Throws(IOException::class)
override fun readAt(
position: Long,
buffer: ByteArray,
offset: Int,
size: Int,
): Int {
if (position >= imageData.size) {
return -1
}
val newSize =
if (position + size > imageData.size) {
size - ((position.toInt() + size) - imageData.size)
} else {
size
}
imageData.copyInto(buffer, offset, position.toInt(), position.toInt() + newSize)
return newSize
}
@Throws(IOException::class)
override fun close() {}
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.R
class HttpStatusMessages {
companion object {
fun resourceIdFor(statusCode: Int = 0): Int? =
when (statusCode) {
400 -> R.string.http_status_400
401 -> R.string.http_status_401
402 -> R.string.http_status_402
403 -> R.string.http_status_403
404 -> R.string.http_status_404
405 -> R.string.http_status_405
406 -> R.string.http_status_406
407 -> R.string.http_status_407
408 -> R.string.http_status_408
409 -> R.string.http_status_409
410 -> R.string.http_status_410
411 -> R.string.http_status_411
412 -> R.string.http_status_412
413 -> R.string.http_status_413
414 -> R.string.http_status_414
415 -> R.string.http_status_415
416 -> R.string.http_status_416
417 -> R.string.http_status_417
426 -> R.string.http_status_426
500 -> R.string.http_status_500
501 -> R.string.http_status_501
502 -> R.string.http_status_502
503 -> R.string.http_status_503
504 -> R.string.http_status_504
505 -> R.string.http_status_505
506 -> R.string.http_status_506
507 -> R.string.http_status_507
508 -> R.string.http_status_508
511 -> R.string.http_status_511
else -> null
}
}
}

View File

@@ -0,0 +1,141 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.annotation.SuppressLint
import android.content.Context
import android.location.Geocoder
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.HandlerThread
import android.util.LruCache
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
class LocationUtil(
context: Context,
) {
companion object {
const val MIN_TIME: Long = 1000L
const val MIN_DISTANCE: Float = 0.0f
}
private val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var locationListener: LocationListener? = null
val locationStateFlow = MutableStateFlow<Location>(Location(LocationManager.NETWORK_PROVIDER))
val providerState = mutableStateOf(false)
val isStart: MutableState<Boolean> = mutableStateOf(false)
private val locHandlerThread = HandlerThread("LocationUtil Thread")
init {
locHandlerThread.start()
}
@SuppressLint("MissingPermission")
fun start(
minTimeMs: Long = MIN_TIME,
minDistanceM: Float = MIN_DISTANCE,
) {
locationListener().let {
locationListener = it
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
minTimeMs,
minDistanceM,
it,
locHandlerThread.looper,
)
}
providerState.value = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
isStart.value = true
}
fun stop() {
locationListener?.let { locationManager.removeUpdates(it) }
isStart.value = false
}
private fun locationListener() =
object : LocationListener {
override fun onLocationChanged(location: Location) {
locationStateFlow.value = location
}
override fun onProviderEnabled(provider: String) {
providerState.value = true
}
override fun onProviderDisabled(provider: String) {
providerState.value = false
}
}
}
object CachedGeoLocations {
val locationNames = LruCache<String, String>(20)
fun cached(geoHashStr: String): String? = locationNames[geoHashStr]
suspend fun geoLocate(
geoHashStr: String,
location: Location,
context: Context,
): String? {
locationNames[geoHashStr]?.let {
return it
}
val name = ReverseGeoLocationUtil().execute(location, context)?.ifBlank { null }
if (name != null) {
locationNames.put(geoHashStr, name)
}
return name
}
}
private class ReverseGeoLocationUtil {
suspend fun execute(
location: Location,
context: Context,
): String? {
return try {
Geocoder(context)
.getFromLocation(location.latitude, location.longitude, 1)
?.firstOrNull()
?.let { address ->
listOfNotNull(address.locality ?: address.subAdminArea, address.countryCode)
.joinToString(", ")
}
} catch (e: Exception) {
if (e is CancellationException) throw e
e.printStackTrace()
return null
}
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.os.Looper
import com.vitorpamplona.amethyst.BuildConfig
fun checkNotInMainThread() {
if (BuildConfig.DEBUG && isMainThread()) {
throw OnMainThreadException("It should not be in the MainThread")
}
}
fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
class OnMainThreadException(
str: String,
) : RuntimeException(str)

View File

@@ -0,0 +1,137 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
class Nip05NostrAddressVerifier {
fun assembleUrl(nip05address: String): String? {
val parts = nip05address.trim().split("@")
if (parts.size == 2) {
return "https://${parts[1]}/.well-known/nostr.json?name=${parts[0]}"
}
if (parts.size == 1) {
return "https://${parts[0]}/.well-known/nostr.json?name=_"
}
return null
}
suspend fun fetchNip05Json(
nip05: String,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
) = withContext(Dispatchers.IO) {
checkNotInMainThread()
val url = assembleUrl(nip05)
if (url == null) {
onError("Could not assemble url from Nip05: \"${nip05}\". Check the user's setup")
return@withContext
}
try {
val request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
// Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.
HttpClientManager
.getHttpClient()
.newBuilder()
.followRedirects(false)
.build()
.newCall(request)
.execute()
.use {
checkNotInMainThread()
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError(
"Could not resolve $nip05. Error: ${it.code}. Check if the server is up and if the address $nip05 is correct",
)
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
onError("Could not resolve NIP-05 $nip05 as URL $url: ${e.message}")
}
}
suspend fun verifyNip05(
nip05: String,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
) {
// check fails on tests
checkNotInMainThread()
val mapper = jacksonObjectMapper()
fetchNip05Json(
nip05,
onSuccess = {
checkNotInMainThread()
// NIP05 usernames are case insensitive, but JSON properties are not
// converts the json to lowercase and then tries to access the username via a
// lowercase version of the username.
val nip05url =
try {
mapper.readTree(it.lowercase())
} catch (e: Throwable) {
if (e is CancellationException) throw e
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
null
}
val parts = nip05.split("@")
val user =
if (parts.size == 2) {
parts[0].lowercase()
} else {
"_"
}
val hexKey = nip05url?.get("names")?.get(user)?.asText()
if (hexKey == null) {
onError("Username not found in the NIP05 JSON")
} else {
onSuccess(hexKey)
}
},
onError = onError,
)
}
}

View File

@@ -0,0 +1,183 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.util.Log
import android.util.LruCache
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CancellationException
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
object Nip11CachedRetriever {
open class RetrieveResult(
val time: Long,
)
class RetrieveResultError(
val error: Nip11Retriever.ErrorCode,
val msg: String? = null,
) : RetrieveResult(TimeUtils.now())
class RetrieveResultSuccess(
val data: Nip11RelayInformation,
) : RetrieveResult(TimeUtils.now())
class RetrieveResultLoading : RetrieveResult(TimeUtils.now())
private val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
private val retriever = Nip11Retriever()
fun getFromCache(dirtyUrl: String): Nip11RelayInformation? {
val result = relayInformationDocumentCache.get(RelayUrlFormatter.getHttpsUrl(dirtyUrl)) ?: return null
if (result is RetrieveResultSuccess) return result.data
return null
}
suspend fun loadRelayInfo(
dirtyUrl: String,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
) {
checkNotInMainThread()
val url = RelayUrlFormatter.getHttpsUrl(dirtyUrl)
val doc = relayInformationDocumentCache.get(url)
if (doc != null) {
if (doc is RetrieveResultSuccess) {
onInfo(doc.data)
} else if (doc is RetrieveResultLoading) {
if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) {
// just wait.
} else {
retrieve(url, dirtyUrl, onInfo, onError)
}
} else if (doc is RetrieveResultError) {
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
onError(dirtyUrl, doc.error, null)
} else {
retrieve(url, dirtyUrl, onInfo, onError)
}
}
} else {
retrieve(url, dirtyUrl, onInfo, onError)
}
}
private suspend fun retrieve(
url: String,
dirtyUrl: String,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
) {
relayInformationDocumentCache.put(url, RetrieveResultLoading())
retriever.loadRelayInfo(
url = url,
dirtyUrl = dirtyUrl,
onInfo = {
checkNotInMainThread()
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
onInfo(it)
},
onError = { dirtyUrl, code, errorMsg ->
checkNotInMainThread()
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
onError(url, code, errorMsg)
},
)
}
}
class Nip11Retriever {
enum class ErrorCode {
FAIL_TO_ASSEMBLE_URL,
FAIL_TO_REACH_SERVER,
FAIL_TO_PARSE_RESULT,
FAIL_WITH_HTTP_STATUS,
}
suspend fun loadRelayInfo(
url: String,
dirtyUrl: String,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (String, ErrorCode, String?) -> Unit,
) {
checkNotInMainThread()
try {
val request: Request =
Request
.Builder()
.header("Accept", "application/nostr+json")
.url(url)
.build()
HttpClientManager
.getHttpClientForUrl(dirtyUrl)
.newCall(request)
.enqueue(
object : Callback {
override fun onResponse(
call: Call,
response: Response,
) {
checkNotInMainThread()
response.use {
val body = it.body.string()
try {
if (it.isSuccessful) {
onInfo(Nip11RelayInformation.fromJson(body))
} else {
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e(
"RelayInfoFail",
"Resulting Message from Relay $dirtyUrl in not parseable: $body",
e,
)
onError(dirtyUrl, ErrorCode.FAIL_TO_PARSE_RESULT, e.message)
}
}
}
override fun onFailure(
call: Call,
e: IOException,
) {
Log.e("RelayInfoFail", "$dirtyUrl unavailable", e)
onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message)
}
},
)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
}
}
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.CancellationException
import okhttp3.Request
object Nip96MediaServers {
val DEFAULT =
listOf(
ServerName("Nostr.Build", "https://nostr.build"),
ServerName("NostrCheck.me", "https://nostrcheck.me"),
ServerName("NostPic", "https://nostpic.com"),
ServerName("Sovbit", "https://files.sovbit.host"),
ServerName("Void.cat", "https://void.cat"),
)
data class ServerName(
val name: String,
val baseUrl: String,
)
val cache: MutableMap<String, Nip96Retriever.ServerInfo> = mutableMapOf()
suspend fun load(url: String): Nip96Retriever.ServerInfo {
val cached = cache[url]
if (cached != null) return cached
val fetched = Nip96Retriever().loadInfo(url)
cache[url] = fetched
return fetched
}
}
class Nip96Retriever {
data class ServerInfo(
@JsonProperty("api_url") val apiUrl: String,
@JsonProperty("download_url") val downloadUrl: String? = null,
@JsonProperty("delegated_to_url") val delegatedToUrl: String? = null,
@JsonProperty("supported_nips") val supportedNips: ArrayList<Int> = arrayListOf(),
@JsonProperty("tos_url") val tosUrl: String? = null,
@JsonProperty("content_types") val contentTypes: ArrayList<MimeType> = arrayListOf(),
@JsonProperty("plans") val plans: Map<PlanName, Plan> = mapOf(),
)
data class Plan(
@JsonProperty("name") val name: String? = null,
@JsonProperty("is_nip98_required") val isNip98Required: Boolean? = null,
@JsonProperty("url") val url: String? = null,
@JsonProperty("max_byte_size") val maxByteSize: Long? = null,
@JsonProperty("file_expiration") val fileExpiration: ArrayList<Int> = arrayListOf(),
@JsonProperty("media_transformations")
val mediaTransformations: Map<MimeType, Array<String>> = emptyMap(),
)
fun parse(body: String): ServerInfo {
val mapper =
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return mapper.readValue(body, ServerInfo::class.java)
}
suspend fun loadInfo(baseUrl: String): ServerInfo {
checkNotInMainThread()
val request: Request =
Request
.Builder()
.header("Accept", "application/nostr+json")
.url(baseUrl.removeSuffix("/") + "/.well-known/nostr/nip96.json")
.build()
HttpClientManager.getHttpClient().newCall(request).execute().use { response ->
checkNotInMainThread()
response.use {
val body = it.body.string()
try {
if (it.isSuccessful) {
return parse(body)
} else {
throw RuntimeException(
"Resulting Message from $baseUrl is an error: ${response.code} ${response.message}",
)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("RelayInfoFail", "Resulting Message from $baseUrl in not parseable: $body", e)
throw e
}
}
}
}
}
typealias PlanName = String
typealias MimeType = String

View File

@@ -0,0 +1,335 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import androidx.core.net.toFile
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody
import okio.BufferedSink
import okio.source
import java.io.InputStream
import java.util.Base64
import kotlin.coroutines.resume
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun randomChars() = List(16) { charPool.random() }.joinToString("")
class Nip96Uploader(
val account: Account?,
) {
suspend fun uploadImage(
uri: Uri,
contentType: String?,
size: Long?,
alt: String?,
sensitiveContent: String?,
server: Nip96MediaServers.ServerName,
contentResolver: ContentResolver,
onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent {
val serverInfo =
Nip96Retriever()
.loadInfo(
server.baseUrl,
)
return uploadImage(
uri,
contentType,
size,
alt,
sensitiveContent,
serverInfo,
contentResolver,
onProgress,
context,
)
}
suspend fun uploadImage(
uri: Uri,
contentType: String?,
size: Long?,
alt: String?,
sensitiveContent: String?,
server: Nip96Retriever.ServerInfo,
contentResolver: ContentResolver,
onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent {
checkNotInMainThread()
val myContentType = contentType ?: contentResolver.getType(uri)
val imageInputStream = contentResolver.openInputStream(uri)
val length =
size
?: contentResolver.query(uri, null, null, null, null)?.use {
it.moveToFirst()
val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
it.getLong(sizeIndex)
}
?: kotlin.runCatching { uri.toFile().length() }.getOrNull() ?: 0
checkNotNull(imageInputStream) { "Can't open the image input stream" }
return uploadImage(
imageInputStream,
length,
myContentType,
alt,
sensitiveContent,
server,
onProgress,
context,
)
}
suspend fun uploadImage(
inputStream: InputStream,
length: Long,
contentType: String?,
alt: String?,
sensitiveContent: String?,
server: Nip96Retriever.ServerInfo,
onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent {
checkNotInMainThread()
val fileName = randomChars()
val extension =
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
val client = HttpClientManager.getHttpClient()
val requestBuilder = Request.Builder()
val requestBody: RequestBody =
MultipartBody
.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("expiration", "")
.addFormDataPart("size", length.toString())
.also { body ->
alt?.let { body.addFormDataPart("alt", it) }
sensitiveContent?.let { body.addFormDataPart("content-warning", it) }
contentType?.let { body.addFormDataPart("content_type", it) }
}.addFormDataPart(
"file",
"$fileName.$extension",
object : RequestBody() {
override fun contentType() = contentType?.toMediaType()
override fun contentLength() = length
override fun writeTo(sink: BufferedSink) {
inputStream.source().use(sink::writeAll)
}
},
).build()
nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) }
requestBuilder
.addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(server.apiUrl)
.post(requestBody)
val request = requestBuilder.build()
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
response.body.use { body ->
val str = body.string()
val result = parseResults(str)
if (!result.processingUrl.isNullOrBlank()) {
return waitProcessing(result, server, onProgress)
} else if (result.status == "success" && result.nip94Event != null) {
return result.nip94Event
} else {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, result.message))
}
}
} else {
val explanation = HttpStatusMessages.resourceIdFor(response.code)
if (explanation != null) {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, stringRes(context, explanation)))
} else {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, response.code))
}
}
}
}
suspend fun delete(
hash: String,
contentType: String?,
server: Nip96Retriever.ServerInfo,
context: Context,
): Boolean {
val extension =
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
val client = HttpClientManager.getHttpClient()
val requestBuilder = Request.Builder()
nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) }
val request =
requestBuilder
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(server.apiUrl.removeSuffix("/") + "/$hash.$extension")
.delete()
.build()
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
response.body.use { body ->
val str = body.string()
val result = parseDeleteResults(str)
return result.status == "success"
}
} else {
val explanation = HttpStatusMessages.resourceIdFor(response.code)
if (explanation != null) {
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation)))
} else {
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code))
}
}
}
}
private suspend fun waitProcessing(
result: Nip96Result,
server: Nip96Retriever.ServerInfo,
onProgress: (percentage: Float) -> Unit,
): PartialEvent {
val client = HttpClientManager.getHttpClient()
var currentResult = result
while (!result.processingUrl.isNullOrBlank() && (currentResult.percentage ?: 100) < 100) {
onProgress((currentResult.percentage ?: 100) / 100f)
val request: Request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(result.processingUrl)
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
it.body.use { currentResult = parseResults(it.string()) }
}
}
delay(500)
}
onProgress((currentResult.percentage ?: 100) / 100f)
val nip94 = currentResult.nip94Event
if (nip94 != null) {
return nip94
} else {
throw RuntimeException("Error waiting for processing. Final result is unavailable")
}
}
suspend fun nip98Header(url: String): String? =
withTimeoutOrNull(5000) {
suspendCancellableCoroutine { continuation ->
nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) }
}
}
fun nip98Header(
url: String,
method: String,
file: ByteArray? = null,
onReady: (String?) -> Unit,
) {
val myAccount = account
if (myAccount == null) {
onReady(null)
return
}
myAccount.createHTTPAuthorization(url, method, file) {
val encodedNIP98Event = Base64.getEncoder().encodeToString(it.toJson().toByteArray())
onReady("Nostr $encodedNIP98Event")
}
}
data class DeleteResult(
val status: String?,
val message: String?,
)
private fun parseDeleteResults(body: String): DeleteResult {
val mapper =
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return mapper.readValue(body, DeleteResult::class.java)
}
data class Nip96Result(
val status: String? = null,
val message: String? = null,
@JsonProperty("processing_url") val processingUrl: String? = null,
val percentage: Int? = null,
@JsonProperty("nip94_event") val nip94Event: PartialEvent? = null,
)
class PartialEvent(
val tags: Array<Array<String>>? = null,
val content: String? = null,
)
private fun parseResults(body: String): Nip96Result {
val mapper =
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return mapper.readValue(body, Nip96Result::class.java)
}
}

View File

@@ -0,0 +1,464 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.CalendarDateSlotEvent
import com.vitorpamplona.quartz.events.CalendarRSVPEvent
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.FileServersEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.GitIssueEvent
import com.vitorpamplona.quartz.events.GitPatchEvent
import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.SearchRelayListEvent
import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils
// TODO: Migrate this to a property of AccountVi
object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
lateinit var account: Account
var otherAccounts = listOf<HexKey>()
val latestEOSEs = EOSEAccount()
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
fun createAccountContactListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(ContactListEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1,
),
)
fun createAccountMetadataFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1,
),
)
fun createAccountRelayListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds =
listOf(
StatusEvent.KIND,
AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND,
SearchRelayListEvent.KIND,
FileServersEvent.KIND,
PrivateOutboxRelayListEvent.KIND,
),
authors = listOf(account.userProfile().pubkeyHex),
limit = 10,
),
)
fun createOtherAccountsBaseFilter(): TypedFilter? {
val otherAuthors = otherAccounts.filter { it != account.userProfile().pubkeyHex }
if (otherAuthors.isEmpty()) return null
return TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds =
listOf(
MetadataEvent.KIND,
ContactListEvent.KIND,
AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND,
SearchRelayListEvent.KIND,
FileServersEvent.KIND,
MuteListEvent.KIND,
PeopleListEvent.KIND,
),
authors = otherAuthors,
limit = 100,
),
)
}
fun createAccountSettingsFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 100,
),
)
fun createAccountReportsFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(DraftEvent.KIND, ReportEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultNotificationFollowList.value)
?.relayList,
),
)
fun createAccountLastPostsListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
authors = listOf(account.userProfile().pubkeyHex),
limit = 400,
),
)
fun createNotificationFilter(): TypedFilter {
val since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultNotificationFollowList.value)
?.relayList
?: account.connectToRelays.value.associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
PollNoteEvent.KIND,
ReactionEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ReportEvent.KIND,
LnZapEvent.KIND,
LnZapPaymentResponseEvent.KIND,
ChannelMessageEvent.KIND,
BadgeAwardEvent.KIND,
),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
limit = 4000,
since = since,
),
)
}
fun createNotificationFilter2(): TypedFilter {
val since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultNotificationFollowList.value)
?.relayList
?: account.connectToRelays.value.associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
?: account.convertLocalRelays().associate { it.url to EOSETime(TimeUtils.oneWeekAgo()) }
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds =
listOf(
GitReplyEvent.KIND,
GitIssueEvent.KIND,
GitPatchEvent.KIND,
HighlightEvent.KIND,
CalendarDateSlotEvent.KIND,
CalendarTimeSlotEvent.KIND,
CalendarRSVPEvent.KIND,
),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
limit = 400,
since = since,
),
)
}
fun createGiftWrapsToMeFilter() =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(GiftWrapEvent.KIND),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get("&&((GIFTWRAPS_EOSE))&&")
?.relayList
?.mapValues {
EOSETime(it.value.time - TimeUtils.twoDays())
},
),
)
val accountChannel =
requestNewChannel { time, relayUrl ->
if (hasLoadedTheBasics[account.userProfile()] != null) {
latestEOSEs.addOrUpdate(
account.userProfile(),
account.defaultNotificationFollowList.value,
relayUrl,
time,
)
latestEOSEs.addOrUpdate(
account.userProfile(),
"&&((GIFTWRAPS_EOSE))&&",
relayUrl,
time,
)
} else {
hasLoadedTheBasics[account.userProfile()] = true
invalidateFilters()
}
}
override fun consume(
event: Event,
relay: Relay,
) {
checkNotInMainThread()
if (LocalCache.justVerify(event)) {
when (event) {
is PrivateOutboxRelayListEvent -> {
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
val noteEvent = note?.event
if (noteEvent == null || event.createdAt > noteEvent.createdAt()) {
event.privateTags(account.signer) {
LocalCache.justConsume(event, relay)
}
}
}
is DraftEvent -> {
// Avoid decrypting over and over again if the event already exist.
if (!event.isDeleted()) {
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
val noteEvent = note?.event
if (noteEvent != null) {
if (event.createdAt > noteEvent.createdAt() || relay.brief !in note.relays) {
LocalCache.consume(event, relay)
}
} else {
// decrypts
event.cachedDraft(account.signer) {}
LocalCache.justConsume(event, relay)
}
}
}
is GiftWrapEvent -> {
// Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id)
val noteEvent = note?.event as? GiftWrapEvent
if (noteEvent != null) {
if (relay.brief !in note.relays) {
noteEvent.cachedGift(account.signer) {
LocalCache.justConsume(noteEvent, relay)
this.consume(it, relay)
}
}
} else {
// new event
event.cachedGift(account.signer) {
LocalCache.justConsume(event, relay)
this.consume(it, relay)
}
}
}
is SealedGossipEvent -> {
// Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id)
val noteEvent = note?.event as? SealedGossipEvent
if (noteEvent != null) {
if (relay.brief !in note.relays) {
// adds the relay to seal and inner chat
noteEvent.cachedGossip(account.signer) {
LocalCache.consume(noteEvent, relay)
LocalCache.justConsume(it, relay)
}
}
} else {
// new event
event.cachedGossip(account.signer) {
LocalCache.justConsume(event, relay)
LocalCache.justConsume(it, relay)
}
}
}
is LnZapEvent -> {
// Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id)
if (note?.event == null) {
event.zapRequest?.let {
if (it.isPrivateZap()) {
it.decryptPrivateZap(account.signer) {}
}
}
LocalCache.justConsume(event, relay)
}
}
else -> {
LocalCache.justConsume(event, relay)
}
}
}
}
override fun markAsSeenOnRelay(
eventId: String,
relay: Relay,
) {
checkNotInMainThread()
super.markAsSeenOnRelay(eventId, relay)
val note = LocalCache.getNoteIfExists(eventId) ?: return
val noteEvent = note.event ?: return
markInnerAsSeenOnRelay(noteEvent, relay)
}
private fun markInnerAsSeenOnRelay(
noteEvent: EventInterface,
relay: Relay,
) {
LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay)
if (noteEvent is GiftWrapEvent) {
noteEvent.cachedGift(account.signer) { gift -> markInnerAsSeenOnRelay(gift, relay) }
} else if (noteEvent is SealedGossipEvent) {
noteEvent.cachedGossip(account.signer) { rumor ->
markInnerAsSeenOnRelay(rumor, relay)
}
}
}
override fun updateChannelFilters() =
if (hasLoadedTheBasics[account.userProfile()] != null) {
// gets everything about the user logged in
accountChannel.typedFilters =
listOfNotNull(
createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createNotificationFilter(),
createNotificationFilter2(),
createGiftWrapsToMeFilter(),
createAccountReportsFilter(),
createAccountSettingsFilter(),
createAccountLastPostsListFilter(),
createOtherAccountsBaseFilter(),
).ifEmpty { null }
} else {
// just the basics.
accountChannel.typedFilters =
listOf(
createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createAccountSettingsFilter(),
).ifEmpty { null }
}
override fun auth(
relay: Relay,
challenge: String,
) {
super.auth(relay, challenge)
if (this::account.isInitialized) {
account.createAuthEvent(relay, challenge) {
Client.send(
it,
relay.url,
)
}
}
}
override fun notify(
relay: Relay,
description: String,
) {
super.notify(relay, description)
if (this::account.isInitialized) {
account.addPaymentRequestIfNew(Account.PaymentRequest(relay.url, description))
}
}
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
var account: Account? = null
var channel: Channel? = null
fun loadMessagesBetween(
account: Account,
channel: Channel,
) {
this.account = account
this.channel = channel
resetFilters()
}
fun clear() {
account = null
channel = null
}
fun createMessagesByMeToChannelFilter(): TypedFilter? {
val myAccount = account ?: return null
if (channel is PublicChatChannel) {
// Brings on messages by the user from all other relays.
// Since we ship with write to public, read from private only
// this guarantees that messages from the author do not disappear.
return TypedFilter(
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
filter =
Filter(
kinds = listOf(ChannelMessageEvent.KIND),
authors = listOf(myAccount.userProfile().pubkeyHex),
limit = 50,
),
)
} else if (channel is LiveActivitiesChannel) {
// Brings on messages by the user from all other relays.
// Since we ship with write to public, read from private only
// this guarantees that messages from the author do not disappear.
return TypedFilter(
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
filter =
Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
authors = listOf(myAccount.userProfile().pubkeyHex),
limit = 50,
),
)
}
return null
}
fun createMessagesToChannelFilter(): TypedFilter? {
if (channel is PublicChatChannel) {
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
kinds = listOf(ChannelMessageEvent.KIND),
tags = mapOf("e" to listOfNotNull(channel?.idHex)),
limit = 200,
),
)
} else if (channel is LiveActivitiesChannel) {
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
tags = mapOf("a" to listOfNotNull(channel?.idHex)),
limit = 200,
),
)
}
return null
}
val messagesChannel = requestNewChannel()
override fun updateChannelFilters() {
messagesChannel.typedFilters =
listOfNotNull(
createMessagesToChannelFilter(),
createMessagesByMeToChannelFilter(),
).ifEmpty { null }
}
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.PrivateDmEvent
object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
lateinit var account: Account
private var withRoom: ChatroomKey? = null
private val latestEOSEs = EOSEAccount()
fun loadMessagesBetween(
accountIn: Account,
withRoom: ChatroomKey,
) {
this.account = accountIn
this.withRoom = withRoom
resetFilters()
}
fun createMessagesToMeFilter(): TypedFilter? {
val myPeer = withRoom
return if (myPeer != null) {
TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter =
Filter(
kinds = listOf(PrivateDmEvent.KIND),
authors = myPeer.users.toList(),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(withRoom.hashCode().toString())
?.relayList,
),
)
} else {
null
}
}
fun createMessagesFromMeFilter(): TypedFilter? {
val myPeer = withRoom
return if (myPeer != null) {
TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter =
Filter(
kinds = listOf(PrivateDmEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
tags = mapOf("p" to myPeer.users.map { it }),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(withRoom.hashCode().toString())
?.relayList,
),
)
} else {
null
}
}
fun clearEOSEs(account: Account) {
latestEOSEs.removeDataFor(account.userProfile())
}
val inandoutChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), withRoom.hashCode().toString(), relayUrl, time)
}
override fun updateChannelFilters() {
inandoutChannel.typedFilters =
listOfNotNull(
createMessagesToMeFilter(),
createMessagesFromMeFilter(),
).ifEmpty { null }
}
}

View File

@@ -0,0 +1,170 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
val chatRoomList = "ChatroomList"
fun createMessagesToMeFilter() =
TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter =
Filter(
kinds = listOf(PrivateDmEvent.KIND),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
),
)
fun createMessagesFromMeFilter() =
TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter =
Filter(
kinds = listOf(PrivateDmEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
),
)
fun createChannelsCreatedbyMeFilter() =
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
kinds = listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
),
)
fun createMyChannelsFilter(): TypedFilter? {
val followingEvents = account.selectedChatsFollowList()
if (followingEvents.isEmpty()) return null
return TypedFilter(
// Metadata comes from any relay
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(ChannelCreateEvent.KIND),
ids = followingEvents.toList(),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
),
)
}
fun createLastChannelInfoFilter(): List<TypedFilter>? {
val followingEvents = account.selectedChatsFollowList()
if (followingEvents.isEmpty()) return null
return followingEvents.map {
TypedFilter(
// Metadata comes from any relay
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(ChannelMetadataEvent.KIND),
tags = mapOf("e" to listOf(it)),
limit = 1,
),
)
}
}
fun createLastMessageOfEachChannelFilter(): List<TypedFilter>? {
val followingEvents = account.selectedChatsFollowList()
if (followingEvents.isEmpty()) return null
return followingEvents.map {
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
kinds = listOf(ChannelMessageEvent.KIND),
tags = mapOf("e" to listOf(it)),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
// Remember to consider spam that is being removed from the UI
limit = 50,
),
)
}
}
val chatroomListChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), chatRoomList, relayUrl, time)
}
override fun updateChannelFilters() {
val list =
listOfNotNull(
createMessagesToMeFilter(),
createMessagesFromMeFilter(),
createMyChannelsFilter(),
)
chatroomListChannel.typedFilters =
listOfNotNull(
list,
createLastChannelInfoFilter(),
createLastMessageOfEachChannelFilter(),
).flatten()
.ifEmpty { null }
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") {
private var communityToWatch: AddressableNote? = null
private fun createLoadCommunityFilter(): TypedFilter? {
val myCommunityToWatch = communityToWatch ?: return null
val community = myCommunityToWatch.event as? CommunityDefinitionEvent ?: return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
authors =
community
.moderators()
.map { it.key }
.plus(listOfNotNull(myCommunityToWatch.author?.pubkeyHex)),
tags =
mapOf(
"a" to listOf(myCommunityToWatch.address.toTag()),
),
kinds = listOf(CommunityPostApprovalEvent.KIND),
limit = 500,
),
)
}
val loadCommunityChannel = requestNewChannel()
override fun updateChannelFilters() {
loadCommunityChannel.typedFilters = listOfNotNull(createLoadCommunityFilter()).ifEmpty { null }
}
fun loadCommunity(note: AddressableNote?) {
communityToWatch = note
invalidateFilters()
}
}

View File

@@ -0,0 +1,478 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
lateinit var account: Account
val scope = Amethyst.instance.applicationIOScope
val latestEOSEs = EOSEAccount()
var job: Job? = null
override fun start() {
job?.cancel()
job =
scope.launch(Dispatchers.IO) {
account.liveDiscoveryFollowLists.collect {
if (this@NostrDiscoveryDataSource::account.isInitialized) {
invalidateFilters()
}
}
}
super.start()
}
override fun stop() {
super.stop()
job?.cancel()
}
fun createMarketplaceFilter(): List<TypedFilter> {
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
val geohashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
return listOfNotNull(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
authors = follows,
kinds = listOf(ClassifiedsEvent.KIND),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
),
hashToLoad?.let {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(ClassifiedsEvent.KIND),
tags =
mapOf(
"t" to
it
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
},
geohashToLoad?.let {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(ClassifiedsEvent.KIND),
tags =
mapOf(
"g" to
it
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
},
)
}
fun createNIP89Filter(kTags: List<String>): List<TypedFilter> =
listOfNotNull(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(AppDefinitionEvent.KIND),
limit = 300,
tags = mapOf("k" to kTags),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
),
)
fun createLiveStreamFilter(): List<TypedFilter> {
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
return listOfNotNull(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
authors = follows,
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
),
follows?.let {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
tags = mapOf("p" to it),
kinds = listOf(LiveActivitiesEvent.KIND),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
},
)
}
fun createPublicChatFilter(): List<TypedFilter> {
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
val followChats = account.selectedChatsFollowList().toList()
return listOfNotNull(
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
authors = follows,
kinds = listOf(ChannelMessageEvent.KIND),
limit = 500,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
),
if (followChats.isNotEmpty()) {
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
ids = followChats,
kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
} else {
null
},
)
}
fun createCommunitiesFilter(): TypedFilter {
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
authors = follows,
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
}
fun createLiveStreamTagsFilter(): TypedFilter? {
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
tags =
mapOf(
"t" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
}
fun createLiveStreamGeohashesFilter(): TypedFilter? {
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
tags =
mapOf(
"g" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
}
fun createPublicChatsTagsFilter(): TypedFilter? {
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
kinds =
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
tags =
mapOf(
"t" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
}
fun createPublicChatsGeohashesFilter(): TypedFilter? {
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
kinds =
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
tags =
mapOf(
"g" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
}
fun createCommunitiesTagsFilter(): TypedFilter? {
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
tags =
mapOf(
"t" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
}
fun createCommunitiesGeohashesFilter(): TypedFilter? {
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
tags =
mapOf(
"g" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 300,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultDiscoveryFollowList.value)
?.relayList,
),
)
}
val discoveryFeedChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(
account.userProfile(),
account.defaultDiscoveryFollowList.value,
relayUrl,
time,
)
}
override fun updateChannelFilters() {
discoveryFeedChannel.typedFilters =
createLiveStreamFilter()
.plus(createNIP89Filter(listOf("5300")))
.plus(createPublicChatFilter())
.plus(createMarketplaceFilter())
.plus(
listOfNotNull(
createLiveStreamTagsFilter(),
createLiveStreamGeohashesFilter(),
createCommunitiesFilter(),
createCommunitiesTagsFilter(),
createCommunitiesGeohashesFilter(),
createPublicChatsTagsFilter(),
createPublicChatsGeohashesFilter(),
),
).toList()
.ifEmpty { null }
}
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrGeohashDataSource : AmethystNostrDataSource("SingleGeoHashFeed") {
private var geohashToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
val hashToLoad = geohashToWatch ?: return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
tags =
mapOf(
"g" to
listOf(
hashToLoad,
),
),
kinds =
listOf(
TextNoteEvent.KIND,
ChannelMessageEvent.KIND,
LongTextNoteEvent.KIND,
PollNoteEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
WikiNoteEvent.KIND,
),
limit = 200,
),
)
}
val loadGeohashChannel = requestNewChannel()
override fun updateChannelFilters() {
loadGeohashChannel.typedFilters = listOfNotNull(createLoadHashtagFilter()).ifEmpty { null }
}
fun loadHashtag(tag: String?) {
geohashToWatch = tag
invalidateFilters()
}
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrHashtagDataSource : AmethystNostrDataSource("SingleHashtagFeed") {
private var hashtagToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
val hashToLoad = hashtagToWatch ?: return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
tags =
mapOf(
"t" to
listOf(
hashToLoad,
hashToLoad.lowercase(),
hashToLoad.uppercase(),
hashToLoad.capitalize(),
),
),
kinds =
listOf(
TextNoteEvent.KIND,
ChannelMessageEvent.KIND,
LongTextNoteEvent.KIND,
PollNoteEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
WikiNoteEvent.KIND,
),
limit = 200,
),
)
}
val loadHashtagChannel = requestNewChannel()
override fun updateChannelFilters() {
loadHashtagChannel.typedFilters = listOfNotNull(createLoadHashtagFilter()).ifEmpty { null }
}
fun loadHashtag(tag: String?) {
hashtagToWatch = tag
invalidateFilters()
}
}

View File

@@ -0,0 +1,273 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
lateinit var account: Account
val scope = Amethyst.instance.applicationIOScope
val latestEOSEs = EOSEAccount()
var job: Job? = null
override fun start() {
job?.cancel()
job =
scope.launch(Dispatchers.IO) {
// creates cache on main
withContext(Dispatchers.Main) { account.userProfile().live() }
account.liveHomeFollowLists.collect {
if (this@NostrHomeDataSource::account.isInitialized) {
invalidateFilters()
}
}
}
super.start()
}
override fun stop() {
super.stop()
job?.cancel()
}
fun createFollowAccountsFilter(): TypedFilter {
val follows = account.liveHomeFollowLists.value?.users
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.toList()?.ifEmpty { null }
return TypedFilter(
types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS),
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ClassifiedsEvent.KIND,
LongTextNoteEvent.KIND,
PollNoteEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
PinListEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
LiveActivitiesEvent.KIND,
WikiNoteEvent.KIND,
),
authors = followSet,
limit = 400,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultHomeFollowList.value)
?.relayList,
),
)
}
fun createFollowMetadataAndReleaseFilter(): TypedFilter? {
val follows = account.liveHomeFollowLists.value?.users
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.shuffled()?.ifEmpty { null }
return if (followSet != null) {
TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter =
Filter(
kinds =
listOf(
MetadataEvent.KIND,
AdvertisedRelayListEvent.KIND,
),
authors = followSet.take(500),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultHomeFollowList.value)
?.relayList,
),
)
} else {
null
}
}
fun createFollowTagsFilter(): TypedFilter? {
val hashToLoad = account.liveHomeFollowLists.value?.hashtags ?: return null
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
WikiNoteEvent.KIND,
),
tags =
mapOf(
"t" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultHomeFollowList.value)
?.relayList,
),
)
}
fun createFollowGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveHomeFollowLists.value?.geotags ?: return null
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
WikiNoteEvent.KIND,
),
tags =
mapOf(
"g" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultHomeFollowList.value)
?.relayList,
),
)
}
fun createFollowCommunitiesFilter(): TypedFilter? {
val communitiesToLoad = account.liveHomeFollowLists.value?.communities ?: return null
if (communitiesToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
WikiNoteEvent.KIND,
CommunityPostApprovalEvent.KIND,
),
tags =
mapOf(
"a" to communitiesToLoad.toList(),
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultHomeFollowList.value)
?.relayList,
),
)
}
val followAccountChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(
account.userProfile(),
account.defaultHomeFollowList.value,
relayUrl,
time,
)
}
override fun updateChannelFilters() {
followAccountChannel.typedFilters =
listOfNotNull(
createFollowAccountsFilter(),
createFollowMetadataAndReleaseFilter(),
createFollowCommunitiesFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter(),
).ifEmpty { null }
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.signers.NostrSigner
class NostrLnZapPaymentResponseDataSource(
private val fromServiceHex: String,
private val toUserHex: String,
private val replyingToHex: String,
private val authSigner: NostrSigner,
) : AmethystNostrDataSource("LnZapPaymentResponseFeed") {
val feedTypes = setOf(FeedType.WALLET_CONNECT)
private fun createWalletConnectServiceWatcher(): TypedFilter {
// downloads all the reactions to a given event.
return TypedFilter(
types = feedTypes,
filter =
Filter(
kinds = listOf(LnZapPaymentResponseEvent.KIND),
authors = listOf(fromServiceHex),
tags =
mapOf(
"e" to listOf(replyingToHex),
"p" to listOf(toUserHex),
),
limit = 1,
),
)
}
val channel = requestNewChannel()
override fun updateChannelFilters() {
val wc = createWalletConnectServiceWatcher()
channel.typedFilters = listOfNotNull(wc).ifEmpty { null }
}
}

View File

@@ -0,0 +1,209 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexValidator
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.NNSEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlin.coroutines.cancellation.CancellationException
object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFeed") {
private var searchString: String? = null
private fun createAnythingWithIDFilter(): List<TypedFilter>? {
val mySearchString = searchString
if (mySearchString.isNullOrBlank()) {
return null
}
val hexToWatch =
try {
val isAStraightHex =
if (HexValidator.isHex(mySearchString)) {
Hex.decode(mySearchString).toHexKey()
} else {
null
}
when (val parsed = Nip19Bech32.uriToRoute(mySearchString)?.entity) {
is Nip19Bech32.NSec -> KeyPair(privKey = parsed.hex.bechToBytes()).pubKey.toHexKey()
is Nip19Bech32.NPub -> parsed.hex
is Nip19Bech32.NProfile -> parsed.hex
is Nip19Bech32.Note -> parsed.hex
is Nip19Bech32.NEvent -> parsed.hex
is Nip19Bech32.NEmbed -> parsed.event.id
is Nip19Bech32.NRelay -> null
is Nip19Bech32.NAddress -> parsed.atag
else -> isAStraightHex
}
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
val directReferenceFilters =
hexToWatch?.let {
if (it.contains(":")) {
// naddr
listOfNotNull(
ATag.parse(it, null)?.let { aTag ->
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(MetadataEvent.KIND, aTag.kind),
authors = listOfNotNull(aTag.pubKeyHex),
// just to be sure
limit = 5,
),
)
},
)
} else {
// event ids
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
ids = listOfNotNull(hexToWatch),
),
),
// authors
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOfNotNull(hexToWatch),
// just to be sure
limit = 5,
),
),
)
}
} ?: emptyList()
// downloads all the reactions to a given event.
return directReferenceFilters +
listOfNotNull(
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
Filter(
kinds = listOf(MetadataEvent.KIND),
search = mySearchString,
limit = 1000,
),
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
LongTextNoteEvent.KIND,
BadgeDefinitionEvent.KIND,
PeopleListEvent.KIND,
BookmarkListEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
PollNoteEvent.KIND,
ChannelCreateEvent.KIND,
),
search = mySearchString,
limit = 100,
),
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
Filter(
kinds =
listOf(
ChannelMetadataEvent.KIND,
ClassifiedsEvent.KIND,
CommunityDefinitionEvent.KIND,
EmojiPackEvent.KIND,
HighlightEvent.KIND,
LiveActivitiesEvent.KIND,
PollNoteEvent.KIND,
NNSEvent.KIND,
WikiNoteEvent.KIND,
),
search = mySearchString,
limit = 100,
),
),
)
}
val searchChannel = requestNewChannel()
override fun updateChannelFilters() {
searchChannel.typedFilters = createAnythingWithIDFilter()
}
fun search(searchString: String) {
if (this.searchString != searchString) {
println("DataSource: ${this.javaClass.simpleName} Search for $searchString")
this.searchString = searchString
invalidateFilters()
}
}
fun clear() {
if (searchString != null) {
println("DataSource: ${this.javaClass.simpleName} Clear")
searchString = null
invalidateFilters()
}
}
}

View File

@@ -0,0 +1,125 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed") {
private var channelsToWatch = setOf<Channel>()
private fun createMetadataChangeFilter(): TypedFilter? {
val reactionsToWatch = channelsToWatch.filter { it is PublicChatChannel }.map { it.idHex }
if (reactionsToWatch.isEmpty()) {
return null
}
// downloads all the reactions to a given event.
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
Filter(
kinds = listOf(ChannelMetadataEvent.KIND),
tags = mapOf("e" to reactionsToWatch),
),
)
}
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
val directEventsToLoad =
channelsToWatch.filter { it.notes.isEmpty() && it is PublicChatChannel }
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
if (interestedEvents.isEmpty()) {
return null
}
// downloads linked events to this event.
return TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(ChannelCreateEvent.KIND),
ids = interestedEvents.toList(),
),
)
}
fun createLoadStreamingIfNotLoadedFilter(): List<TypedFilter>? {
val directEventsToLoad =
channelsToWatch.filterIsInstance<LiveActivitiesChannel>().filter { it.info == null }
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
if (interestedEvents.isEmpty()) {
return null
}
// downloads linked events to this event.
return directEventsToLoad.map {
it.address().let { aTag ->
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex),
),
)
}
}
}
val singleChannelChannel = requestNewChannel()
override fun updateChannelFilters() {
val reactions = createMetadataChangeFilter()
val missing = createLoadEventsIfNotLoadedFilter()
val missingStreaming = createLoadStreamingIfNotLoadedFilter()
singleChannelChannel.typedFilters =
((listOfNotNull(reactions, missing)) + (missingStreaming ?: emptyList())).ifEmpty { null }
}
fun add(eventId: Channel) {
if (eventId !in channelsToWatch) {
channelsToWatch = channelsToWatch.plus(eventId)
invalidateFilters()
}
}
fun remove(eventId: Channel) {
if (eventId in channelsToWatch) {
channelsToWatch = channelsToWatch.minus(eventId)
invalidateFilters()
}
}
}

View File

@@ -0,0 +1,369 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.events.NIP90StatusEvent
import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
private var nextEventsToWatch = setOf<Note>()
private var nextAddressesToWatch = setOf<Note>()
private var eventsToWatchInProd = setOf<Note>()
private var addressesToWatchInProd = setOf<Note>()
private fun createReactionsToWatchInAddressFilter(): List<TypedFilter>? {
val myAddressesToWatch =
(
eventsToWatchInProd.filter { it.address() != null } +
addressesToWatchInProd.filter { it.address() != null }
).toSet()
if (myAddressesToWatch.isEmpty()) {
return null
}
return groupByEOSEPresence(myAddressesToWatch)
.map {
listOf(
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
ReactionEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ReportEvent.KIND,
LnZapEvent.KIND,
PollNoteEvent.KIND,
CommunityPostApprovalEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
),
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 1000,
),
),
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds =
listOf(
DeletionEvent.KIND,
),
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 10,
),
),
)
}.flatten()
}
private fun createAddressFilter(): List<TypedFilter>? {
val myAddressesToWatch = addressesToWatchInProd.filter { it.event == null }
if (myAddressesToWatch.isEmpty()) {
return null
}
return myAddressesToWatch.mapNotNull {
it.address()?.let { aTag ->
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(aTag.kind),
authors = listOf(aTag.pubKeyHex),
limit = 5,
),
)
} else {
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex),
limit = 5,
),
)
}
}
}
}
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
if (eventsToWatchInProd.isEmpty()) {
return null
}
return groupByEOSEPresence(eventsToWatchInProd)
.map {
listOf(
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
ReactionEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ReportEvent.KIND,
LnZapEvent.KIND,
PollNoteEvent.KIND,
OtsEvent.KIND,
TextNoteModificationEvent.KIND,
GitReplyEvent.KIND,
),
tags = mapOf("e" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 10000,
),
),
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds =
listOf(
DeletionEvent.KIND,
NIP90ContentDiscoveryResponseEvent.KIND,
NIP90StatusEvent.KIND,
),
tags = mapOf("e" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
limit = 100,
),
),
)
}.flatten()
}
private fun createQuotesFilter(): List<TypedFilter>? {
if (eventsToWatchInProd.isEmpty()) {
return null
}
return groupByEOSEPresence(eventsToWatchInProd)
.map {
listOf(
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(TextNoteEvent.KIND),
tags = mapOf("q" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 1000,
),
),
)
}.flatten()
}
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
val directEventsToLoad = eventsToWatchInProd.filter { it.event == null }
val threadingEventsToLoad =
eventsToWatchInProd
.mapNotNull { it.replyTo }
.flatten()
.filter { it !is AddressableNote && it.event == null }
val interestedEvents = (directEventsToLoad + threadingEventsToLoad).map { it.idHex }.toSet()
if (interestedEvents.isEmpty()) {
return null
}
// downloads linked events to this event.
return listOf(
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
ids = interestedEvents.toList(),
),
),
)
}
val singleEventChannel =
requestNewChannel { time, relayUrl ->
// Ignores EOSE if it is in the middle of a filter change.
if (changingFilters.get()) return@requestNewChannel
checkNotInMainThread()
eventsToWatchInProd.forEach {
val eose = it.lastReactionsDownloadTime[relayUrl]
if (eose == null) {
it.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
addressesToWatchInProd.forEach {
val eose = it.lastReactionsDownloadTime[relayUrl]
if (eose == null) {
it.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
invalidateFilters()
}
override fun updateChannelFilters() {
addressesToWatchInProd = nextAddressesToWatch
eventsToWatchInProd = nextEventsToWatch
val reactions = createRepliesAndReactionsFilter()
val missing = createLoadEventsIfNotLoadedFilter()
val addresses = createAddressFilter()
val addressReactions = createReactionsToWatchInAddressFilter()
val quotes = createQuotesFilter()
singleEventChannel.typedFilters =
listOfNotNull(missing, addresses, reactions, addressReactions, quotes).flatten().ifEmpty { null }
}
fun add(eventId: Note) {
if (!nextEventsToWatch.contains(eventId)) {
nextEventsToWatch = nextEventsToWatch.plus(eventId)
invalidateFilters()
}
}
fun remove(eventId: Note) {
if (nextEventsToWatch.contains(eventId)) {
nextEventsToWatch = nextEventsToWatch.minus(eventId)
invalidateFilters()
}
}
fun addAddress(addressableNote: Note) {
if (!nextAddressesToWatch.contains(addressableNote)) {
nextAddressesToWatch = nextAddressesToWatch.plus(addressableNote)
invalidateFilters()
}
}
fun removeAddress(addressableNote: Note) {
if (nextAddressesToWatch.contains(addressableNote)) {
nextAddressesToWatch = nextAddressesToWatch.minus(addressableNote)
invalidateFilters()
}
}
}
fun groupByEOSEPresence(notes: Set<Note>): Collection<List<Note>> =
notes
.groupBy {
it.lastReactionsDownloadTime.keys
.sorted()
.joinToString(",")
}.values
.map {
it.sortedBy { it.idHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
}
fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> =
users
.groupBy {
it.latestEOSEs.keys
.sorted()
.joinToString(",")
}.values
.map {
it.sortedBy { it.pubkeyHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
}
fun findMinimumEOSEs(notes: List<Note>): Map<String, EOSETime> {
val minLatestEOSEs = mutableMapOf<String, EOSETime>()
notes.forEach { note ->
note.lastReactionsDownloadTime.forEach {
val minEose = minLatestEOSEs[it.key]
if (minEose == null) {
minLatestEOSEs.put(it.key, EOSETime(it.value.time))
} else if (it.value.time < minEose.time) {
minEose.time = it.value.time
}
}
}
return minLatestEOSEs
}
fun findMinimumEOSEsForUsers(users: List<User>): Map<String, EOSETime> {
val minLatestEOSEs = mutableMapOf<String, EOSETime>()
users.forEach {
it.latestEOSEs.forEach {
val minEose = minLatestEOSEs[it.key]
if (minEose == null) {
minLatestEOSEs.put(it.key, EOSETime(it.value.time))
} else if (it.value.time < minEose.time) {
minEose.time = it.value.time
}
}
}
return minLatestEOSEs
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.StatusEvent
object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
private var usersToWatch = setOf<User>()
fun createUserMetadataFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null
val firstTimers = usersToWatch.filter { it.latestMetadata == null }.map { it.pubkeyHex }
if (firstTimers.isEmpty()) return null
return listOf(
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND),
authors = firstTimers,
),
),
)
}
fun createUserMetadataStatusReportFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null
val secondTimers = usersToWatch.filter { it.latestMetadata != null }
if (secondTimers.isEmpty()) return null
return groupByEOSEPresence(secondTimers)
.map { group ->
val groupIds = group.map { it.pubkeyHex }
val minEOSEs = findMinimumEOSEsForUsers(group)
listOf(
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND),
authors = groupIds,
since = minEOSEs,
),
),
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
Filter(
kinds = listOf(ReportEvent.KIND),
tags = mapOf("p" to groupIds),
since = minEOSEs,
),
),
)
}.flatten()
}
val userChannel =
requestNewChannel { time, relayUrl ->
checkNotInMainThread()
usersToWatch.forEach {
val eose = it.latestEOSEs[relayUrl]
if (eose == null) {
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
}
override fun updateChannelFilters() {
checkNotInMainThread()
userChannel.typedFilters =
listOfNotNull(
createUserMetadataFilter(),
createUserMetadataStatusReportFilter(),
).flatten()
.ifEmpty { null }
}
fun add(user: User) {
if (!usersToWatch.contains(user)) {
usersToWatch = usersToWatch.plus(user)
invalidateFilters()
}
}
fun remove(user: User) {
if (usersToWatch.contains(user)) {
usersToWatch = usersToWatch.minus(user)
invalidateFilters()
}
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.ThreadAssembler
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
object NostrThreadDataSource : AmethystNostrDataSource("SingleThreadFeed") {
private var eventToWatch: String? = null
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
val threadToLoad = eventToWatch ?: return null
val eventsToLoad =
ThreadAssembler()
.findThreadFor(threadToLoad)
.filter { it.event == null }
.map { it.idHex }
.toSet()
.ifEmpty { null }
?: return null
if (eventsToLoad.isEmpty()) return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
ids = eventsToLoad.toList(),
),
)
}
val loadEventsChannel =
requestNewChannel { _, _ ->
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
invalidateFilters()
}
override fun updateChannelFilters() {
loadEventsChannel.typedFilters =
listOfNotNull(createLoadEventsIfNotLoadedFilter()).ifEmpty { null }
}
fun loadThread(noteId: String?) {
if (eventToWatch != noteId) {
eventToWatch = noteId
invalidateFilters()
}
}
}

View File

@@ -0,0 +1,200 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AppRecommendationEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
var user: User? = null
fun loadUserProfile(user: User?) {
this.user = user
}
fun createUserInfoFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 1,
),
)
}
fun createUserPostsFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
GenericRepostEvent.KIND,
RepostEvent.KIND,
LongTextNoteEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
PinListEvent.KIND,
PollNoteEvent.KIND,
HighlightEvent.KIND,
WikiNoteEvent.KIND,
),
authors = listOf(it.pubkeyHex),
limit = 200,
),
)
}
fun createUserReceivedZapsFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(LnZapEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 200,
),
)
}
fun createFollowFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(ContactListEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 1,
),
)
}
fun createFollowersFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(ContactListEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)),
),
)
}
fun createAcceptedAwardsFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(BadgeProfilesEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 1,
),
)
}
fun createBookmarksFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds =
listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 100,
),
)
}
fun createProfileGalleryFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds =
listOf(ProfileGalleryEntryEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 1000,
),
)
}
fun createReceivedAwardsFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds = listOf(BadgeAwardEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 20,
),
)
}
val userInfoChannel = requestNewChannel()
override fun updateChannelFilters() {
userInfoChannel.typedFilters =
listOfNotNull(
createUserInfoFilter(),
createUserPostsFilter(),
createProfileGalleryFilter(),
createFollowFilter(),
createFollowersFilter(),
createUserReceivedZapsFilter(),
createAcceptedAwardsFilter(),
createReceivedAwardsFilter(),
createBookmarksFilter(),
).ifEmpty { null }
}
}

View File

@@ -0,0 +1,169 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif")
val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet()
object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
lateinit var account: Account
val scope = Amethyst.instance.applicationIOScope
val latestEOSEs = EOSEAccount()
var job: Job? = null
override fun start() {
job?.cancel()
job =
scope.launch(Dispatchers.IO) {
account.liveStoriesFollowLists.collect {
if (this@NostrVideoDataSource::account.isInitialized) {
invalidateFilters()
}
}
}
super.start()
}
override fun stop() {
super.stop()
job?.cancel()
}
fun createContextualFilter(): TypedFilter {
val follows =
account.liveStoriesFollowLists.value
?.users
?.toList()
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
authors = follows,
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
limit = 200,
tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultStoriesFollowList.value)
?.relayList,
),
)
}
fun createFollowTagsFilter(): TypedFilter? {
val hashToLoad =
account.liveStoriesFollowLists.value
?.hashtags
?.toList() ?: return null
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags =
mapOf(
"t" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultStoriesFollowList.value)
?.relayList,
),
)
}
fun createFollowGeohashesFilter(): TypedFilter? {
val hashToLoad =
account.liveStoriesFollowLists.value
?.geotags
?.toList() ?: return null
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
Filter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags =
mapOf(
"g" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.defaultStoriesFollowList.value)
?.relayList,
),
)
}
val videoFeedChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(
account.userProfile(),
account.defaultStoriesFollowList.value,
relayUrl,
time,
)
}
override fun updateChannelFilters() {
videoFeedChannel.typedFilters =
listOfNotNull(
createContextualFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter(),
).ifEmpty { null }
}
}

View File

@@ -0,0 +1,111 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.crypto.CryptoUtils
import okhttp3.EventListener
import okhttp3.Protocol
import okhttp3.Request
import okio.ByteString.Companion.toByteString
import kotlin.coroutines.cancellation.CancellationException
@Immutable data class OnlineCheckResult(
val timeInMs: Long,
val online: Boolean,
)
object OnlineChecker {
val checkOnlineCache = LruCache<String, OnlineCheckResult>(100)
val fiveMinutes = 1000 * 60 * 5
fun isOnlineCached(url: String?): Boolean {
if (url.isNullOrBlank()) return false
if ((checkOnlineCache.get(url)?.timeInMs ?: 0) > System.currentTimeMillis() - fiveMinutes) {
return checkOnlineCache.get(url).online
}
return false
}
fun isOnline(url: String?): Boolean {
checkNotInMainThread()
if (url.isNullOrBlank()) return false
if ((checkOnlineCache.get(url)?.timeInMs ?: 0) > System.currentTimeMillis() - fiveMinutes) {
return checkOnlineCache.get(url).online
}
return try {
val result =
if (url.startsWith("wss")) {
val request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url.replace("wss+livekit://", "wss://"))
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Key", CryptoUtils.random(16).toByteString().base64())
.header("Sec-WebSocket-Version", "13")
.header("Sec-WebSocket-Extensions", "permessage-deflate")
.build()
val client =
HttpClientManager
.getHttpClient()
.newBuilder()
.eventListener(EventListener.NONE)
.protocols(listOf(Protocol.HTTP_1_1))
.build()
client.newCall(request).execute().use {
checkNotInMainThread()
it.isSuccessful
}
} else {
val request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()
.build()
HttpClientManager.getHttpClient().newCall(request).execute().use {
checkNotInMainThread()
it.isSuccessful
}
}
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result))
result
} catch (e: Exception) {
if (e is CancellationException) throw e
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), false))
Log.e("LiveActivities", "Failed to check streaming url $url", e)
false
}
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.content.Context
import android.content.Intent
import android.net.Uri
object PackageUtils {
private fun isPackageInstalled(
context: Context,
target: String,
): Boolean =
context.packageManager.getInstalledApplications(0).find { info ->
info.packageName == target
} != null
fun isOrbotInstalled(context: Context): Boolean = isPackageInstalled(context, "org.torproject.android")
fun isExternalSignerInstalled(context: Context): Boolean {
val intent =
Intent().apply {
action = Intent.ACTION_VIEW
data = Uri.parse("nostrsigner:")
}
val infos = context.packageManager.queryIntentActivities(intent, 0)
return infos.size > 0
}
}

View File

@@ -0,0 +1,385 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.content.Context
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.screen.loggedIn.collectSuccessfulSigningOperations
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.ZapSplitSetup
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.math.round
class ZapPaymentHandler(
val account: Account,
) {
@Immutable
data class Payable(
val info: ZapSplitSetup,
val user: User?,
val amountMilliSats: Long,
val invoice: String,
)
suspend fun zap(
note: Note,
amountMilliSats: Long,
pollOption: Int?,
message: String,
context: Context,
showErrorIfNoLnAddress: Boolean,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
zapType: LnZapEvent.ZapType,
) = withContext(Dispatchers.IO) {
val noteEvent = note.event
val zapSplitSetup = noteEvent?.zapSplitSetup()
val zapsToSend =
if (!zapSplitSetup.isNullOrEmpty()) {
zapSplitSetup
} else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) {
noteEvent.hosts().map { ZapSplitSetup(it, null, weight = 1.0, false) }
} else if (noteEvent is AppDefinitionEvent) {
val appLud16 = noteEvent.appMetaData()?.lnAddress()
if (appLud16 != null) {
listOf(ZapSplitSetup(appLud16, null, weight = 1.0, true))
} else {
val lud16 = note.author?.info?.lnAddress()
if (lud16.isNullOrBlank()) {
onError(
stringRes(context, R.string.missing_lud16),
stringRes(
context,
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
),
)
return@withContext
}
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
}
} else {
val lud16 = note.author?.info?.lnAddress()
if (lud16.isNullOrBlank()) {
onError(
stringRes(context, R.string.missing_lud16),
stringRes(
context,
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
),
)
return@withContext
}
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
}
onProgress(0.02f)
signAllZapRequests(note, pollOption, message, zapType, zapsToSend) { splitZapRequestPairs ->
if (splitZapRequestPairs.isEmpty()) {
onProgress(0.00f)
return@signAllZapRequests
} else {
onProgress(0.05f)
}
assembleAllInvoices(splitZapRequestPairs.toList(), amountMilliSats, message, showErrorIfNoLnAddress, onError, onProgress = {
onProgress(it * 0.7f + 0.05f) // keeps within range.
}, context) {
if (it.isEmpty()) {
onProgress(0.00f)
return@assembleAllInvoices
} else {
onProgress(0.75f)
}
if (account.hasWalletConnectSetup()) {
payViaNWC(it.values.map { it.invoice }, note, onError, onProgress = {
onProgress(it * 0.25f + 0.75f) // keeps within range.
}, context) {
// onProgress(1f)
}
} else {
onPayViaIntent(
it
.map {
Payable(
info = it.key.first,
user = it.key.second.user,
amountMilliSats = it.value.zapValue,
invoice = it.value.invoice,
)
}.toImmutableList(),
)
onProgress(0f)
}
}
}
}
private fun calculateZapValue(
amountMilliSats: Long,
weight: Double,
totalWeight: Double,
): Long {
val shareValue = amountMilliSats * (weight / totalWeight)
val roundedZapValue = round(shareValue / 1000f).toLong() * 1000
return roundedZapValue
}
class SignAllZapRequestsReturn(
val zapRequestJson: String,
val user: User? = null,
)
suspend fun signAllZapRequests(
note: Note,
pollOption: Int?,
message: String,
zapType: LnZapEvent.ZapType,
zapsToSend: List<ZapSplitSetup>,
onAllDone: suspend (MutableMap<ZapSplitSetup, SignAllZapRequestsReturn>) -> Unit,
) {
val authorRelayList =
note.author
?.pubkeyHex
?.let {
(
LocalCache
.getAddressableNoteIfExists(
AdvertisedRelayListEvent.createAddressTag(it),
)?.event as? AdvertisedRelayListEvent?
)?.readRelays()
}?.toSet()
collectSuccessfulSigningOperations<ZapSplitSetup, SignAllZapRequestsReturn>(
operationsInput = zapsToSend,
runRequestFor = { next: ZapSplitSetup, onReady ->
if (next.isLnAddress) {
prepareZapRequestIfNeeded(note, pollOption, message, zapType) { zapRequestJson ->
if (zapRequestJson != null) {
onReady(SignAllZapRequestsReturn(zapRequestJson))
}
}
} else {
val user = LocalCache.getUserIfExists(next.lnAddressOrPubKeyHex)
val userRelayList =
(
(
LocalCache
.getAddressableNoteIfExists(
AdvertisedRelayListEvent.createAddressTag(next.lnAddressOrPubKeyHex),
)?.event as? AdvertisedRelayListEvent?
)?.readRelays()?.toSet() ?: emptySet()
) + (authorRelayList ?: emptySet())
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList) { zapRequestJson ->
if (zapRequestJson != null) {
onReady(SignAllZapRequestsReturn(zapRequestJson, user))
}
}
}
},
onReady = onAllDone,
)
}
suspend fun assembleAllInvoices(
invoices: List<Pair<ZapSplitSetup, SignAllZapRequestsReturn>>,
totalAmountMilliSats: Long,
message: String,
showErrorIfNoLnAddress: Boolean,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
context: Context,
onAllDone: suspend (MutableMap<Pair<ZapSplitSetup, SignAllZapRequestsReturn>, AssembleInvoiceReturn>) -> Unit,
) {
var progressAllPayments = 0.00f
val totalWeight = invoices.sumOf { it.first.weight }
collectSuccessfulSigningOperations<Pair<ZapSplitSetup, SignAllZapRequestsReturn>, AssembleInvoiceReturn>(
operationsInput = invoices,
runRequestFor = { splitZapRequestPair: Pair<ZapSplitSetup, SignAllZapRequestsReturn>, onReady ->
assembleInvoice(
splitSetup = splitZapRequestPair.first,
nostrZapRequest = splitZapRequestPair.second.zapRequestJson,
zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.first.weight, totalWeight),
message = message,
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
onError = onError,
onProgressStep = { percentStepForThisPayment ->
progressAllPayments += percentStepForThisPayment / invoices.size
onProgress(progressAllPayments)
},
context = context,
onReady = onReady,
)
},
onReady = onAllDone,
)
}
suspend fun payViaNWC(
invoices: List<String>,
note: Note,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
context: Context,
onAllDone: suspend (MutableMap<String, Boolean>) -> Unit,
) {
var progressAllPayments = 0.00f
collectSuccessfulSigningOperations<String, Boolean>(
operationsInput = invoices,
runRequestFor = { invoice: String, onReady ->
account.sendZapPaymentRequestFor(
bolt11 = invoice,
zappedNote = note,
onSent = {
progressAllPayments += 0.5f / invoices.size
onProgress(progressAllPayments)
onReady(true)
},
onResponse = { response ->
if (response is PayInvoiceErrorResponse) {
progressAllPayments += 0.5f / invoices.size
onProgress(progressAllPayments)
onError(
stringRes(context, R.string.error_dialog_pay_invoice_error),
stringRes(
context,
R.string.wallet_connect_pay_invoice_error_error,
response.error?.message
?: response.error?.code?.toString() ?: "Error parsing error message",
),
)
} else {
progressAllPayments += 0.5f / invoices.size
onProgress(progressAllPayments)
}
},
)
},
onReady = onAllDone,
)
}
class AssembleInvoiceReturn(
val zapValue: Long,
val invoice: String,
)
private fun assembleInvoice(
splitSetup: ZapSplitSetup,
nostrZapRequest: String,
zapValue: Long,
message: String,
showErrorIfNoLnAddress: Boolean = true,
onError: (String, String) -> Unit,
onProgressStep: (percent: Float) -> Unit,
context: Context,
onReady: (AssembleInvoiceReturn) -> Unit,
) {
var progressThisPayment = 0.00f
var user: User? = null
val lud16 =
if (splitSetup.isLnAddress) {
splitSetup.lnAddressOrPubKeyHex
} else {
user = LocalCache.getUserIfExists(splitSetup.lnAddressOrPubKeyHex)
user?.info?.lnAddress()
}
if (lud16 != null) {
LightningAddressResolver()
.lnAddressInvoice(
lnaddress = lud16,
milliSats = zapValue,
message = message,
nostrRequest = nostrZapRequest,
onError = onError,
onProgress = {
val step = it - progressThisPayment
progressThisPayment = it
onProgressStep(step)
},
context = context,
onSuccess = {
onProgressStep(1 - progressThisPayment)
onReady(AssembleInvoiceReturn(zapValue, it))
},
)
} else {
if (showErrorIfNoLnAddress) {
onError(
stringRes(
context,
R.string.missing_lud16,
),
stringRes(
context,
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
user?.toBestDisplayName() ?: splitSetup.lnAddressOrPubKeyHex,
),
)
}
}
}
private fun prepareZapRequestIfNeeded(
note: Note,
pollOption: Int?,
message: String,
zapType: LnZapEvent.ZapType,
overrideUser: User? = null,
additionalRelays: Set<String>? = null,
onReady: (String?) -> Unit,
) {
if (zapType != LnZapEvent.ZapType.NONZAP) {
account.createZapRequestFor(note, pollOption, message, zapType, overrideUser, additionalRelays) { zapRequest ->
println("Zap Request " + zapRequest.toJson())
onReady(zapRequest.toJson())
}
} else {
onReady(null)
}
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.lnurl
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import kotlinx.coroutines.CancellationException
import java.text.NumberFormat
@Stable
data class InvoiceAmount(
val invoice: String,
val amount: String?,
)
object CachedLnInvoiceParser {
val lnInvoicesCache = LruCache<String, InvoiceAmount>(20)
fun cached(lnurl: String): InvoiceAmount? = lnInvoicesCache[lnurl]
fun parse(lnbcWord: String): InvoiceAmount? {
val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord)
if (myInvoice != null) {
val myInvoiceAmount =
try {
NumberFormat.getInstance().format(LnInvoiceUtil.getAmountInSats(myInvoice))
} catch (e: Exception) {
if (e is CancellationException) throw e
e.printStackTrace()
null
}
val lnInvoice = InvoiceAmount(myInvoice, myInvoiceAmount)
lnInvoicesCache.put(lnbcWord, lnInvoice)
return lnInvoice
}
return null
}
}

View File

@@ -0,0 +1,298 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.lnurl
import android.content.Context
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06
import okhttp3.Request
import java.math.BigDecimal
import java.math.RoundingMode
import java.net.URLEncoder
import kotlin.coroutines.cancellation.CancellationException
class LightningAddressResolver {
val client = HttpClientManager.getHttpClient()
fun assembleUrl(lnaddress: String): String? {
val parts = lnaddress.split("@")
if (parts.size == 2) {
return "https://${parts[1]}/.well-known/lnurlp/${parts[0]}"
}
if (lnaddress.lowercase().startsWith("lnurl")) {
return Lud06().toLnUrlp(lnaddress)
}
return null
}
private fun fetchLightningAddressJson(
lnaddress: String,
onSuccess: (String) -> Unit,
onError: (String, String) -> Unit,
context: Context,
) {
checkNotInMainThread()
val url = assembleUrl(lnaddress)
if (url == null) {
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup,
lnaddress,
),
)
return
}
try {
val request: Request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string
.the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct,
url,
lnaddress,
it.code.toString(),
),
)
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
e.printStackTrace()
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string
.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception,
url,
lnaddress,
e.suppressedExceptions.getOrNull(0)?.message ?: e.cause?.message ?: e.message,
),
)
}
}
fun fetchLightningInvoice(
lnCallback: String,
milliSats: Long,
message: String,
nostrRequest: String? = null,
onSuccess: (String) -> Unit,
onError: (String, String) -> Unit,
context: Context,
) {
checkNotInMainThread()
val encodedMessage = URLEncoder.encode(message, "utf-8")
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
var url = "$lnCallback${urlBinder}amount=$milliSats&comment=$encodedMessage"
if (nostrRequest != null) {
val encodedNostrRequest = URLEncoder.encode(nostrRequest, "utf-8")
url += "&nostr=$encodedNostrRequest"
}
val request: Request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(context, R.string.could_not_fetch_invoice_from, lnCallback),
)
}
}
}
fun lnAddressInvoice(
lnaddress: String,
milliSats: Long,
message: String,
nostrRequest: String? = null,
onSuccess: (String) -> Unit,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
context: Context,
) {
val mapper = jacksonObjectMapper()
fetchLightningAddressJson(
lnaddress,
onSuccess = { lnAddressJson ->
onProgress(0.4f)
val lnurlp =
try {
mapper.readTree(lnAddressJson)
} catch (t: Throwable) {
if (t is CancellationException) throw t
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user,
lnaddress,
),
)
null
}
val callback = lnurlp?.get("callback")?.asText()
if (callback == null) {
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user,
lnaddress,
),
)
}
val allowsNostr = lnurlp?.get("allowsNostr")?.asBoolean() ?: false
callback?.let { cb ->
fetchLightningInvoice(
cb,
milliSats,
message,
if (allowsNostr) nostrRequest else null,
onSuccess = {
onProgress(0.6f)
val lnInvoice =
try {
mapper.readTree(it)
} catch (t: Throwable) {
if (t is CancellationException) throw t
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string
.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user,
lnaddress,
),
)
null
}
lnInvoice
?.get("pr")
?.asText()
?.ifBlank { null }
?.let { pr ->
// Forces LN Invoice amount to be the requested amount.
val expectedAmountInSats =
BigDecimal(milliSats).divide(BigDecimal(1000), RoundingMode.HALF_UP).toLong()
val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr)
if (invoiceAmount.toLong() == expectedAmountInSats) {
onProgress(0.7f)
onSuccess(pr)
} else {
onProgress(0.0f)
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string.incorrect_invoice_amount_sats_from_it_should_have_been,
invoiceAmount.toLong().toString(),
lnaddress,
expectedAmountInSats.toString(),
),
)
}
}
?: lnInvoice
?.get("reason")
?.asText()
?.ifBlank { null }
?.let { reason ->
onProgress(0.0f)
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string
.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user,
lnaddress,
reason,
),
)
}
?: run {
onProgress(0.0f)
onError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(
context,
R.string
.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user,
lnaddress,
),
)
}
},
onError = onError,
context,
)
}
},
onError = onError,
context,
)
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.events.zaps
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.ZapReqResponse
import com.vitorpamplona.quartz.events.LnZapEventInterface
object UserZaps {
fun forProfileFeed(zaps: Map<Note, Note?>?): List<ZapReqResponse> {
if (zaps == null) return emptyList()
return (
zaps
.mapNotNull { entry -> entry.value?.let { ZapReqResponse(entry.key, it) } }
.sortedBy { (it.zapEvent.event as? LnZapEventInterface)?.amount() }
.reversed()
)
}
}

View File

@@ -0,0 +1,328 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.notifications
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.persistentSetOf
import java.math.BigDecimal
class EventNotificationConsumer(
private val applicationContext: Context,
) {
suspend fun consume(event: GiftWrapEvent) {
Log.d("EventNotificationConsumer", "New Notification Arrived")
if (!LocalCache.justVerify(event)) return
if (!notificationManager().areNotificationsEnabled()) return
// PushNotification Wraps don't include a receiver.
// Test with all logged in accounts
var matchAccount = false
LocalPreferences.allSavedAccounts().forEach {
if (!matchAccount && it.hasPrivKey || it.loggedInWithExternalSigner) {
LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc ->
Log.d("EventNotificationConsumer", "New Notification Testing if for ${it.npub}")
try {
consumeIfMatchesAccount(event, acc)
matchAccount = true
} catch (e: Exception) {
// Not for this account.
}
}
}
}
}
private suspend fun consumeIfMatchesAccount(
pushWrappedEvent: GiftWrapEvent,
account: Account,
) {
pushWrappedEvent.cachedGift(account.signer) { notificationEvent ->
val consumed = LocalCache.hasConsumed(notificationEvent)
val verified = LocalCache.justVerify(notificationEvent)
Log.d("EventNotificationConsumer", "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.userProfile().toBestDisplayName()} consumed= $consumed && verified= $verified")
if (!consumed && verified) {
Log.d("EventNotificationConsumer", "New Notification was verified")
unwrapAndConsume(notificationEvent, account) { innerEvent ->
Log.d("EventNotificationConsumer", "Unwrapped consume $consumed ${innerEvent.javaClass.simpleName}")
if (!consumed) {
if (innerEvent is PrivateDmEvent) {
Log.d("EventNotificationConsumer", "New Nip-04 DM to Notify")
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
Log.d("EventNotificationConsumer", "New Zap to Notify")
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
Log.d("EventNotificationConsumer", "New ChatMessage to Notify")
notify(innerEvent, account)
}
}
}
}
}
}
private fun unwrapAndConsume(
event: Event,
account: Account,
onReady: (Event) -> Unit,
) {
if (!LocalCache.justVerify(event)) return
if (LocalCache.hasConsumed(event)) return
when (event) {
is GiftWrapEvent -> {
event.cachedGift(account.signer) { unwrapAndConsume(it, account, onReady) }
}
is SealedGossipEvent -> {
event.cachedGossip(account.signer) {
if (!LocalCache.hasConsumed(it)) {
// this is not verifiable
LocalCache.justConsume(it, null)
onReady(it)
}
}
}
else -> {
LocalCache.justConsume(event, null)
onReady(event)
}
}
}
private fun notify(
event: ChatMessageEvent,
acc: Account,
) {
if (
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
// old event being re-broadcasted
event.pubKey != acc.userProfile().pubkeyHex
) { // from the user
val chatNote = LocalCache.getNoteIfExists(event.id) ?: return
val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey())
val followingKeySet = acc.followingKeySet()
val isKnownRoom =
(
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(chatRoom)
) &&
!acc.isAllHidden(chatRoom.users)
if (isKnownRoom) {
val content = chatNote.event?.content() ?: ""
val user = chatNote.author?.toBestDisplayName() ?: ""
val userPicture = chatNote.author?.profilePicture()
val noteUri = chatNote.toNEvent()
notificationManager()
.sendDMNotification(
event.id,
content,
user,
event.createdAt,
userPicture,
noteUri,
applicationContext,
)
}
}
}
private fun notify(
event: PrivateDmEvent,
acc: Account,
) {
val note = LocalCache.getNoteIfExists(event.id) ?: return
// old event being re-broadcast
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
if (acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
val followingKeySet = acc.followingKeySet()
val knownChatrooms =
acc
.userProfile()
.privateChatrooms
.keys
.filter {
(
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(it)
) &&
!acc.isAllHidden(it.users)
}.toSet()
note.author?.let {
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
acc.decryptContent(note) { content ->
val user = note.author?.toBestDisplayName() ?: ""
val userPicture = note.author?.profilePicture()
val noteUri = note.toNEvent()
notificationManager()
.sendDMNotification(event.id, content, user, event.createdAt, userPicture, noteUri, applicationContext)
}
}
}
}
}
private fun notify(
event: LnZapEvent,
acc: Account,
) {
Log.d("EventNotificationConsumer", "Notify Start ${event.toNostrUri()}")
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
Log.d("EventNotificationConsumer", "Notify Not Notified Yet")
// old event being re-broadcast
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
Log.d("EventNotificationConsumer", "Notify Not an old event")
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
val noteZapped =
event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
Log.d("EventNotificationConsumer", "Notify ZapRequest $noteZapRequest zapped $noteZapped")
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
Log.d("EventNotificationConsumer", "Notify Amount Bigger than 10")
if (event.isTaggedUser(acc.userProfile().pubkeyHex)) {
val amount = showAmount(event.amount)
Log.d("EventNotificationConsumer", "Notify Amount $amount")
(noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
acc.decryptZapContentAuthor(noteZapRequest) {
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Zap ${event.id}")
val author = LocalCache.getOrCreateUser(it.pubKey)
val senderInfo = Pair(author, it.content.ifBlank { null })
if (noteZapped.event?.content() != null) {
acc.decryptContent(noteZapped) {
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Note")
val zappedContent = it.split("\n").get(0)
val user = senderInfo.first.toBestDisplayName()
var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
var content =
stringRes(
applicationContext,
R.string.app_notification_zaps_channel_message_from,
user,
)
zappedContent.let {
content +=
" " +
stringRes(
applicationContext,
R.string.app_notification_zaps_channel_message_for,
zappedContent,
)
}
val userPicture = senderInfo?.first?.profilePicture()
val noteUri = "nostr:Notifications"
Log.d("EventNotificationConsumer", "Notify ${event.id} $content $title $noteUri")
notificationManager()
.sendZapNotification(
event.id,
content,
title,
event.createdAt,
userPicture,
noteUri,
applicationContext,
)
}
} else {
// doesn't have a base note to refer to.
Log.d("EventNotificationConsumer", "Notify Zapped note not available")
val user = senderInfo.first.toBestDisplayName()
var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
val content =
stringRes(
applicationContext,
R.string.app_notification_zaps_channel_message_from,
user,
)
val userPicture = senderInfo.first.profilePicture()
val noteUri = "nostr:Notifications"
Log.d("EventNotificationConsumer", "Notify ${event.id} $content $title $noteUri")
notificationManager()
.sendZapNotification(
event.id,
content,
title,
event.createdAt,
userPicture,
noteUri,
applicationContext,
)
}
}
}
}
}
fun notificationManager(): NotificationManager =
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
as NotificationManager
}

View File

@@ -0,0 +1,257 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.notifications
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
import coil.ImageLoader
import coil.executeBlocking
import coil.request.ImageRequest
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.amethyst.ui.stringRes
object NotificationUtils {
private var dmChannel: NotificationChannel? = null
private var zapChannel: NotificationChannel? = null
private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION"
private const val ZAP_GROUP_KEY = "com.vitorpamplona.amethyst.ZAP_NOTIFICATION"
fun NotificationManager.getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
if (dmChannel != null) return dmChannel!!
dmChannel =
NotificationChannel(
stringRes(applicationContext, R.string.app_notification_dms_channel_id),
stringRes(applicationContext, R.string.app_notification_dms_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
).apply {
description =
stringRes(applicationContext, R.string.app_notification_dms_channel_description)
}
// Register the channel with the system
val notificationManager: NotificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(dmChannel!!)
return dmChannel!!
}
fun NotificationManager.getOrCreateZapChannel(applicationContext: Context): NotificationChannel {
if (zapChannel != null) return zapChannel!!
zapChannel =
NotificationChannel(
stringRes(applicationContext, R.string.app_notification_zaps_channel_id),
stringRes(applicationContext, R.string.app_notification_zaps_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
).apply {
description =
stringRes(applicationContext, R.string.app_notification_zaps_channel_description)
}
// Register the channel with the system
val notificationManager: NotificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(zapChannel!!)
return zapChannel!!
}
fun NotificationManager.sendZapNotification(
id: String,
messageBody: String,
messageTitle: String,
time: Long,
pictureUrl: String?,
uri: String,
applicationContext: Context,
) {
val zapChannel = getOrCreateZapChannel(applicationContext)
val channelId = stringRes(applicationContext, R.string.app_notification_zaps_channel_id)
sendNotification(
id,
messageBody,
messageTitle,
time,
pictureUrl,
uri,
channelId,
ZAP_GROUP_KEY,
applicationContext,
)
}
fun NotificationManager.sendDMNotification(
id: String,
messageBody: String,
messageTitle: String,
time: Long,
pictureUrl: String?,
uri: String,
applicationContext: Context,
) {
val dmChannel = getOrCreateDMChannel(applicationContext)
val channelId = stringRes(applicationContext, R.string.app_notification_dms_channel_id)
sendNotification(
id,
messageBody,
messageTitle,
time,
pictureUrl,
uri,
channelId,
DM_GROUP_KEY,
applicationContext,
)
}
fun NotificationManager.sendNotification(
id: String,
messageBody: String,
messageTitle: String,
time: Long,
pictureUrl: String?,
uri: String,
channelId: String,
notificationGroupKey: String,
applicationContext: Context,
) {
if (pictureUrl != null) {
val request = ImageRequest.Builder(applicationContext).data(pictureUrl).build()
val imageLoader = ImageLoader(applicationContext)
val imageResult = imageLoader.executeBlocking(request)
sendNotification(
id = id,
messageBody = messageBody,
messageTitle = messageTitle,
time = time,
picture = imageResult.drawable as? BitmapDrawable,
uri = uri,
channelId,
notificationGroupKey,
applicationContext = applicationContext,
)
} else {
sendNotification(
id = id,
messageBody = messageBody,
messageTitle = messageTitle,
time = time,
picture = null,
uri = uri,
channelId,
notificationGroupKey,
applicationContext = applicationContext,
)
}
}
private fun NotificationManager.sendNotification(
id: String,
messageBody: String,
messageTitle: String,
time: Long,
picture: BitmapDrawable?,
uri: String,
channelId: String,
notificationGroupKey: String,
applicationContext: Context,
) {
val notId = id.hashCode()
// dont notify twice
val notifications: Array<StatusBarNotification> = getActiveNotifications()
for (notification in notifications) {
if (notification.id == notId) {
return
}
}
val contentIntent =
Intent(applicationContext, MainActivity::class.java).apply { data = Uri.parse(uri) }
val contentPendingIntent =
PendingIntent.getActivity(
applicationContext,
notId,
contentIntent,
PendingIntent.FLAG_MUTABLE,
)
// Build the notification
val builderPublic =
NotificationCompat
.Builder(
applicationContext,
channelId,
).setSmallIcon(R.drawable.amethyst)
.setContentTitle(messageTitle)
.setContentText(stringRes(applicationContext, R.string.app_notification_private_message))
.setLargeIcon(picture?.bitmap)
// .setGroup(messageTitle)
// .setGroup(notificationGroupKey) //-> Might need a Group summary as well before we
// activate this
.setContentIntent(contentPendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setWhen(time * 1000)
// Build the notification
val builder =
NotificationCompat
.Builder(
applicationContext,
channelId,
).setSmallIcon(R.drawable.amethyst)
.setContentTitle(messageTitle)
.setContentText(messageBody)
.setLargeIcon(picture?.bitmap)
// .setGroup(messageTitle)
// .setGroup(notificationGroupKey) //-> Might need a Group summary as well before we
// activate this
.setContentIntent(contentPendingIntent)
.setPublicVersion(builderPublic.build())
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setWhen(time * 1000)
notify(notId, builder.build())
}
/** Cancels all notifications. */
fun NotificationManager.cancelNotifications() {
cancelAll()
}
}

View File

@@ -0,0 +1,185 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.notifications
import android.util.Log
import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.RelayAuthEvent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import kotlin.coroutines.resume
class RegisterAccounts(
private val accounts: List<AccountInfo>,
) {
val tag =
if (BuildConfig.FLAVOR == "play") {
"RegisterAccounts FirebaseMsgService"
} else {
"RegisterAccounts UnifiedPushService"
}
private suspend fun signAllAuths(
notificationToken: String,
remainingTos: List<Pair<Account, String>>,
output: MutableList<RelayAuthEvent>,
onReady: (List<RelayAuthEvent>) -> Unit,
) {
if (remainingTos.isEmpty()) {
onReady(output)
return
}
coroutineScope {
val jobs =
remainingTos.map { accountRelayPair ->
async {
val result =
withTimeoutOrNull(10000) {
suspendCancellableCoroutine { continuation ->
accountRelayPair.first.createAuthEvent(accountRelayPair.second, notificationToken) { result ->
continuation.resume(result)
}
}
}
if (result != null) {
output.add(result)
}
}
}
// runs in parallel to avoid overcrowding Amber.
withTimeoutOrNull(15000) {
jobs.joinAll()
}
}
onReady(output)
}
// creates proof that it controls all accounts
private suspend fun signEventsToProveControlOfAccounts(
accounts: List<AccountInfo>,
notificationToken: String,
onReady: (List<RelayAuthEvent>) -> Unit,
) {
val readyToSend =
accounts
.map {
Log.d(tag, "Register Account ${it.npub}")
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
if (acc != null && acc.isWriteable()) {
val nip65Read =
(
LocalCache
.getAddressableNoteIfExists(
AdvertisedRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
)?.event as? AdvertisedRelayListEvent
)?.readRelays() ?: acc.backupNIP65RelayList?.readRelays() ?: emptyList<String>()
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP65 Reads ${nip65Read.joinToString(", ")}")
val nip17Read =
(
LocalCache
.getAddressableNoteIfExists(
ChatMessageRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
)?.event as? ChatMessageRelayListEvent
)?.relays() ?: acc.backupDMRelayList?.relays() ?: emptyList<String>()
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP17 Reads ${nip17Read.joinToString(", ")}")
val kind3Relays = (acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays())
val readKind3Relays = kind3Relays?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList<String>()
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
(nip65Read + nip17Read + readKind3Relays).map {
Pair(acc, it)
}
} else {
emptyList<Pair<Account, String>>()
}
}.flatten()
val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
signAllAuths(
notificationToken,
readyToSend,
listOfAuthEvents,
onReady,
)
}
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
try {
val jsonObject =
"""{
"events": [ ${events.joinToString(", ") { it.toJson() }} ]
}
"""
val mediaType = "application/json; charset=utf-8".toMediaType()
val body = jsonObject.toRequestBody(mediaType)
val request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url("https://push.amethyst.social/register")
.post(body)
.build()
val client = HttpClientManager.getHttpClient()
val isSucess = client.newCall(request).execute().use { it.isSuccessful }
} catch (e: java.lang.Exception) {
if (e is CancellationException) throw e
Log.e(tag, "Unable to register with push server", e)
}
}
suspend fun go(notificationToken: String) =
withContext(Dispatchers.IO) {
signEventsToProveControlOfAccounts(accounts, notificationToken) { postRegistrationEvent(it) }
PushNotificationUtils.hasInit = true
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.ots
import android.util.Log
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.ots.BitcoinExplorer
import com.vitorpamplona.quartz.ots.BlockHeader
import com.vitorpamplona.quartz.ots.exceptions.UrlException
import okhttp3.Request
class OkHttpBlockstreamExplorer : BitcoinExplorer {
/**
* Retrieve the block information from the block hash.
*
* @param hash Hash of the block.
* @return the blockheader of the hash
* @throws Exception desc
*/
override fun block(hash: String): BlockHeader {
val client = HttpClientManager.getHttpClient()
val url = "$BLOCKSTREAM_API_URL/block/$hash"
val request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/json")
.url(url)
.get()
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val jsonObject = jacksonObjectMapper().readTree(it.body.string())
val blockHeader = BlockHeader()
blockHeader.merkleroot = jsonObject["merkle_root"].asText()
blockHeader.setTime(jsonObject["timestamp"].asInt().toString())
blockHeader.blockHash = hash
Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash")
return blockHeader
} else {
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
}
}
}
/**
* Retrieve the block hash from the block height.
*
* @param height Height of the block.
* @return the hash of the block at height height
* @throws Exception desc
*/
@Throws(Exception::class)
override fun blockHash(height: Int): String {
val client = HttpClientManager.getHttpClient()
val url = "$BLOCKSTREAM_API_URL/block-height/$height"
val request =
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val blockHash = it.body.string()
Log.d("OkHttpBlockstreamExplorer", "$url $blockHash")
return blockHash
} else {
throw UrlException(it.message)
}
}
}
companion object {
private const val BLOCKSTREAM_API_URL = "https://blockstream.info/api"
}
}

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.ots.ICalendar
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
import com.vitorpamplona.quartz.ots.Timestamp
import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException
import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException
import com.vitorpamplona.quartz.ots.exceptions.UrlException
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
/**
* Class representing remote calendar server interface.
*/
class OkHttpCalendar(
val url: String,
) : ICalendar {
/**
* Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest.
*
* @param digest The digest hash to send.
* @return the Timestamp received from the calendar.
* @throws ExceededSizeException if response is too big.
* @throws UrlException if url is not reachable.
* @throws DeserializationException if the data is corrupt
*/
@Throws(ExceededSizeException::class, UrlException::class, DeserializationException::class)
override fun submit(digest: ByteArray): Timestamp {
try {
val client = HttpClientManager.getHttpClient()
val url = "$url/digest"
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
val requestBody = digest.toRequestBody(mediaType)
val request =
okhttp3.Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val ctx = StreamDeserializationContext(it.body.bytes())
return Timestamp.deserialize(ctx, digest)
} else {
throw UrlException("Failed to open $url")
}
}
} catch (e: ExceededSizeException) {
throw e
} catch (e: DeserializationException) {
throw e
} catch (e: Exception) {
throw UrlException(e.message)
}
}
/**
* Get a timestamp for a given commitment.
*
* @param commitment The digest hash to send.
* @return the Timestamp from the calendar server (with blockchain information if already written).
* @throws ExceededSizeException if response is too big.
* @throws UrlException if url is not reachable.
* @throws CommitmentNotFoundException if commit is not found.
* @throws DeserializationException if the data is corrupt
*/
@Throws(
DeserializationException::class,
ExceededSizeException::class,
CommitmentNotFoundException::class,
UrlException::class,
)
override fun getTimestamp(commitment: ByteArray): Timestamp {
try {
val client = HttpClientManager.getHttpClient()
val url = url + "/timestamp/" + Hex.encode(commitment)
val request =
okhttp3.Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
.url(url)
.get()
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val ctx = StreamDeserializationContext(it.body.bytes())
return Timestamp.deserialize(ctx, commitment)
} else {
throw CommitmentNotFoundException("Calendar response a status code != 200: " + it.code)
}
}
} catch (e: DeserializationException) {
throw e
} catch (e: ExceededSizeException) {
throw e
} catch (e: CommitmentNotFoundException) {
throw e
} catch (e: Exception) {
throw UrlException(e.message)
}
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
import com.vitorpamplona.quartz.ots.Timestamp
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Optional
import java.util.concurrent.BlockingQueue
/**
* For making async calls to a calendar server
*/
class OkHttpCalendarAsyncSubmit(
private val url: String,
private val digest: ByteArray,
) : ICalendarAsyncSubmit {
private var queue: BlockingQueue<Optional<Timestamp>>? = null
fun setQueue(queue: BlockingQueue<Optional<Timestamp>>?) {
this.queue = queue
}
@Throws(Exception::class)
override fun call(): Optional<Timestamp> {
val client = HttpClientManager.getHttpClient()
val url = "$url/digest"
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
val requestBody = digest.toRequestBody(mediaType)
val request =
okhttp3.Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
if (it.isSuccessful) {
val ctx = StreamDeserializationContext(it.body.bytes())
val timestamp = Timestamp.deserialize(ctx, digest)
val of = Optional.of(timestamp)
queue!!.add(of)
return of
} else {
queue!!.add(Optional.empty())
return Optional.empty()
}
}
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.quartz.ots.CalendarBuilder
import com.vitorpamplona.quartz.ots.ICalendar
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
class OkHttpCalendarBuilder : CalendarBuilder {
override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url)
override fun newAsyncCalendar(
url: String,
digest: ByteArray,
): ICalendarAsyncSubmit = OkHttpCalendarAsyncSubmit(url, digest)
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.playback
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.ammolite.service.HttpClientManager
/**
* HLS LiveStreams cannot use cache.
*/
@UnstableApi
class CustomMediaSourceFactory : MediaSource.Factory {
private var cachingFactory: MediaSource.Factory =
DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
private var nonCachingFactory: MediaSource.Factory =
DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory {
cachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
nonCachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
return this
}
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory {
cachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
nonCachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
return this
}
override fun getSupportedTypes(): IntArray = nonCachingFactory.supportedTypes
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
if (mediaItem.mediaId.contains(".m3u8", true)) {
return nonCachingFactory.createMediaSource(mediaItem)
}
return cachingFactory.createMediaSource(mediaItem)
}
}

View File

@@ -0,0 +1,184 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.playback
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.util.LruCache
import androidx.core.net.toUri
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.Player.PositionInfo
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.Player.STATE_READY
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import com.vitorpamplona.amethyst.ui.MainActivity
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.math.abs
class MultiPlayerPlaybackManager(
private val dataSourceFactory: androidx.media3.exoplayer.source.MediaSource.Factory? = null,
private val cachedPositions: VideoViewedPositionCache,
) {
// protects from LruCache killing playing sessions
private val playingMap = mutableMapOf<String, MediaSession>()
private val cache =
object : LruCache<String, MediaSession>(10) { // up to 10 videos in the screen at the same time
override fun entryRemoved(
evicted: Boolean,
key: String?,
oldValue: MediaSession?,
newValue: MediaSession?,
) {
super.entryRemoved(evicted, key, oldValue, newValue)
if (!playingMap.contains(key)) {
oldValue?.let {
it.player.release()
it.release()
}
}
}
}
private fun getCallbackIntent(
callbackUri: String,
applicationContext: Context,
): PendingIntent =
PendingIntent.getActivity(
applicationContext,
0,
Intent(Intent.ACTION_VIEW, callbackUri.toUri(), applicationContext, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun getMediaSession(
id: String,
uri: String,
callbackUri: String?,
context: Context,
applicationContext: Context,
): MediaSession {
val existingSession = playingMap.get(id) ?: cache.get(id)
if (existingSession != null) return existingSession
val player =
ExoPlayer.Builder(context).run {
dataSourceFactory?.let { setMediaSourceFactory(it) }
build()
}
player.apply {
repeatMode = Player.REPEAT_MODE_ALL
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
volume = 0f
}
val mediaSession =
MediaSession.Builder(context, player).run {
callbackUri?.let { setSessionActivity(getCallbackIntent(it, applicationContext)) }
setId(id)
build()
}
player.addListener(
object : Player.Listener {
// avoids saving positions for live streams otherwise caching goes crazy
val mustCachePositions = !uri.contains(".m3u8", true)
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) {
player.setWakeMode(C.WAKE_MODE_NETWORK)
playingMap.put(id, mediaSession)
} else {
player.setWakeMode(C.WAKE_MODE_NONE)
if (mustCachePositions) {
cachedPositions.add(uri, player.currentPosition)
}
cache.put(id, mediaSession)
playingMap.remove(id, mediaSession)
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
STATE_IDLE -> {
// only saves if it wqs playing
if (mustCachePositions && abs(player.currentPosition) > 1) {
cachedPositions.add(uri, player.currentPosition)
}
}
STATE_READY -> {
if (mustCachePositions) {
cachedPositions.get(uri)?.let { lastPosition ->
if (abs(player.currentPosition - lastPosition) > 5 * 60) {
player.seekTo(lastPosition)
}
}
}
}
else -> {
// only saves if it wqs playing
if (mustCachePositions && abs(player.currentPosition) > 1) {
cachedPositions.add(uri, player.currentPosition)
}
}
}
}
override fun onPositionDiscontinuity(
oldPosition: PositionInfo,
newPosition: PositionInfo,
reason: Int,
) {
if (mustCachePositions && player.playbackState != STATE_IDLE) {
cachedPositions.add(uri, newPosition.positionMs)
}
}
},
)
cache.put(id, mediaSession)
return mediaSession
}
@OptIn(DelicateCoroutinesApi::class)
fun releaseAppPlayers() {
GlobalScope.launch(Dispatchers.Main) {
cache.evictAll()
playingMap.forEach {
it.value.player.release()
it.value.release()
}
playingMap.clear()
}
}
fun playingContent(): Collection<MediaSession> = playingMap.values
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.playback
import android.content.ComponentName
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.util.LruCache
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import kotlinx.coroutines.CancellationException
import java.util.concurrent.Executors
object PlaybackClientController {
var executorService = Executors.newCachedThreadPool()
val cache = LruCache<Int, SessionToken>(1)
fun prepareController(
controllerID: String,
videoUri: String,
callbackUri: String?,
context: Context,
onReady: (MediaController) -> Unit,
) {
try {
// creating a bundle object
// creating a bundle object
val bundle = Bundle()
bundle.putString("id", controllerID)
bundle.putString("uri", videoUri)
bundle.putString("callbackUri", callbackUri)
var session = cache.get(context.hashCode())
if (session == null) {
session = SessionToken(context, ComponentName(context, PlaybackService::class.java))
cache.put(context.hashCode(), session)
}
val controllerFuture =
MediaController.Builder(context, session).setConnectionHints(bundle).buildAsync()
controllerFuture.addListener(
{
try {
onReady(controllerFuture.get())
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
}
},
executorService,
)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
}
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.playback
import android.content.Intent
import android.util.Log
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.vitorpamplona.ammolite.service.HttpClientManager
class PlaybackService : MediaSessionService() {
private var videoViewedPositionCache = VideoViewedPositionCache()
private var managerAllInOne: MultiPlayerPlaybackManager? = null
@OptIn(UnstableApi::class)
fun lazyDS(): MultiPlayerPlaybackManager {
managerAllInOne?.let {
return it
}
val newInstance = MultiPlayerPlaybackManager(CustomMediaSourceFactory(), videoViewedPositionCache)
managerAllInOne = newInstance
return newInstance
}
// Create your Player and MediaSession in the onCreate lifecycle event
@OptIn(UnstableApi::class)
override fun onCreate() {
super.onCreate()
Log.d("Lifetime Event", "PlaybackService.onCreate")
// Stop all videos and recreates all managers when the proxy changes.
HttpClientManager.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
}
@OptIn(UnstableApi::class)
private fun onProxyUpdated() {
val toDestroyAllInOne = managerAllInOne
managerAllInOne = MultiPlayerPlaybackManager(CustomMediaSourceFactory(), videoViewedPositionCache)
toDestroyAllInOne?.releaseAppPlayers()
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
Log.d("Lifetime Event", "onTaskRemoved")
}
override fun onDestroy() {
Log.d("Lifetime Event", "PlaybackService.onDestroy")
HttpClientManager.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated)
managerAllInOne?.releaseAppPlayers()
super.onDestroy()
}
override fun onUpdateNotification(
session: MediaSession,
startInForegroundRequired: Boolean,
) {
// Updates any new player ready
super.onUpdateNotification(session, startInForegroundRequired)
// Overrides the notification with any player actually playing
managerAllInOne?.playingContent()?.forEach {
if (it.player.isPlaying) {
super.onUpdateNotification(it, startInForegroundRequired)
}
}
// Overrides again with playing with audio
managerAllInOne?.playingContent()?.forEach {
if (it.player.isPlaying && it.player.volume > 0) {
super.onUpdateNotification(it, startInForegroundRequired)
}
}
}
// Return a MediaSession to link with the MediaController that is making
// this request.
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
val id = controllerInfo.connectionHints.getString("id") ?: return null
val uri = controllerInfo.connectionHints.getString("uri") ?: return null
val callbackUri = controllerInfo.connectionHints.getString("callbackUri")
val manager = lazyDS()
return manager.getMediaSession(
id,
uri,
callbackUri,
context = this,
applicationContext = applicationContext,
)
}
}

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