Compare commits

...

458 Commits

Author SHA1 Message Date
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
Vitor Pamplona
15ab955a7d Fixes alt of NIP-96 server lists 2024-07-19 11:55:46 -04:00
Vitor Pamplona
9176151971 Fixes auth infinite loop with nostr.wine 2024-07-19 10:45:06 -04: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
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
863 changed files with 18903 additions and 11608 deletions

View File

@@ -18,11 +18,11 @@ jobs:
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
distribution: 'zulu'
java-version: 17
- name: Cache gradle
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
@@ -46,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

@@ -15,11 +15,11 @@ jobs:
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
distribution: 'zulu'
java-version: 17
- name: Cache gradle
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
@@ -32,7 +32,7 @@ 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 }}
@@ -44,7 +44,7 @@ jobs:
- 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 }}
@@ -59,7 +59,7 @@ 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 }}
@@ -71,7 +71,7 @@ jobs:
- 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 }}
@@ -99,7 +99,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -110,7 +110,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -121,7 +121,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -132,7 +132,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-arm64-v8a-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -143,7 +143,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-armeabi-v7a-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -155,7 +155,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -166,7 +166,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -177,7 +177,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -188,7 +188,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-arm64-v8a-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -199,7 +199,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-armeabi-v7a-release-unsigned-signed.apk
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip
@@ -213,7 +213,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/playRelease/app-play-release.aab
asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
asset_content_type: application/zip
@@ -225,6 +225,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
asset_content_type: application/zip

2
.gitignore vendored
View File

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

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.24" />
<option name="version" value="2.0.0" />
</component>
</project>

View File

@@ -226,7 +226,7 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
- `KEY_PASSWORD` <- `<your password>`
- `KEY_STORE_PASSWORD` <- `<your key store password>`
- `SIGNING_KEY` <- the data from `<my-release-key.keystore>`
3. Change the `versionCode` and `versionName` on `app/build.gradle`
3. Change the `versionCode` and `versionName` on `amethyst/build.gradle`
4. Commit and push.
5. Tag the commit with `v{x.x.x}`
6. Let the [Create Release GitHub Action](https://github.com/vitorpamplona/amethyst/actions/workflows/create-release.yml) build a new `aab` file.

View File

@@ -1,20 +1,23 @@
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 34
compileSdk libs.versions.android.compileSdk.get().toInteger()
defaultConfig {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 373
versionName "0.87.4"
buildConfigField "String", "RELEASE_NOTES_ID", "\"863124b6592359494396c22b917f9d251ad12ac31b09ea29e0265e3c088ce9f8\""
minSdk libs.versions.android.minSdk.get().toInteger()
targetSdk libs.versions.android.targetSdk.get().toInteger()
versionCode 390
versionName "0.89.4"
buildConfigField "String", "RELEASE_NOTES_ID", "\"1130badb252bdf62aaf93cb65bfa2bb09e7600c7d6764894f2954555b3b73018\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -132,19 +135,11 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
compose true
buildConfig true
}
composeOptions {
// Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin
kotlinCompilerExtensionVersion "1.5.14"
}
packagingOptions {
resources {
excludes += ['/META-INF/{AL2.0,LGPL2.1}', '**/libscrypt.dylib']
@@ -160,11 +155,22 @@ android {
}
}
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
@@ -289,34 +295,3 @@ dependencies {
debugImplementation libs.androidx.ui.test.manifest
}
// https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds#differing-assetsdexoptbaselineprofm-easy-to-fix
// NB: Android Studio can't find the imports; this does not affect the
// actual build since Gradle can find them just fine.
import com.android.tools.profgen.ArtProfileKt
import com.android.tools.profgen.ArtProfileSerializer
import com.android.tools.profgen.DexFile
project.afterEvaluate {
tasks.each { task ->
if (task.name.startsWith("compile") && task.name.endsWith("ReleaseArtProfile")) {
task.doLast {
outputs.files.each { file ->
if (file.name.endsWith(".profm")) {
println("Sorting ${file} ...")
def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
def profile = ArtProfileKt.ArtProfile(file)
def keys = new ArrayList(profile.profileData.keySet())
def sortedData = new LinkedHashMap()
Collections.sort keys, new DexFile.Companion()
keys.each { key -> sortedData[key] = profile.profileData[key] }
new FileOutputStream(file).with {
write(version.magicBytes$profgen)
write(version.versionBytes$profgen)
version.write$profgen(it, sortedData, "")
}
}
}
}
}
}
}

View File

@@ -63,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

@@ -75,9 +75,21 @@ class ImageUploadTesting {
)
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 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)

View File

@@ -79,8 +79,7 @@ class ThreadAssemblerTest {
{"id":"512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","pubkey":"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194","created_at":1688194800,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","","reply"]],"content":"another one","sig":"b4bc4d8206a08de0a918043129b27c8863d17f60e4f58b4d2b535326625870d32640b04f247eb9eb5df1d62fe98c8e1b0341bdd119655553488300ec9d5b4036"},
{"id":"ce6e32e3e17b6901d2cc70b60f3743e24f885bb6e9da6d88cff516079eac1883","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1688234921,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","","reply"]],"content":"yet another one, testing 0.0.3","sig":"13bd41c4029c6a7ee41cb03d12e831e9e9b6e14d43a61c78a72657070b45385ba2ce98e8121049fbcbdb7b2dd777c36c7867ba70b5e6a4798a9b866910ae5b62"}
]
"""
.trimIndent()
""".trimIndent()
val header =
"""
@@ -111,8 +110,7 @@ class ThreadAssemblerTest {
["p","b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc"]
]
}
"""
.trimIndent()
""".trimIndent()
@Test
fun threadOrderTest() =

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

@@ -55,15 +55,11 @@ object PushDistributorHandler : PushDistributorActions {
endpointInternal = ""
}
override fun getSavedDistributor(): String {
return unifiedPush.getSavedDistributor(appContext) ?: ""
}
override fun getSavedDistributor(): String = unifiedPush.getSavedDistributor(appContext) ?: ""
fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty()
override fun getInstalledDistributors(): List<String> {
return unifiedPush.getDistributors(appContext)
}
override fun getInstalledDistributors(): List<String> = unifiedPush.getDistributors(appContext)
fun formattedDistributorNames(): List<String> {
val distributorsArray = getInstalledDistributors().toTypedArray()
@@ -87,8 +83,7 @@ object PushDistributorHandler : PushDistributorActions {
it
}
as String
}
.toTypedArray()
}.toTypedArray()
return distributorsNameArray.toList()
}

View File

@@ -126,8 +126,7 @@ class PushMessageReceiver : MessagingReceiver() {
pushHandler.removeEndpoint()
}
fun notificationManager(): NotificationManager {
return ContextCompat.getSystemService(appContext, NotificationManager::class.java)
fun notificationManager(): NotificationManager =
ContextCompat.getSystemService(appContext, NotificationManager::class.java)
as NotificationManager
}
}

View File

@@ -43,7 +43,6 @@ 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
@@ -60,6 +59,7 @@ 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
@@ -79,7 +79,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
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") {
@@ -101,9 +101,9 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
} else {
AlertDialog(
onDismissRequest = { distributorPresent = true },
title = { Text(stringResource(R.string.push_server_install_app)) },
title = { Text(stringRes(R.string.push_server_install_app)) },
text = {
val content = stringResource(R.string.push_server_install_app_description)
val content = stringRes(R.string.push_server_install_app_description)
val astNode =
remember {
@@ -128,7 +128,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
sharedPreferencesViewModel.dontShowPushNotificationSelector()
},
) {
Text(stringResource(R.string.quick_action_dont_show_again_button))
Text(stringRes(R.string.quick_action_dont_show_again_button))
}
Button(
onClick = { distributorPresent = true },
@@ -142,7 +142,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
contentDescription = null,
)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.error_dialog_button_ok))
Text(stringRes(R.string.error_dialog_button_ok))
}
}
}
@@ -168,20 +168,19 @@ fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, Immuta
}
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),
stringRes(id = R.string.push_server_none),
stringRes(id = R.string.push_server_none_explainer),
),
)
.toImmutableList()
).toImmutableList()
onInner(
currentDistributor,

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -118,7 +118,5 @@ private class PullRefreshNestedScrollConnection(
else -> Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
return Velocity(0f, onRelease(available.y))
}
override suspend fun onPreFling(available: Velocity): Velocity = Velocity(0f, onRelease(available.y))
}

View File

@@ -63,8 +63,7 @@ fun Modifier.pullRefreshIndicatorTransform(
) {
this@drawWithContent.drawContent()
}
}
.graphicsLayer {
}.graphicsLayer {
translationY = state.position - size.height
if (scale && !state.refreshing) {

View File

@@ -22,6 +22,7 @@ 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
@@ -61,7 +62,8 @@ class Amethyst : Application() {
}
val coilCache: DiskCache by lazy {
DiskCache.Builder()
DiskCache
.Builder()
.directory(applicationContext.safeCacheDir.resolve("image_cache"))
.maxSizePercent(0.2)
.maximumMaxSizeBytes(500L * 1024 * 1024) // 250MB
@@ -70,17 +72,28 @@ class Amethyst : Application() {
override fun onCreate() {
super.onCreate()
instance = this
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") {
StrictMode.setThreadPolicy(
ThreadPolicy.Builder().detectAll().penaltyLog().build(),
ThreadPolicy
.Builder()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
VmPolicy.Builder().detectAll().penaltyLog().build(),
VmPolicy
.Builder()
.detectAll()
.penaltyLog()
.build(),
)
Looper.getMainLooper().setMessageLogging(LogMonitor())
ChoreographerHelper.start()
}
GlobalScope.launch(Dispatchers.IO) {
@@ -93,13 +106,9 @@ class Amethyst : Application() {
}
}
fun imageLoaderBuilder(): ImageLoader.Builder {
return ImageLoader.Builder(applicationContext).diskCache { coilCache }
}
fun imageLoaderBuilder(): ImageLoader.Builder = ImageLoader.Builder(applicationContext).diskCache { coilCache }
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences {
return EncryptedStorage.preferences(instance, npub)
}
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
companion object {
lateinit var instance: Amethyst

View File

@@ -29,16 +29,15 @@ class EncryptedStorage {
private const val PREFERENCES_NAME = "secret_keeper"
// returns the preferences for each account or a global file if null.
fun prefsFileName(npub: String? = null): String {
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
}
fun prefsFileName(npub: String? = null): String = if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
fun preferences(
applicationContext: Context,
npub: String? = null,
): EncryptedSharedPreferences {
val masterKey: MasterKey =
MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
MasterKey
.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

View File

@@ -31,17 +31,19 @@ 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.RelaySetupInfo
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.service.HttpClientManager
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
@@ -90,6 +92,8 @@ private object PrefKeys {
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
@@ -99,14 +103,7 @@ private object PrefKeys {
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 AUTOMATICALLY_SHOW_IMAGES = "automatically_show_images"
const val AUTOMATICALLY_START_PLAYBACK = "automatically_start_playback"
const val THEME = "theme"
const val PREFERRED_LANGUAGE = "preferred_Language"
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars"
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
const val SIGNER_PACKAGE_NAME = "signer_package_name"
const val HAS_DONATED_IN_VERSION = "has_donated_in_version"
const val PENDING_ATTESTATIONS = "pending_attestations"
@@ -148,7 +145,7 @@ object LocalPreferences {
Event.mapper.readValue<List<AccountInfo>>(it)
}
if (newSystemOfAccounts != null && newSystemOfAccounts.isNotEmpty()) {
if (!newSystemOfAccounts.isNullOrEmpty()) {
savedAccounts = newSystemOfAccounts
} else {
val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf()
@@ -164,6 +161,8 @@ object LocalPreferences {
}
savedAccounts = migrated
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply()
}
}
}
@@ -244,7 +243,8 @@ object LocalPreferences {
* deleted
*/
@SuppressLint("ApplySharedPref")
suspend fun updatePrefsForLogout(accountInfo: AccountInfo) =
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()
@@ -257,17 +257,17 @@ object LocalPreferences {
updateCurrentAccount(savedAccounts().elementAt(0).npub)
}
}
}
suspend fun updatePrefsForLogin(account: Account) {
setCurrentAccount(account)
saveToEncryptedStorage(account)
}
fun allSavedAccounts(): List<AccountInfo> {
return savedAccounts()
}
fun allSavedAccounts(): List<AccountInfo> = savedAccounts()
suspend fun saveToEncryptedStorage(account: Account) =
suspend fun saveToEncryptedStorage(account: Account) {
Log.d("LocalPreferences", "Saving to encrypted storage")
withContext(Dispatchers.IO) {
checkNotInMainThread()
@@ -315,10 +315,33 @@ object LocalPreferences {
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
Event.mapper.writeValueAsString(account.zapPaymentRequest),
)
putString(
PrefKeys.LATEST_CONTACT_LIST,
Event.mapper.writeValueAsString(account.backupContactList),
)
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)
@@ -332,28 +355,27 @@ object LocalPreferences {
)
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
if (account.showSensitiveContent == null) {
if (account.showSensitiveContent.value == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent.value!!)
}
putString(
PrefKeys.PENDING_ATTESTATIONS,
Event.mapper.writeValueAsString(account.pendingAttestations),
Event.mapper.writeValueAsString(account.pendingAttestations.value),
)
}
.apply()
}.apply()
}
suspend fun loadCurrentAccountFromEncryptedStorage(): Account? {
return currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) }
}
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()
@@ -361,6 +383,7 @@ object LocalPreferences {
}
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) }
@@ -395,8 +418,10 @@ object LocalPreferences {
}
}
suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): Account? =
withContext(Dispatchers.IO) {
suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): Account? {
Log.d("LocalPreferences", "Load account from file")
return withContext(Dispatchers.IO) {
checkNotInMainThread()
return@withContext with(encryptedPreferences(npub)) {
@@ -473,8 +498,8 @@ object LocalPreferences {
val latestContactList =
try {
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
println("Decoding Contact List: " + it)
if (it != null) {
if (it != "null") {
println("Decoding Contact List: $it")
Event.fromJson(it) as ContactListEvent?
} else {
null
@@ -490,6 +515,46 @@ object LocalPreferences {
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 {
@@ -504,7 +569,7 @@ object LocalPreferences {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}",
"Error Decoding Contact List ${getString(PrefKeys.PENDING_ATTESTATIONS, null)}",
e,
)
null
@@ -597,14 +662,16 @@ object LocalPreferences {
hideBlockAlertDialog = hideBlockAlertDialog,
hideNIP17WarningDialog = hideNIP17WarningDialog,
backupContactList = latestContactList,
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
proxy = proxy,
proxyPort = proxyPort,
showSensitiveContent = showSensitiveContent,
showSensitiveContent = MutableStateFlow(showSensitiveContent),
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,
hasDonatedInVersion = hasDonatedInVersion,
pendingAttestations = pendingAttestations ?: emptyMap(),
pendingAttestations = MutableStateFlow(pendingAttestations ?: emptyMap()),
)
// Loads from DB
@@ -618,4 +685,5 @@ object LocalPreferences {
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

@@ -30,7 +30,6 @@ 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.HttpClientManager
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
@@ -47,7 +46,8 @@ 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.amethyst.service.relays.Client
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
@@ -86,6 +86,7 @@ class ServiceManager {
// Resets Proxy Use
HttpClientManager.setDefaultProxy(account?.proxy)
HttpClientManager.setDefaultUserAgent("Amethyst/${BuildConfig.VERSION_NAME}")
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
Coil.setImageLoader {
Amethyst.instance

View File

@@ -25,15 +25,18 @@ import android.util.LruCache
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
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>)
data class Spammer(
val pubkeyHex: HexKey,
var duplicatedMessages: Set<HexKey>,
)
class AntiSpamFilter {
val recentMessages = LruCache<Int, String>(1000)
@@ -109,7 +112,9 @@ class AntiSpamFilter {
}
@Stable
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
class AntiSpamLiveData(
val cache: AntiSpamFilter,
) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO)
@@ -126,4 +131,6 @@ class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(Anti
}
}
class AntiSpamState(val cache: AntiSpamFilter)
class AntiSpamState(
val cache: AntiSpamFilter,
)

View File

@@ -25,9 +25,9 @@ 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.components.BundledUpdate
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
@@ -37,7 +37,9 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import kotlinx.coroutines.Dispatchers
@Stable
class PublicChatChannel(idHex: String) : Channel(idHex) {
class PublicChatChannel(
idHex: String,
) : Channel(idHex) {
var info = ChannelCreateEvent.ChannelData(null, null, null)
fun updateChannelInfo(
@@ -49,26 +51,22 @@ class PublicChatChannel(idHex: String) : Channel(idHex) {
super.updateChannelInfo(creator, updatedAt)
}
override fun toBestDisplayName(): String {
return info.name ?: super.toBestDisplayName()
}
override fun toBestDisplayName(): String = info.name ?: super.toBestDisplayName()
override fun summary(): String? {
return info.about
}
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 {
return listOfNotNull(info.name, info.about).filter { it.contains(prefix, true) }.isNotEmpty()
}
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()) {
class LiveActivitiesChannel(
val address: ATag,
) : Channel(address.toTag()) {
var info: LiveActivitiesEvent? = null
override fun idNote() = address.toNAddr()
@@ -86,27 +84,22 @@ class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
super.updateChannelInfo(creator, updatedAt)
}
override fun toBestDisplayName(): String {
return info?.title() ?: super.toBestDisplayName()
}
override fun toBestDisplayName(): String = info?.title() ?: super.toBestDisplayName()
override fun summary(): String? {
return info?.summary()
}
override fun summary(): String? = info?.summary()
override fun profilePicture(): String? {
return info?.image()?.ifBlank { null }
}
override fun profilePicture(): String? = info?.image()?.ifBlank { null }
override fun anyNameStartsWith(prefix: String): Boolean {
return listOfNotNull(info?.title(), info?.summary())
override fun anyNameStartsWith(prefix: String): Boolean =
listOfNotNull(info?.title(), info?.summary())
.filter { it.contains(prefix, true) }
.isNotEmpty()
}
}
@Stable
abstract class Channel(val idHex: String) {
abstract class Channel(
val idHex: String,
) {
var creator: User? = null
var updatedMetadataAt: Long = 0
val notes = LargeCache<HexKey, Note>()
@@ -118,21 +111,13 @@ abstract class Channel(val idHex: String) {
open fun idDisplayNote() = idNote().toShortenHex()
open fun toBestDisplayName(): String {
return idDisplayNote()
}
open fun toBestDisplayName(): String = idDisplayNote()
open fun summary(): String? {
return null
}
open fun summary(): String? = null
open fun creatorName(): String? {
return creator?.toBestDisplayName()
}
open fun creatorName(): String? = creator?.toBestDisplayName()
open fun profilePicture(): String? {
return creator?.info?.banner
}
open fun profilePicture(): String? = creator?.info?.banner
open fun updateChannelInfo(
creator: User,
@@ -167,10 +152,10 @@ abstract class Channel(val idHex: String) {
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
val important =
notes.filter { key, it ->
it.author?.let { author -> account.isHidden(author) } == false
}
.sortedWith(DefaultFeedOrder)
notes
.filter { key, it ->
it.author?.let { author -> account.isHidden(author) } == false
}.sortedWith(DefaultFeedOrder)
.take(500)
.toSet()
@@ -182,7 +167,9 @@ abstract class Channel(val idHex: String) {
}
}
class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelState(channel)) {
class ChannelLiveData(
val channel: Channel,
) : LiveData<ChannelState>(ChannelState(channel)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO)
@@ -208,4 +195,6 @@ class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelStat
}
}
class ChannelState(val channel: Channel)
class ChannelState(
val channel: Channel,
)

View File

@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.utils.TimeUtils
@Stable
class Chatroom() {
class Chatroom {
var authors: Set<User> = setOf()
var roomMessages: Set<Note> = setOf()
var subject: String? = null
@@ -72,9 +72,7 @@ class Chatroom() {
}
}
fun senderIntersects(keySet: Set<HexKey>): Boolean {
return authors.any { it.pubkeyHex in keySet }
}
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()

View File

@@ -68,8 +68,8 @@ fun RenderHashTagIcons() {
}
}
fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
return when (tag.lowercase()) {
fun checkForHashtagWithIcon(tag: String): HashtagIcon? =
when (tag.lowercase()) {
"₿itcoin", "bitcoin", "btc", "timechain", "bitcoiner", "bitcoiners" -> bitcoin
"nostr", "nostrich", "nostriches", "thenostr" -> nostr
"lightning", "lightningnetwork" -> lightning
@@ -86,7 +86,6 @@ fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
"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))

View File

@@ -29,8 +29,8 @@ import com.vitorpamplona.amethyst.commons.data.LargeCache
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledInsert
import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.HexValidator
@@ -103,6 +103,7 @@ import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RecommendRelayEvent
import com.vitorpamplona.quartz.events.RelaySetEvent
@@ -148,8 +149,8 @@ object LocalCache {
val deletionIndex = DeletionIndex()
private val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>>(10)
private val observablesByKindAndAuthor = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindAndAuthor<Event>>>(10)
val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>>(10)
val observablesByKindAndAuthor = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindAndAuthor<Event>>>(10)
fun <T : Event> observeETag(
kind: Int,
@@ -209,13 +210,14 @@ object LocalCache {
} as LatestByKindAndAuthor<T>
}
fun updateObservables(event: Event) {
val observablesOfKind = observablesByKindAndETag[event.kind()] ?: return
event.forEachTaggedEvent {
observablesOfKind[it]?.updateIfMatches(event)
private fun updateObservables(event: Event) {
observablesByKindAndETag[event.kind]?.let { observablesOfKind ->
event.forEachTaggedEvent {
observablesOfKind[it]?.updateIfMatches(event)
}
}
observablesByKindAndAuthor[event.kind()]?.get(event.pubKey)?.updateIfMatches(event)
observablesByKindAndAuthor[event.kind]?.get(event.pubKey)?.updateIfMatches(event)
}
fun checkGetOrCreateUser(key: String): User? {
@@ -241,33 +243,25 @@ object LocalCache {
return users.get(key)
}
fun getAddressableNoteIfExists(key: String): AddressableNote? {
return addressables.get(key)
}
fun getAddressableNoteIfExists(key: String): AddressableNote? = addressables.get(key)
fun getNoteIfExists(key: String): Note? {
return addressables.get(key) ?: notes.get(key)
}
fun getNoteIfExists(key: String): Note? = addressables.get(key) ?: notes.get(key)
fun getChannelIfExists(key: String): Channel? {
return channels.get(key)
}
fun getChannelIfExists(key: String): Channel? = channels.get(key)
fun getNoteIfExists(event: Event): Note? {
return if (event is AddressableEvent) {
fun getNoteIfExists(event: Event): Note? =
if (event is AddressableEvent) {
getAddressableNoteIfExists(event.addressTag())
} else {
getNoteIfExists(event.id)
}
}
fun getOrCreateNote(event: Event): Note {
return if (event is AddressableEvent) {
fun getOrCreateNote(event: Event): Note =
if (event is AddressableEvent) {
getOrCreateAddressableNote(event.address())
} else {
getOrCreateNote(event.id)
}
}
fun checkGetOrCreateNote(key: String): Note? {
checkNotInMainThread()
@@ -348,8 +342,8 @@ object LocalCache {
return HexValidator.isHex(key)
}
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
return try {
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? =
try {
val addr = ATag.parse(key, null) // relay doesn't matter for the index.
if (addr != null) {
getOrCreateAddressableNote(addr)
@@ -360,7 +354,6 @@ object LocalCache {
Log.e("LocalCache", "Invalid Key to create channel: $key", e)
null
}
}
fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote {
// checkNotInMainThread()
@@ -395,6 +388,10 @@ object LocalCache {
val newUserMetadata = event.contactMetaData()
if (newUserMetadata != null) {
oldUser.updateUserInfo(newUserMetadata, event)
if (relay != null) {
oldUser.addRelayBeingUsed(relay, event.createdAt)
oldUser.latestMetadataRelay = relay.url
}
}
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex()} ${oldUser.toBestDisplayName()} from ${relay?.url}")
} else {
@@ -428,11 +425,11 @@ object LocalCache {
}
}
fun formattedDateTime(timestamp: Long): String {
return Instant.ofEpochSecond(timestamp)
fun formattedDateTime(timestamp: Long): String =
Instant
.ofEpochSecond(timestamp)
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
}
fun consume(
event: TextNoteEvent,
@@ -755,8 +752,8 @@ object LocalCache {
}
}
fun computeReplyTo(event: Event): List<Note> {
return when (event) {
fun computeReplyTo(event: Event): List<Note> =
when (event) {
is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
@@ -800,12 +797,11 @@ object LocalCache {
.mapNotNull { checkGetOrCreateNote(it) }
is DraftEvent -> {
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { checkGetOrCreateAddressableNote(it.toTag()) }
event.mapTaggedEvent { checkGetOrCreateNote(it) } + event.mapTaggedAddress { checkGetOrCreateAddressableNote(it) }
}
else -> emptyList<Note>()
}
}
fun consume(
event: PollNoteEvent,
@@ -1218,9 +1214,23 @@ object LocalCache {
if (deletionIndex.add(event)) {
var deletedAtLeastOne = false
event.deleteEvents()
event
.deleteEvents()
.mapNotNull { getNoteIfExists(it) }
.forEach { deleteNote ->
val deleteNoteEvent = deleteNote.event
if (deleteNoteEvent is AddressableEvent) {
val addressableNote = getAddressableNoteIfExists(deleteNoteEvent.addressTag())
if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0) <= event.createdAt) {
// Counts the replies
deleteNote(addressableNote)
addressables.remove(addressableNote.idHex)
deletedAtLeastOne = true
}
}
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) {
// reverts the add
@@ -1673,6 +1683,26 @@ object LocalCache {
refreshObservers(note)
}
fun consume(
event: ProfileGalleryEntryEvent,
relay: Relay?,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume(
event: FileStorageHeaderEvent,
relay: Relay?,
@@ -2030,16 +2060,16 @@ object LocalCache {
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
checkNotInMainThread()
return addressables.filter { _, it ->
val noteEvent = it.event
(
noteEvent is StatusEvent &&
noteEvent.pubKey == user.pubkeyHex &&
!noteEvent.isExpired() &&
noteEvent.content.isNotBlank()
)
}
.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
return addressables
.filter { _, it ->
val noteEvent = it.event
(
noteEvent is StatusEvent &&
noteEvent.pubKey == user.pubkeyHex &&
!noteEvent.isExpired() &&
noteEvent.content.isNotBlank()
)
}.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
.reversed()
.toImmutableList()
}
@@ -2066,9 +2096,7 @@ object LocalCache {
val modificationCache = LruCache<HexKey, List<Note>>(20)
fun cachedModificationEventsForNote(note: Note): List<Note>? {
return modificationCache[note.idHex]
}
fun cachedModificationEventsForNote(note: Note): List<Note>? = modificationCache[note.idHex]
suspend fun findLatestModificationForNote(note: Note): List<Note> {
checkNotInMainThread()
@@ -2082,11 +2110,12 @@ object LocalCache {
val time = TimeUtils.now()
val newNotes =
notes.filter { _, item ->
val noteEvent = item.event
notes
.filter { _, item ->
val noteEvent = item.event
noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
}.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
}.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
modificationCache.put(note.idHex, newNotes)
@@ -2210,9 +2239,11 @@ object LocalCache {
note.event is GenericRepostEvent
) &&
note.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
note.liveSet?.isInUse() != true && // don't delete if observing.
note.liveSet?.isInUse() != true &&
// don't delete if observing.
note.author?.pubkeyHex !in
accounts && // don't delete if it is the logged in account
accounts &&
// don't delete if it is the logged in account
note.event?.isTaggedUsers(accounts) !=
true // don't delete if it's a notification to the logged in user
}
@@ -2308,8 +2339,7 @@ object LocalCache {
?.hiddenUsers
?.map { userHex ->
(notes.filter { _, it -> it.event?.pubKey() == userHex } + addressables.filter { _, it -> it.event?.pubKey() == userHex }).toSet()
}
?.flatten()
}?.flatten()
?: emptyList()
toBeRemoved.forEach {
@@ -2341,7 +2371,7 @@ object LocalCache {
}
// Observers line up here.
val live: LocalCacheLiveData = LocalCacheLiveData()
val live: LocalCacheFlow = LocalCacheFlow()
private fun refreshObservers(newNote: Note) {
updateObservables(newNote.event as Event)
@@ -2489,7 +2519,28 @@ object LocalCache {
event: Event,
relay: Relay?,
) {
if (deletionIndex.hasBeenDeleted(event)) return
if (deletionIndex.hasBeenDeleted(event)) {
// update relay with deletion event from another.
if (relay != null) {
deletionIndex.hasBeenDeletedBy(event)?.let {
Log.d("LocalCache", "Updating ${relay.url} with a Deletion Event ${it.toJson()} because of ${event.toJson()}")
relay.send(it)
}
}
return
}
if (event is AddressableEvent && relay != null) {
// updates relay with a new event.
getAddressableNoteIfExists(event.addressTag())?.let {
it.event?.let { existingEvent ->
if (existingEvent.createdAt() > event.createdAt) {
Log.d("LocalCache", "Updating ${relay.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}")
relay.send(existingEvent)
}
}
}
}
checkNotInMainThread()
@@ -2534,6 +2585,7 @@ object LocalCache {
}
is FhirResourceEvent -> consume(event, relay)
is FileHeaderEvent -> consume(event, relay)
is ProfileGalleryEntryEvent -> consume(event, relay)
is FileServersEvent -> consume(event, relay)
is FileStorageEvent -> consume(event, relay)
is FileStorageHeaderEvent -> consume(event, relay)
@@ -2596,8 +2648,8 @@ object LocalCache {
}
}
fun hasConsumed(notificationEvent: Event): Boolean {
return if (notificationEvent is AddressableEvent) {
fun hasConsumed(notificationEvent: Event): Boolean =
if (notificationEvent is AddressableEvent) {
val note = addressables.get(notificationEvent.addressTag())
val noteEvent = note?.event
noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt()
@@ -2605,11 +2657,10 @@ object LocalCache {
val note = notes.get(notificationEvent.id)
note?.event != null
}
}
}
@Stable
class LocalCacheLiveData {
class LocalCacheFlow {
private val _newEventBundles = MutableSharedFlow<Set<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
@@ -2617,8 +2668,7 @@ class LocalCacheLiveData {
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
fun invalidateData(newNote: Note) {
bundler.invalidateList(newNote) {
bundledNewNotes ->
bundler.invalidateList(newNote) { bundledNewNotes ->
_newEventBundles.emit(bundledNewNotes)
}
}

View File

@@ -66,9 +66,7 @@ class HumanName(
var family: String? = null,
var given: ArrayList<String> = arrayListOf(),
) {
fun assembleName(): String {
return given.joinToString(" ") + " " + family
}
fun assembleName(): String = given.joinToString(" ") + " " + family
}
class Bundle(

View File

@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.model
import android.util.LruCache
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
@@ -29,18 +28,18 @@ import androidx.lifecycle.distinctUntilChanged
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.relays.updated
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.combineWith
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.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
@@ -75,7 +74,9 @@ import java.math.BigDecimal
import kotlin.coroutines.resume
@Stable
class AddressableNote(val address: ATag) : Note(address.toTag()) {
class AddressableNote(
val address: ATag,
) : Note(address.toTag()) {
override fun idNote() = address.toNAddr()
override fun toNEvent() = address.toNAddr()
@@ -93,9 +94,7 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
return minOf(publishedAt, lastCreatedAt)
}
fun dTag(): String? {
return (event as? AddressableEvent)?.dTag()
}
fun dTag(): String? = (event as? AddressableEvent)?.dTag()
override fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>,
@@ -107,7 +106,9 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
}
@Stable
open class Note(val idHex: String) {
open class Note(
val idHex: String,
) {
// These fields are only available after the Text Note event is received.
// They are immutable after that.
var event: EventInterface? = null
@@ -138,6 +139,8 @@ open class Note(val idHex: String) {
var relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
private set
var associatedNote: Note? = null
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()
fun id() = Hex.decode(idHex)
@@ -163,14 +166,12 @@ open class Note(val idHex: String) {
}
}
fun toNostrUri(): String {
return "nostr:${toNEvent()}"
}
fun toNostrUri(): String = "nostr:${toNEvent()}"
open fun idDisplayNote() = idNote().toShortenHex()
fun channelHex(): HexKey? {
return if (
fun channelHex(): HexKey? =
if (
event is ChannelMessageEvent ||
event is ChannelMetadataEvent ||
event is ChannelCreateEvent ||
@@ -185,7 +186,6 @@ open class Note(val idHex: String) {
} else {
null
}
}
open fun address(): ATag? = null
@@ -259,7 +259,9 @@ open class Note(val idHex: String) {
if (repliesChanged) liveSet?.innerReplies?.invalidateData()
if (reactionsChanged) liveSet?.innerReactions?.invalidateData()
if (boostsChanged) liveSet?.innerBoosts?.invalidateData()
if (reportsChanged) liveSet?.innerReports?.invalidateData()
if (reportsChanged) {
flowSet?.reports?.invalidateData()
}
if (zapsChanged) liveSet?.innerZaps?.invalidateData()
return toBeRemoved
@@ -291,7 +293,7 @@ open class Note(val idHex: String) {
if (reports[author]?.contains(deleteNote) == true) {
reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote))
liveSet?.innerReports?.invalidateData()
flowSet?.reports?.invalidateData()
}
}
}
@@ -400,10 +402,10 @@ open class Note(val idHex: String) {
if (reportsByAuthor == null) {
reports = reports + Pair(author, listOf(note))
liveSet?.innerReports?.invalidateData()
flowSet?.reports?.invalidateData()
} else if (!reportsByAuthor.contains(note)) {
reports = reports + Pair(author, reportsByAuthor + note)
liveSet?.innerReports?.invalidateData()
flowSet?.reports?.invalidateData()
}
}
@@ -417,7 +419,7 @@ open class Note(val idHex: String) {
fun addRelay(relay: Relay) {
if (relay.brief !in relays) {
addRelaySync(relay.brief)
liveSet?.innerRelays?.invalidateData()
flowSet?.relays?.invalidateData()
}
}
@@ -523,39 +525,30 @@ open class Note(val idHex: String) {
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
}
fun getReactionBy(user: User): String? {
return reactions.firstNotNullOfOrNull {
fun getReactionBy(user: User): String? =
reactions.firstNotNullOfOrNull {
if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) {
it.key
} else {
null
}
}
}
fun isBoostedBy(user: User): Boolean {
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
}
fun isBoostedBy(user: User): Boolean = boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
fun hasReportsBy(user: User): Boolean {
return reports[user]?.isNotEmpty() ?: false
}
fun hasReportsBy(user: User): Boolean = reports[user]?.isNotEmpty() ?: false
fun countReportAuthorsBy(users: Set<HexKey>): Int {
return reports.count { it.key.pubkeyHex in users }
}
fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
fun reportsBy(users: Set<HexKey>): List<Note> {
return reports
fun reportsBy(users: Set<HexKey>): List<Note> =
reports
.mapNotNull {
if (it.key.pubkeyHex in users) {
it.value
} else {
null
}
}
.flatten()
}
}.flatten()
private fun updateZapTotal() {
var sumOfAmounts = BigDecimal.ZERO
@@ -635,8 +628,8 @@ open class Note(val idHex: String) {
)
}
fun hasPledgeBy(user: User): Boolean {
return replies
fun hasPledgeBy(user: User): Boolean =
replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.any {
val pledgeValue =
@@ -650,10 +643,9 @@ open class Note(val idHex: String) {
pledgeValue != null && it.author == user
}
}
fun pledgedAmountByOthers(): BigDecimal {
return replies
fun pledgedAmountByOthers(): BigDecimal =
replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.mapNotNull {
try {
@@ -663,9 +655,7 @@ open class Note(val idHex: String) {
null
// do nothing if it can't convert to bigdecimal
}
}
.sumOf { it }
}
}.sumOf { it }
fun hasAnyReports(): Boolean {
val dayAgo = TimeUtils.oneDayAgo()
@@ -676,8 +666,8 @@ open class Note(val idHex: String) {
)
}
fun isNewThread(): Boolean {
return (
fun isNewThread(): Boolean =
(
event is RepostEvent ||
event is GenericRepostEvent ||
replyTo == null ||
@@ -685,29 +675,20 @@ open class Note(val idHex: String) {
) &&
event !is ChannelMessageEvent &&
event !is LiveActivitiesChatMessageEvent
}
fun hasZapped(loggedIn: User): Boolean {
return zaps.any { it.key.author == loggedIn }
}
fun hasZapped(loggedIn: User): Boolean = zaps.any { it.key.author == loggedIn }
fun hasReacted(
loggedIn: User,
content: String,
): Boolean {
return reactedBy(loggedIn, content).isNotEmpty()
}
): Boolean = reactedBy(loggedIn, content).isNotEmpty()
fun reactedBy(
loggedIn: User,
content: String,
): List<Note> {
return reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
}
): List<Note> = reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
fun reactedBy(loggedIn: User): List<String> {
return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
}
fun reactedBy(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
return boosts.firstOrNull {
@@ -715,9 +696,7 @@ open class Note(val idHex: String) {
} != null // 5 minute protection
}
fun boostedBy(loggedIn: User): List<Note> {
return boosts.filter { it.author == loggedIn }
}
fun boostedBy(loggedIn: User): List<Note> = boosts.filter { it.author == loggedIn }
fun moveAllReferencesTo(note: AddressableNote) {
// migrates these comments to a new version
@@ -762,33 +741,38 @@ open class Note(val idHex: String) {
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
val thisEvent = event ?: return false
val hash = thisEvent.pubKey().hashCode()
val isBoostedNoteHidden =
if (
thisEvent is GenericRepostEvent ||
thisEvent is RepostEvent ||
thisEvent is CommunityPostApprovalEvent
) {
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
} else {
false
// if the author is hidden by spam or blocked
if (accountChoices.hiddenUsersHashCodes.contains(hash) ||
accountChoices.spammersHashCodes.contains(hash)
) {
return true
}
// if the post is sensitive and the user doesn't want to see sensitive content
if (accountChoices.showSensitiveContent == false && thisEvent.isSensitive()) {
return true
}
// if this is a repost, consider the inner event.
if (
thisEvent is GenericRepostEvent ||
thisEvent is RepostEvent ||
thisEvent is CommunityPostApprovalEvent
) {
if (replyTo?.lastOrNull()?.isHiddenFor(accountChoices) == true) {
return true
}
}
val isHiddenByWord =
if (thisEvent is BaseTextNoteEvent) {
accountChoices.hiddenWords.any {
thisEvent.content.containsAny(accountChoices.hiddenWordsCase)
}
} else {
false
if (thisEvent is BaseTextNoteEvent) {
if (accountChoices.hiddenWordsCase.isNotEmpty() && thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) {
return true
}
}
val isSensitive = thisEvent.isSensitive()
return isBoostedNoteHidden ||
isHiddenByWord ||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
accountChoices.spammers.contains(author?.pubkeyHex) ||
(isSensitive && accountChoices.showSensitiveContent == false)
return false
}
var liveSet: NoteLiveSet? = null
@@ -858,28 +842,35 @@ open class Note(val idHex: String) {
}
@Stable
class NoteFlowSet(u: Note) {
class NoteFlowSet(
u: Note,
) {
// Observers line up here.
val metadata = NoteBundledRefresherFlow(u)
val reports = NoteBundledRefresherFlow(u)
val relays = NoteBundledRefresherFlow(u)
fun isInUse(): Boolean {
return metadata.stateFlow.subscriptionCount.value > 0
}
fun isInUse(): Boolean =
metadata.stateFlow.subscriptionCount.value > 0 ||
reports.stateFlow.subscriptionCount.value > 0 ||
relays.stateFlow.subscriptionCount.value > 0
fun destroy() {
metadata.destroy()
reports.destroy()
relays.destroy()
}
}
@Stable
class NoteLiveSet(u: Note) {
class NoteLiveSet(
u: Note,
) {
// Observers line up here.
val innerMetadata = NoteBundledRefresherLiveData(u)
val innerReactions = NoteBundledRefresherLiveData(u)
val innerBoosts = NoteBundledRefresherLiveData(u)
val innerReplies = NoteBundledRefresherLiveData(u)
val innerReports = NoteBundledRefresherLiveData(u)
val innerRelays = NoteBundledRefresherLiveData(u)
val innerZaps = NoteBundledRefresherLiveData(u)
val innerOts = NoteBundledRefresherLiveData(u)
val innerModifications = NoteBundledRefresherLiveData(u)
@@ -888,8 +879,6 @@ class NoteLiveSet(u: Note) {
val reactions = innerReactions.map { it }
val boosts = innerBoosts.map { it }
val replies = innerReplies.map { it }
val reports = innerReports.map { it }
val relays = innerRelays.map { it }
val zaps = innerZaps.map { it }
val hasEvent = innerMetadata.map { it.note.event != null }.distinctUntilChanged()
@@ -901,8 +890,7 @@ class NoteLiveSet(u: Note) {
?: false ||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
reactionState?.note?.reactions?.isNotEmpty() ?: false
}
.distinctUntilChanged()
}.distinctUntilChanged()
val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged()
@@ -912,22 +900,17 @@ class NoteLiveSet(u: Note) {
var total = 0
it.note.reactions.forEach { total += it.value.size }
total
}
.distinctUntilChanged()
}.distinctUntilChanged()
val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged()
val relayInfo = innerRelays.map { it.note.relays }
val content = innerMetadata.map { it.note.event?.content() ?: "" }
fun isInUse(): Boolean {
return metadata.hasObservers() ||
fun isInUse(): Boolean =
metadata.hasObservers() ||
reactions.hasObservers() ||
boosts.hasObservers() ||
replies.hasObservers() ||
reports.hasObservers() ||
relays.hasObservers() ||
zaps.hasObservers() ||
hasEvent.hasObservers() ||
hasReactions.hasObservers() ||
@@ -936,15 +919,12 @@ class NoteLiveSet(u: Note) {
boostCount.hasObservers() ||
innerOts.hasObservers() ||
innerModifications.hasObservers()
}
fun destroy() {
innerMetadata.destroy()
innerReactions.destroy()
innerBoosts.destroy()
innerReplies.destroy()
innerReports.destroy()
innerRelays.destroy()
innerZaps.destroy()
innerOts.destroy()
innerModifications.destroy()
@@ -952,8 +932,11 @@ class NoteLiveSet(u: Note) {
}
@Stable
class NoteBundledRefresherFlow(val note: Note) {
class NoteBundledRefresherFlow(
val note: Note,
) {
// Refreshes observers in batches.
// TODO: Replace the bundler for .sample
private val bundler = BundledUpdate(500, Dispatchers.IO)
val stateFlow = MutableStateFlow(NoteState(note))
@@ -973,7 +956,9 @@ class NoteBundledRefresherFlow(val note: Note) {
}
@Stable
class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
class NoteBundledRefresherLiveData(
val note: Note,
) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
@@ -1000,7 +985,10 @@ class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteSta
}
@Stable
class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
class NoteLoadingLiveData<Y>(
val note: Note,
initialValue: Y?,
) : MediatorLiveData<Y>(initialValue) {
override fun onActive() {
super.onActive()
if (note is AddressableNote) {
@@ -1020,24 +1008,6 @@ class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveDat
}
}
@Immutable class NoteState(val note: Note)
object RelayBriefInfoCache {
val cache = LruCache<String, RelayBriefInfo?>(50)
@Immutable
data class RelayBriefInfo(
val url: String,
val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern(),
val favIcon: String = "https://$displayUrl/favicon.ico".intern(),
)
fun get(url: String): RelayBriefInfo {
val info = cache[url]
if (info != null) return info
val newInfo = RelayBriefInfo(url)
cache.put(url, newInfo)
return newInfo
}
}
@Immutable class NoteState(
val note: Note,
)

View File

@@ -34,17 +34,20 @@ data class Settings(
val automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS,
val dontShowPushNotificationSelector: Boolean = false,
val dontAskForNotificationPermissions: Boolean = false,
val featureSet: FeatureSetType = FeatureSetType.COMPLETE,
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
)
enum class ThemeType(val screenCode: Int, val resourceId: Int) {
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 {
return when (code) {
fun parseThemeType(code: Int?): ThemeType =
when (code) {
ThemeType.SYSTEM.screenCode -> ThemeType.SYSTEM
ThemeType.LIGHT.screenCode -> ThemeType.LIGHT
ThemeType.DARK.screenCode -> ThemeType.DARK
@@ -52,21 +55,28 @@ fun parseThemeType(code: Int?): ThemeType {
ThemeType.SYSTEM
}
}
}
enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val resourceId: Int) {
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_wifi_only),
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) {
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 {
return when (code) {
fun parseConnectivityType(code: Boolean?): ConnectivityType =
when (code) {
ConnectivityType.ALWAYS.prefCode -> ConnectivityType.ALWAYS
ConnectivityType.WIFI_ONLY.prefCode -> ConnectivityType.WIFI_ONLY
ConnectivityType.NEVER.prefCode -> ConnectivityType.NEVER
@@ -74,10 +84,9 @@ fun parseConnectivityType(code: Boolean?): ConnectivityType {
ConnectivityType.ALWAYS
}
}
}
fun parseConnectivityType(screenCode: Int): ConnectivityType {
return when (screenCode) {
fun parseConnectivityType(screenCode: Int): ConnectivityType =
when (screenCode) {
ConnectivityType.ALWAYS.screenCode -> ConnectivityType.ALWAYS
ConnectivityType.WIFI_ONLY.screenCode -> ConnectivityType.WIFI_ONLY
ConnectivityType.NEVER.screenCode -> ConnectivityType.NEVER
@@ -85,39 +94,40 @@ fun parseConnectivityType(screenCode: Int): ConnectivityType {
ConnectivityType.ALWAYS
}
}
}
fun parseFeatureSetType(screenCode: Int): FeatureSetType {
return when (screenCode) {
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) {
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 {
return when (code) {
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 {
return when (screenCode) {
fun parseBooleanType(screenCode: Int): BooleanType =
when (screenCode) {
BooleanType.ALWAYS.screenCode -> BooleanType.ALWAYS
BooleanType.NEVER.screenCode -> BooleanType.NEVER
else -> {
BooleanType.ALWAYS
}
}
}

View File

@@ -62,8 +62,7 @@ class ThreadAssembler {
} else {
null
}
}
?.filterNotNull()
}?.filterNotNull()
if (roots != null && roots.isNotEmpty()) {
return roots[0]
@@ -148,9 +147,7 @@ class OnlyLatestVersionSet : MutableSet<Note> {
}
}
override fun addAll(elements: Collection<Note>): Boolean {
return elements.map { add(it) }.any()
}
override fun addAll(elements: Collection<Note>): Boolean = elements.map { add(it) }.any()
override val size: Int
get() = set.size
@@ -160,29 +157,17 @@ class OnlyLatestVersionSet : MutableSet<Note> {
map.clear()
}
override fun isEmpty(): Boolean {
return set.isEmpty()
}
override fun isEmpty(): Boolean = set.isEmpty()
override fun containsAll(elements: Collection<Note>): Boolean {
return set.containsAll(elements)
}
override fun containsAll(elements: Collection<Note>): Boolean = set.containsAll(elements)
override fun contains(element: Note): Boolean {
return set.contains(element)
}
override fun contains(element: Note): Boolean = set.contains(element)
override fun iterator(): MutableIterator<Note> {
return set.iterator()
}
override fun iterator(): MutableIterator<Note> = set.iterator()
override fun retainAll(elements: Collection<Note>): Boolean {
return set.retainAll(elements)
}
override fun retainAll(elements: Collection<Note>): Boolean = set.retainAll(elements)
override fun removeAll(elements: Collection<Note>): Boolean {
return elements.map { remove(it) }.any()
}
override fun removeAll(elements: Collection<Note>): Boolean = elements.map { remove(it) }.any()
override fun remove(element: Note): Boolean {
element.address()?.let {

View File

@@ -27,16 +27,20 @@ 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?)
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 {
return Instant.ofEpochSecond(timestamp)
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
@@ -76,8 +80,7 @@ object ThreadLevelCalculator {
accountFollowingSet,
now,
).apply { cachedSignatures.put(it, this) }
}
.maxByOrNull { it.signature.length }
}.maxByOrNull { it.signature.length }
)
val parentSignature = parent?.signature?.removeSuffix(";") ?: ""

View File

@@ -74,7 +74,6 @@ object UrlCachedPreviewer {
onReady(state)
}
},
)
.fetchUrlPreview()
).fetchUrlPreview()
}
}

View File

@@ -27,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
@@ -49,10 +51,13 @@ 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
@@ -76,7 +81,16 @@ class User(val pubkeyHex: String) {
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
@@ -96,17 +110,11 @@ class User(val pubkeyHex: String) {
return firstName
}
fun toBestDisplayName(): String {
return info?.bestName() ?: pubkeyDisplayHex()
}
fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex()
fun nip05(): String? {
return info?.nip05
}
fun nip05(): String? = info?.nip05
fun profilePicture(): String? {
return info?.picture
}
fun profilePicture(): String? = info?.picture
fun updateBookmark(event: BookmarkListEvent) {
if (event.id == latestBookmarkList?.id) return
@@ -132,10 +140,18 @@ class User(val pubkeyHex: String) {
// Update Followers of the past user list
// Update Followers of the new contact list
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
LocalCache
.getUserIfExists(it)
?.liveSet
?.innerFollowers
?.invalidateData()
}
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
LocalCache
.getUserIfExists(it)
?.liveSet
?.innerFollowers
?.invalidateData()
}
liveSet?.innerRelays?.invalidateData()
@@ -198,25 +214,19 @@ class User(val pubkeyHex: String) {
return amount
}
fun reportsBy(user: User): Set<Note> {
return reports[user] ?: emptySet()
}
fun reportsBy(user: User): Set<Note> = reports[user] ?: emptySet()
fun countReportAuthorsBy(users: Set<HexKey>): Int {
return reports.count { it.key.pubkeyHex in users }
}
fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
fun reportsBy(users: Set<HexKey>): List<Note> {
return reports
fun reportsBy(users: Set<HexKey>): List<Note> =
reports
.mapNotNull {
if (it.key.pubkeyHex in users) {
it.value
} else {
null
}
}
.flatten()
}
}.flatten()
@Synchronized
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom {
@@ -235,9 +245,7 @@ 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,
@@ -326,13 +334,9 @@ 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 {
@@ -362,37 +366,21 @@ class User(val pubkeyHex: String) {
?: false
}
fun transientFollowCount(): Int? {
return latestContactList?.unverifiedFollowKeySet()?.size
}
fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size
suspend fun transientFollowerCount(): Int {
return LocalCache.users.count { _, it -> 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.count { _, it -> 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
@@ -403,16 +391,13 @@ class User(val pubkeyHex: String) {
fun hasReport(
loggedIn: User,
type: ReportEvent.ReportType,
): Boolean {
return reports[loggedIn]?.firstOrNull {
): 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
@@ -473,14 +458,14 @@ class User(val pubkeyHex: String) {
}
@Stable
class UserFlowSet(u: User) {
class UserFlowSet(
u: User,
) {
// Observers line up here.
val follows = UserBundledRefresherFlow(u)
val relays = UserBundledRefresherFlow(u)
fun isInUse(): Boolean {
return relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
}
fun isInUse(): Boolean = relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
fun destroy() {
relays.destroy()
@@ -489,7 +474,9 @@ class UserFlowSet(u: User) {
}
@Stable
class UserLiveSet(u: User) {
class UserLiveSet(
u: User,
) {
val innerMetadata = UserBundledRefresherLiveData(u)
// UI Observers line up here.
@@ -501,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.
@@ -513,6 +501,7 @@ 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()
@@ -521,8 +510,8 @@ class UserLiveSet(u: User) {
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() ||
@@ -531,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()
@@ -547,6 +536,7 @@ class UserLiveSet(u: User) {
innerRelayInfo.destroy()
innerZaps.destroy()
innerBookmarks.destroy()
innerGallery.destroy()
innerStatuses.destroy()
}
}
@@ -558,7 +548,9 @@ data class RelayInfo(
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)
@@ -585,7 +577,9 @@ class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserSta
}
@Stable
class UserBundledRefresherFlow(val user: User) {
class UserBundledRefresherFlow(
val user: User,
) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
val stateFlow = MutableStateFlow(UserState(user))
@@ -605,7 +599,10 @@ class UserBundledRefresherFlow(val user: User) {
}
}
class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
class UserLoadingLiveData<Y>(
val user: User,
initialValue: Y?,
) : MediatorLiveData<Y>(initialValue) {
override fun onActive() {
super.onActive()
NostrSingleUserDataSource.add(user)
@@ -617,4 +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

@@ -27,7 +27,10 @@ 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) {
class LatestByKindAndAuthor<T : Event>(
private val kind: Int,
private val pubkey: String,
) {
private val _latest = MutableStateFlow<T?>(null)
val latest = _latest.asStateFlow()
@@ -41,32 +44,38 @@ class LatestByKindAndAuthor<T : Event>(private val kind: Int, private val pubkey
}
}
fun canDelete(): Boolean {
return _latest.subscriptionCount.value == 0
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() {
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
}
_latest.tryEmit(latestNote)
restart()
}
}

View File

@@ -26,7 +26,10 @@ 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) {
class LatestByKindWithETag<T : Event>(
private val kind: Int,
private val eTag: String,
) {
private val _latest = MutableStateFlow<T?>(null)
val latest = _latest.asStateFlow()
@@ -40,21 +43,26 @@ class LatestByKindWithETag<T : Event>(private val kind: Int, private val eTag: S
}
}
fun canDelete(): Boolean {
return _latest.subscriptionCount.value == 0
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() {
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
_latest.tryEmit(latestNote)
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

@@ -59,9 +59,7 @@ class BlurHashFetcher(
data: Uri,
options: Options,
imageLoader: ImageLoader,
): Fetcher {
return BlurHashFetcher(options, data)
}
): Fetcher = BlurHashFetcher(options, data)
}
}
@@ -72,7 +70,8 @@ object BlurHashRequester {
): ImageRequest {
val encodedMessage = URLEncoder.encode(message, "utf-8")
return ImageRequest.Builder(context)
return ImageRequest
.Builder(context)
.data("bluehash:$encodedMessage")
.fetcherFactory(BlurHashFetcher.Factory)
.build()

View File

@@ -28,13 +28,16 @@ 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 {
return if (richTextCache[content] != null) {
richTextCache[content]
val cached = richTextCache[content]
return if (cached != null) {
cached
} else {
val newUrls = RichTextParser().parseText(content, tags, callbackUri)
richTextCache.put(content, newUrls)

View File

@@ -28,6 +28,8 @@ 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
@@ -46,9 +48,7 @@ data class CashuToken(
object CachedCashuProcessor {
val cashuCache = LruCache<String, GenericLoadable<CashuToken>>(20)
fun cached(token: String): GenericLoadable<CashuToken> {
return cashuCache[token] ?: GenericLoadable.Loading()
}
fun cached(token: String): GenericLoadable<CashuToken> = cashuCache[token] ?: GenericLoadable.Loading()
fun parse(token: String): GenericLoadable<CashuToken> {
if (cashuCache[token] !is GenericLoadable.Loaded) {
@@ -150,7 +150,12 @@ class CashuProcessor {
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonObject.toString().toRequestBody(mediaType)
val request = Request.Builder().url(url).post(requestBody).build()
val request =
Request
.Builder()
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
val body = it.body.string()
@@ -163,13 +168,19 @@ class CashuProcessor {
feeCost,
)
} else {
val msg = tree?.get("detail")?.asText()?.split('.')?.getOrNull(0)?.ifBlank { null }
val msg =
tree
?.get("detail")
?.asText()
?.split('.')
?.getOrNull(0)
?.ifBlank { null }
onError(
context.getString(R.string.cashu_failed_redemption),
stringRes(context, R.string.cashu_failed_redemption),
if (msg != null) {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, msg)
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg)
} else {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg)
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg)
},
)
}
@@ -177,8 +188,8 @@ class CashuProcessor {
} catch (e: Exception) {
if (e is CancellationException) throw e
onError(
context.getString(R.string.cashu_successful_redemption),
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
stringRes(context, R.string.cashu_successful_redemption),
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message),
)
}
}
@@ -203,7 +214,12 @@ class CashuProcessor {
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonObject.toString().toRequestBody(mediaType)
val request = Request.Builder().url(url).post(requestBody).build()
val request =
Request
.Builder()
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
val body = it.body.string()
@@ -213,21 +229,28 @@ class CashuProcessor {
if (successful) {
onSuccess(
context.getString(R.string.cashu_successful_redemption),
context.getString(
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 }
val msg =
tree
?.get("detail")
?.asText()
?.split('.')
?.getOrNull(0)
?.ifBlank { null }
onError(
context.getString(R.string.cashu_failed_redemption),
stringRes(context, R.string.cashu_failed_redemption),
if (msg != null) {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, msg)
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg)
} else {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg)
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg)
},
)
}
@@ -235,8 +258,8 @@ class CashuProcessor {
} catch (e: Exception) {
if (e is CancellationException) throw e
onError(
context.getString(R.string.cashu_successful_redemption),
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
stringRes(context, R.string.cashu_successful_redemption),
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message),
)
}
}

View File

@@ -22,9 +22,7 @@ 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) {

View File

@@ -236,10 +236,10 @@ fun MediaMetadataRetriever.prepareVideoHeight(): Int? {
}
}
class ByteArrayMediaDataSource(var imageData: ByteArray) : MediaDataSource() {
override fun getSize(): Long {
return imageData.size.toLong()
}
class ByteArrayMediaDataSource(
var imageData: ByteArray,
) : MediaDataSource() {
override fun getSize(): Long = imageData.size.toLong()
@Throws(IOException::class)
override fun readAt(

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

@@ -33,7 +33,9 @@ import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
class LocationUtil(context: Context) {
class LocationUtil(
context: Context,
) {
companion object {
const val MIN_TIME: Long = 1000L
const val MIN_DISTANCE: Float = 0.0f
@@ -96,9 +98,7 @@ class LocationUtil(context: Context) {
object CachedGeoLocations {
val locationNames = LruCache<String, String>(20)
fun cached(geoHashStr: String): String? {
return locationNames[geoHashStr]
}
fun cached(geoHashStr: String): String? = locationNames[geoHashStr]
suspend fun geoLocate(
geoHashStr: String,

View File

@@ -31,4 +31,6 @@ fun checkNotInMainThread() {
fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
class OnMainThreadException(str: String) : RuntimeException(str)
class OnMainThreadException(
str: String,
) : RuntimeException(str)

View File

@@ -22,15 +22,13 @@ 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.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
class Nip05NostrAddressVerifier() {
class Nip05NostrAddressVerifier {
fun assembleUrl(nip05address: String): String? {
val parts = nip05address.trim().split("@")
@@ -46,7 +44,7 @@ class Nip05NostrAddressVerifier() {
suspend fun fetchNip05Json(
nip05: String,
onSuccess: (String) -> Unit,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
) = withContext(Dispatchers.IO) {
checkNotInMainThread()
@@ -60,52 +58,39 @@ class Nip05NostrAddressVerifier() {
try {
val request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
HttpClientManager.getHttpClient()
// Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.
HttpClientManager
.getHttpClient()
.newBuilder()
.followRedirects(false)
.build()
.newCall(request)
.enqueue(
object : Callback {
override fun onResponse(
call: Call,
response: Response,
) {
checkNotInMainThread()
.execute()
.use {
checkNotInMainThread()
response.use {
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",
)
}
}
}
override fun onFailure(
call: Call,
e: java.io.IOException,
) {
onError(
"Could not resolve $url. Check if the server is up and if the address $nip05 is correct",
)
e.printStackTrace()
}
},
)
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 '$url': ${e.message}")
onError("Could not resolve NIP-05 $nip05 as URL $url: ${e.message}")
}
}
suspend fun verifyNip05(
nip05: String,
onSuccess: (String) -> Unit,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
) {
// check fails on tests

View File

@@ -22,6 +22,7 @@ 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
@@ -33,14 +34,23 @@ import okhttp3.Response
import java.io.IOException
object Nip11CachedRetriever {
open class RetrieveResult(val time: Long)
open class RetrieveResult(
val time: Long,
)
class RetrieveResultError(val error: Nip11Retriever.ErrorCode, val msg: String? = null) : RetrieveResult(TimeUtils.now())
class RetrieveResultError(
val error: Nip11Retriever.ErrorCode,
val msg: String? = null,
) : RetrieveResult(TimeUtils.now())
class RetrieveResultSuccess(val data: Nip11RelayInformation) : RetrieveResult(TimeUtils.now())
class RetrieveResultSuccess(
val data: Nip11RelayInformation,
) : RetrieveResult(TimeUtils.now())
val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
val retriever = Nip11Retriever()
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
@@ -53,45 +63,53 @@ object Nip11CachedRetriever {
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 {
retriever.loadRelayInfo(
url = url,
dirtyUrl = dirtyUrl,
onInfo = {
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
onInfo(it)
},
onError = { dirtyUrl, code, errorMsg ->
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
onError(url, code, errorMsg)
},
)
retrieve(url, dirtyUrl, onInfo, onError)
}
}
} else {
retriever.loadRelayInfo(
url = url,
dirtyUrl = dirtyUrl,
onInfo = {
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
onInfo(it)
},
onError = { dirtyUrl, code, errorMsg ->
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
onError(url, code, errorMsg)
},
)
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 {
@@ -111,9 +129,14 @@ class Nip11Retriever {
checkNotInMainThread()
try {
val request: Request =
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
Request
.Builder()
.header("Accept", "application/nostr+json")
.url(url)
.build()
HttpClientManager.getHttpClientForUrl(dirtyUrl)
HttpClientManager
.getHttpClientForUrl(dirtyUrl)
.newCall(request)
.enqueue(
object : Callback {

View File

@@ -24,6 +24,7 @@ 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
@@ -37,7 +38,10 @@ object Nip96MediaServers {
ServerName("Void.cat", "https://void.cat"),
)
data class ServerName(val name: String, val baseUrl: String)
data class ServerName(
val name: String,
val baseUrl: String,
)
val cache: MutableMap<String, Nip96Retriever.ServerInfo> = mutableMapOf()
@@ -82,7 +86,8 @@ class Nip96Retriever {
checkNotInMainThread()
val request: Request =
Request.Builder()
Request
.Builder()
.header("Accept", "application/nostr+json")
.url(baseUrl.removeSuffix("/") + "/.well-known/nostr/nip96.json")
.build()

View File

@@ -21,6 +21,7 @@
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
@@ -29,7 +30,10 @@ 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
@@ -47,7 +51,9 @@ val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun randomChars() = List(16) { charPool.random() }.joinToString("")
class Nip96Uploader(val account: Account?) {
class Nip96Uploader(
val account: Account?,
) {
suspend fun uploadImage(
uri: Uri,
contentType: String?,
@@ -57,6 +63,7 @@ class Nip96Uploader(val account: Account?) {
server: Nip96MediaServers.ServerName,
contentResolver: ContentResolver,
onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent {
val serverInfo =
Nip96Retriever()
@@ -73,6 +80,7 @@ class Nip96Uploader(val account: Account?) {
serverInfo,
contentResolver,
onProgress,
context,
)
}
@@ -85,6 +93,7 @@ class Nip96Uploader(val account: Account?) {
server: Nip96Retriever.ServerInfo,
contentResolver: ContentResolver,
onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent {
checkNotInMainThread()
@@ -110,6 +119,7 @@ class Nip96Uploader(val account: Account?) {
sensitiveContent,
server,
onProgress,
context,
)
}
@@ -121,6 +131,7 @@ class Nip96Uploader(val account: Account?) {
sensitiveContent: String?,
server: Nip96Retriever.ServerInfo,
onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent {
checkNotInMainThread()
@@ -129,11 +140,11 @@ class Nip96Uploader(val account: Account?) {
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
val client = HttpClientManager.getHttpClient()
val requestBody: RequestBody
val requestBuilder = Request.Builder()
requestBody =
MultipartBody.Builder()
val requestBody: RequestBody =
MultipartBody
.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("expiration", "")
.addFormDataPart("size", length.toString())
@@ -141,8 +152,7 @@ class Nip96Uploader(val account: Account?) {
alt?.let { body.addFormDataPart("alt", it) }
sensitiveContent?.let { body.addFormDataPart("content-warning", it) }
contentType?.let { body.addFormDataPart("content_type", it) }
}
.addFormDataPart(
}.addFormDataPart(
"file",
"$fileName.$extension",
object : RequestBody() {
@@ -154,8 +164,7 @@ class Nip96Uploader(val account: Account?) {
inputStream.source().use(sink::writeAll)
}
},
)
.build()
).build()
nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) }
@@ -177,11 +186,16 @@ class Nip96Uploader(val account: Account?) {
} else if (result.status == "success" && result.nip94Event != null) {
return result.nip94Event
} else {
throw RuntimeException("Failed to upload with message: ${result.message}")
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, result.message))
}
}
} else {
throw RuntimeException("Error Uploading image: ${response.code}")
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))
}
}
}
}
@@ -190,6 +204,7 @@ class Nip96Uploader(val account: Account?) {
hash: String,
contentType: String?,
server: Nip96Retriever.ServerInfo,
context: Context,
): Boolean {
val extension =
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
@@ -215,7 +230,12 @@ class Nip96Uploader(val account: Account?) {
return result.status == "success"
}
} else {
throw RuntimeException("Error Uploading image: ${response.code}")
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))
}
}
}
}
@@ -232,7 +252,8 @@ class Nip96Uploader(val account: Account?) {
onProgress((currentResult.percentage ?: 100) / 100f)
val request: Request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(result.processingUrl)
.build()
@@ -256,13 +277,12 @@ class Nip96Uploader(val account: Account?) {
}
}
suspend fun nip98Header(url: String): String? {
return withTimeoutOrNull(5000) {
suspend fun nip98Header(url: String): String? =
withTimeoutOrNull(5000) {
suspendCancellableCoroutine { continuation ->
nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) }
}
}
}
fun nip98Header(
url: String,

View File

@@ -23,13 +23,14 @@ 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.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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
@@ -69,90 +70,94 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils
// TODO: Migrate this to a property of AccountVi
object NostrAccountDataSource : NostrDataSource("AccountData") {
object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
lateinit var account: Account
var otherAccounts = listOf<HexKey>()
val latestEOSEs = EOSEAccount()
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
fun createAccountContactListFilter(): TypedFilter {
return TypedFilter(
fun createAccountContactListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(ContactListEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1,
),
)
}
fun createAccountMetadataFilter(): TypedFilter {
return TypedFilter(
fun createAccountMetadataFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1,
),
)
}
fun createAccountRelayListFilter(): TypedFilter {
return TypedFilter(
fun createAccountRelayListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds = listOf(StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND, SearchRelayListEvent.KIND, PrivateOutboxRelayListEvent.KIND),
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? {
if (otherAccounts.isEmpty()) return null
val otherAuthors = otherAccounts.filter { it != account.userProfile().pubkeyHex }
if (otherAuthors.isEmpty()) return null
return TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
Filter(
kinds =
listOf(
MetadataEvent.KIND,
ContactListEvent.KIND,
AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND,
PrivateOutboxRelayListEvent.KIND,
SearchRelayListEvent.KIND,
FileServersEvent.KIND,
MuteListEvent.KIND,
PeopleListEvent.KIND,
),
authors = otherAccounts.filter { it != account.userProfile().pubkeyHex },
authors = otherAuthors,
limit = 100,
),
)
}
fun createAccountSettingsFilter(): TypedFilter {
return TypedFilter(
fun createAccountSettingsFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 100,
),
)
}
fun createAccountReportsFilter(): TypedFilter {
return TypedFilter(
fun createAccountReportsFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(DraftEvent.KIND, ReportEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
since =
@@ -162,18 +167,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
?.relayList,
),
)
}
fun createAccountLastPostsListFilter(): TypedFilter {
return TypedFilter(
fun createAccountLastPostsListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
authors = listOf(account.userProfile().pubkeyHex),
limit = 400,
),
)
}
fun createNotificationFilter(): TypedFilter {
val since =
@@ -186,7 +189,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
@@ -219,7 +222,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds =
listOf(
GitReplyEvent.KIND,
@@ -241,7 +244,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(GiftWrapEvent.KIND),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since =
@@ -321,15 +324,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
val noteEvent = note?.event as? GiftWrapEvent
if (noteEvent != null) {
if (relay.brief !in note.relays) {
LocalCache.justConsume(noteEvent, relay)
noteEvent.cachedGift(account.signer) {
LocalCache.justConsume(noteEvent, relay)
this.consume(it, relay)
}
}
} else {
// new event
event.cachedGift(account.signer) { this.consume(it, relay) }
LocalCache.justConsume(event, relay)
event.cachedGift(account.signer) {
LocalCache.justConsume(event, relay)
this.consume(it, relay)
}
}
}
@@ -340,15 +345,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
if (noteEvent != null) {
if (relay.brief !in note.relays) {
// adds the relay to seal and inner chat
LocalCache.consume(noteEvent, relay)
noteEvent.cachedGossip(account.signer) {
LocalCache.consume(noteEvent, relay)
LocalCache.justConsume(it, relay)
}
}
} else {
// new event
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
LocalCache.justConsume(event, relay)
event.cachedGossip(account.signer) {
LocalCache.justConsume(event, relay)
LocalCache.justConsume(it, relay)
}
}
}
@@ -382,30 +389,27 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
super.markAsSeenOnRelay(eventId, relay)
val note = LocalCache.getNoteIfExists(eventId) ?: return
val privKey = account.keyPair.privKey ?: return
val noteEvent = note.event ?: return
markInnerAsSeenOnRelay(noteEvent, privKey, relay)
markInnerAsSeenOnRelay(noteEvent, relay)
}
private fun markInnerAsSeenOnRelay(
noteEvent: EventInterface,
privKey: ByteArray,
relay: Relay,
) {
LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay)
if (noteEvent is GiftWrapEvent) {
noteEvent.cachedGift(account.signer) { gift -> markInnerAsSeenOnRelay(gift, privKey, relay) }
noteEvent.cachedGift(account.signer) { gift -> markInnerAsSeenOnRelay(gift, relay) }
} else if (noteEvent is SealedGossipEvent) {
noteEvent.cachedGossip(account.signer) { rumor ->
markInnerAsSeenOnRelay(rumor, privKey, relay)
markInnerAsSeenOnRelay(rumor, relay)
}
}
}
override fun updateChannelFilters() {
return if (hasLoadedTheBasics[account.userProfile()] != null) {
override fun updateChannelFilters() =
if (hasLoadedTheBasics[account.userProfile()] != null) {
// gets everything about the user logged in
accountChannel.typedFilters =
listOfNotNull(
@@ -419,8 +423,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountSettingsFilter(),
createAccountLastPostsListFilter(),
createOtherAccountsBaseFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
} else {
// just the basics.
accountChannel.typedFilters =
@@ -429,10 +432,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountContactListFilter(),
createAccountRelayListFilter(),
createAccountSettingsFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}
override fun auth(
relay: Relay,

View File

@@ -24,13 +24,13 @@ 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.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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 : NostrDataSource("ChatroomFeed") {
object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
var account: Account? = null
var channel: Channel? = null
@@ -58,7 +58,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
return TypedFilter(
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
filter =
JsonFilter(
Filter(
kinds = listOf(ChannelMessageEvent.KIND),
authors = listOf(myAccount.userProfile().pubkeyHex),
limit = 50,
@@ -71,7 +71,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
return TypedFilter(
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
filter =
JsonFilter(
Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
authors = listOf(myAccount.userProfile().pubkeyHex),
limit = 50,
@@ -86,7 +86,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
kinds = listOf(ChannelMessageEvent.KIND),
tags = mapOf("e" to listOfNotNull(channel?.idHex)),
limit = 200,
@@ -96,7 +96,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
tags = mapOf("a" to listOfNotNull(channel?.idHex)),
limit = 200,
@@ -113,7 +113,6 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
listOfNotNull(
createMessagesToChannelFilter(),
createMessagesByMeToChannelFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}

View File

@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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 : NostrDataSource("ChatroomFeed") {
object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
lateinit var account: Account
private var withRoom: ChatroomKey? = null
@@ -50,9 +50,9 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter =
JsonFilter(
Filter(
kinds = listOf(PrivateDmEvent.KIND),
authors = myPeer.users.map { it },
authors = myPeer.users.toList(),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since =
latestEOSEs.users[account.userProfile()]
@@ -73,7 +73,7 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter =
JsonFilter(
Filter(
kinds = listOf(PrivateDmEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
tags = mapOf("p" to myPeer.users.map { it }),
@@ -103,7 +103,6 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
listOfNotNull(
createMessagesToMeFilter(),
createMessagesFromMeFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}

View File

@@ -21,17 +21,17 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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 : NostrDataSource("MailBoxFeed") {
object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
@@ -41,11 +41,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter =
JsonFilter(
Filter(
kinds = listOf(PrivateDmEvent.KIND),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
),
)
@@ -53,11 +56,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter =
JsonFilter(
Filter(
kinds = listOf(PrivateDmEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
),
)
@@ -65,11 +71,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
kinds = listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
),
)
@@ -80,13 +89,16 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
return TypedFilter(
// Metadata comes from any relay
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(ChannelCreateEvent.KIND),
ids = followingEvents.toList(),
since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
),
)
}
@@ -99,9 +111,9 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
return followingEvents.map {
TypedFilter(
// Metadata comes from any relay
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(ChannelMetadataEvent.KIND),
tags = mapOf("e" to listOf(it)),
limit = 1,
@@ -119,11 +131,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
kinds = listOf(ChannelMessageEvent.KIND),
tags = mapOf("e" to listOf(it)),
since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
// Remember to consider spam that is being removed from the UI
limit = 50,
),
@@ -149,8 +164,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
list,
createLastChannelInfoFilter(),
createLastMessageOfEachChannelFilter(),
)
.flatten()
).flatten()
.ifEmpty { null }
}
}

View File

@@ -21,13 +21,13 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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 : NostrDataSource("SingleCommunityFeed") {
object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") {
private var communityToWatch: AddressableNote? = null
private fun createLoadCommunityFilter(): TypedFilter? {
@@ -38,7 +38,7 @@ object NostrCommunityDataSource : NostrDataSource("SingleCommunityFeed") {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
authors =
community
.moderators()

View File

@@ -23,9 +23,9 @@ 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.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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
@@ -39,7 +39,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
lateinit var account: Account
val scope = Amethyst.instance.applicationIOScope
@@ -66,15 +66,24 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
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()
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 =
JsonFilter(
Filter(
authors = follows,
kinds = listOf(ClassifiedsEvent.KIND),
limit = 300,
@@ -89,7 +98,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
kinds = listOf(ClassifiedsEvent.KIND),
tags =
mapOf(
@@ -111,7 +120,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
kinds = listOf(ClassifiedsEvent.KIND),
tags =
mapOf(
@@ -132,12 +141,12 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
)
}
fun createNIP89Filter(kTags: List<String>): List<TypedFilter> {
return listOfNotNull(
fun createNIP89Filter(kTags: List<String>): List<TypedFilter> =
listOfNotNull(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
kinds = listOf(AppDefinitionEvent.KIND),
limit = 300,
tags = mapOf("k" to kTags),
@@ -149,16 +158,18 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
),
),
)
}
fun createLiveStreamFilter(): List<TypedFilter> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
return listOfNotNull(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
authors = follows,
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
limit = 300,
@@ -173,7 +184,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
tags = mapOf("p" to it),
kinds = listOf(LiveActivitiesEvent.KIND),
limit = 100,
@@ -189,14 +200,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
fun createPublicChatFilter(): List<TypedFilter> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
val followChats = account.selectedChatsFollowList().toList()
return listOfNotNull(
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
authors = follows,
kinds = listOf(ChannelMessageEvent.KIND),
limit = 500,
@@ -211,7 +225,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
ids = followChats,
kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND),
limit = 300,
@@ -229,12 +243,15 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
fun createCommunitiesFilter(): TypedFilter {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
authors = follows,
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
limit = 300,
@@ -248,14 +265,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
fun createLiveStreamTagsFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
tags =
mapOf(
@@ -275,14 +295,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
fun createLiveStreamGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
tags =
mapOf(
@@ -302,14 +325,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
fun createPublicChatsTagsFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
kinds =
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
tags =
@@ -330,14 +356,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
fun createPublicChatsGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
kinds =
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
tags =
@@ -358,14 +387,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
fun createCommunitiesTagsFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
tags =
mapOf(
@@ -385,14 +417,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
fun createCommunitiesGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
tags =
mapOf(
@@ -437,8 +472,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
createPublicChatsTagsFilter(),
createPublicChatsGeohashesFilter(),
),
)
.toList()
).toList()
.ifEmpty { null }
}
}

View File

@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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
@@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
object NostrGeohashDataSource : AmethystNostrDataSource("SingleGeoHashFeed") {
private var geohashToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
@@ -43,7 +43,7 @@ object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
tags =
mapOf(
"g" to

View File

@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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
@@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
object NostrHashtagDataSource : AmethystNostrDataSource("SingleHashtagFeed") {
private var hashtagToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
@@ -43,7 +43,7 @@ object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
tags =
mapOf(
"t" to

View File

@@ -23,9 +23,10 @@ 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.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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
@@ -35,6 +36,7 @@ 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
@@ -45,7 +47,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
object NostrHomeDataSource : NostrDataSource("HomeFeed") {
object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
lateinit var account: Account
val scope = Amethyst.instance.applicationIOScope
@@ -80,7 +82,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter(
types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS),
filter =
JsonFilter(
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
@@ -108,6 +110,33 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
)
}
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
@@ -116,10 +145,12 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter =
JsonFilter(
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
@@ -153,10 +184,12 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter =
JsonFilter(
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
@@ -190,7 +223,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter =
JsonFilter(
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
@@ -231,10 +264,10 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
followAccountChannel.typedFilters =
listOfNotNull(
createFollowAccountsFilter(),
createFollowMetadataAndReleaseFilter(),
createFollowCommunitiesFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}

View File

@@ -20,13 +20,10 @@
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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.events.RelayAuthEvent
import com.vitorpamplona.quartz.signers.NostrSigner
class NostrLnZapPaymentResponseDataSource(
@@ -34,7 +31,7 @@ class NostrLnZapPaymentResponseDataSource(
private val toUserHex: String,
private val replyingToHex: String,
private val authSigner: NostrSigner,
) : NostrDataSource("LnZapPaymentResponseFeed") {
) : AmethystNostrDataSource("LnZapPaymentResponseFeed") {
val feedTypes = setOf(FeedType.WALLET_CONNECT)
private fun createWalletConnectServiceWatcher(): TypedFilter {
@@ -42,7 +39,7 @@ class NostrLnZapPaymentResponseDataSource(
return TypedFilter(
types = feedTypes,
filter =
JsonFilter(
Filter(
kinds = listOf(LnZapPaymentResponseEvent.KIND),
authors = listOf(fromServiceHex),
tags =
@@ -62,18 +59,4 @@ class NostrLnZapPaymentResponseDataSource(
channel.typedFilters = listOfNotNull(wc).ifEmpty { null }
}
override fun auth(
relay: Relay,
challenge: String,
) {
super.auth(relay, challenge)
RelayAuthEvent.create(relay.url, challenge, authSigner) {
Client.send(
it,
relay.url,
)
}
}
}

View File

@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.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
@@ -52,7 +52,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlin.coroutines.cancellation.CancellationException
object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFeed") {
private var searchString: String? = null
private fun createAnythingWithIDFilter(): List<TypedFilter>? {
@@ -95,7 +95,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(MetadataEvent.KIND, aTag.kind),
authors = listOfNotNull(aTag.pubKeyHex),
// just to be sure
@@ -110,7 +110,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
ids = listOfNotNull(hexToWatch),
),
),
@@ -118,7 +118,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOfNotNull(hexToWatch),
// just to be sure
@@ -135,16 +135,16 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
Filter(
kinds = listOf(MetadataEvent.KIND),
search = mySearchString,
limit = 100,
limit = 1000,
),
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
@@ -165,7 +165,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
JsonFilter(
Filter(
kinds =
listOf(
ChannelMetadataEvent.KIND,

View File

@@ -23,14 +23,14 @@ 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.amethyst.service.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.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 : NostrDataSource("SingleChannelFeed") {
object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed") {
private var channelsToWatch = setOf<Channel>()
private fun createMetadataChangeFilter(): TypedFilter? {
@@ -44,7 +44,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter =
JsonFilter(
Filter(
kinds = listOf(ChannelMetadataEvent.KIND),
tags = mapOf("e" to reactionsToWatch),
),
@@ -65,7 +65,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
return TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(ChannelCreateEvent.KIND),
ids = interestedEvents.toList(),
),
@@ -88,7 +88,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex),

View File

@@ -0,0 +1,363 @@
/**
* 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
fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> =
users
.groupBy {
it.latestEOSEs.keys
.sorted()
.joinToString(",")
}.values
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

@@ -21,17 +21,18 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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 : NostrDataSource("SingleUserFeed") {
var usersToWatch = setOf<User>()
object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
private var usersToWatch = setOf<User>()
fun createUserMetadataFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null
@@ -44,7 +45,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND),
authors = firstTimers,
),
@@ -67,8 +68,8 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND),
Filter(
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND),
authors = groupIds,
since = minEOSEs,
),
@@ -76,15 +77,14 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
TypedFilter(
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(ReportEvent.KIND),
tags = mapOf("p" to groupIds),
since = minEOSEs,
),
),
)
}
.flatten()
}.flatten()
}
val userChannel =
@@ -110,8 +110,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
listOfNotNull(
createUserMetadataFilter(),
createUserMetadataStatusReportFilter(),
)
.flatten()
).flatten()
.ifEmpty { null }
}

View File

@@ -21,11 +21,11 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.ThreadAssembler
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.TypedFilter
object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
object NostrThreadDataSource : AmethystNostrDataSource("SingleThreadFeed") {
private var eventToWatch: String? = null
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
@@ -45,7 +45,7 @@ object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
ids = eventsToLoad.toList(),
),
)

View File

@@ -21,9 +21,9 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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
@@ -39,11 +39,12 @@ 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 : NostrDataSource("UserProfileFeed") {
object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
var user: User? = null
fun loadUserProfile(user: User?) {
@@ -55,7 +56,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 1,
@@ -68,7 +69,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds =
listOf(
TextNoteEvent.KIND,
@@ -93,9 +94,10 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(LnZapEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 200,
),
)
}
@@ -105,7 +107,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(ContactListEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 1,
@@ -118,7 +120,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(ContactListEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)),
),
@@ -130,7 +132,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds = listOf(BadgeProfilesEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 1,
@@ -143,7 +145,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
Filter(
kinds =
listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND),
authors = listOf(it.pubkeyHex),
@@ -152,12 +154,26 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
)
}
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 =
JsonFilter(
Filter(
kinds = listOf(BadgeAwardEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 20,
@@ -172,13 +188,13 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
listOfNotNull(
createUserInfoFilter(),
createUserPostsFilter(),
createProfileGalleryFilter(),
createFollowFilter(),
createFollowersFilter(),
createUserReceivedZapsFilter(),
createAcceptedAwardsFilter(),
createReceivedAwardsFilter(),
createBookmarksFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}

View File

@@ -23,16 +23,21 @@ 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.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
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
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
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
@@ -40,8 +45,6 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
var job: Job? = null
val SUPPORTED_VIDEO_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")
override fun start() {
job?.cancel()
job =
@@ -61,16 +64,19 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
}
fun createContextualFilter(): TypedFilter {
val follows = account.liveStoriesFollowLists.value?.users?.toList()
val follows =
account.liveStoriesFollowLists.value
?.users
?.toList()
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
Filter(
authors = follows,
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND),
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
limit = 200,
tags = mapOf("m" to SUPPORTED_VIDEO_MIME_TYPES),
tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES),
since =
latestEOSEs.users[account.userProfile()]
?.followList
@@ -81,22 +87,25 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
}
fun createFollowTagsFilter(): TypedFilter? {
val hashToLoad = account.liveStoriesFollowLists.value?.hashtags?.toList() ?: return null
val hashToLoad =
account.liveStoriesFollowLists.value
?.hashtags
?.toList() ?: return null
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND),
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_MIME_TYPES,
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
),
limit = 100,
since =
@@ -109,22 +118,25 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
}
fun createFollowGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveStoriesFollowLists.value?.geotags?.toList() ?: return null
val hashToLoad =
account.liveStoriesFollowLists.value
?.geotags
?.toList() ?: return null
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND),
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_MIME_TYPES,
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
),
limit = 100,
since =
@@ -152,7 +164,6 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
createContextualFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}

View File

@@ -24,6 +24,7 @@ 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
@@ -31,7 +32,10 @@ import okhttp3.Request
import okio.ByteString.Companion.toByteString
import kotlin.coroutines.cancellation.CancellationException
@Immutable data class OnlineCheckResult(val timeInMs: Long, val online: Boolean)
@Immutable data class OnlineCheckResult(
val timeInMs: Long,
val online: Boolean,
)
object OnlineChecker {
val checkOnlineCache = LruCache<String, OnlineCheckResult>(100)
@@ -57,7 +61,8 @@ object OnlineChecker {
val result =
if (url.startsWith("wss")) {
val request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url.replace("wss+livekit://", "wss://"))
.header("Upgrade", "websocket")
@@ -68,7 +73,9 @@ object OnlineChecker {
.build()
val client =
HttpClientManager.getHttpClient().newBuilder()
HttpClientManager
.getHttpClient()
.newBuilder()
.eventListener(EventListener.NONE)
.protocols(listOf(Protocol.HTTP_1_1))
.build()
@@ -79,7 +86,8 @@ object OnlineChecker {
}
} else {
val request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()

View File

@@ -28,15 +28,12 @@ object PackageUtils {
private fun isPackageInstalled(
context: Context,
target: String,
): Boolean {
return context.packageManager.getInstalledApplications(0).find { info ->
): Boolean =
context.packageManager.getInstalledApplications(0).find { info ->
info.packageName == target
} != null
}
fun isOrbotInstalled(context: Context): Boolean {
return isPackageInstalled(context, "org.torproject.android")
}
fun isOrbotInstalled(context: Context): Boolean = isPackageInstalled(context, "org.torproject.android")
fun isExternalSignerInstalled(context: Context): Boolean {
val intent =

View File

@@ -29,6 +29,8 @@ 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
@@ -40,7 +42,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.math.round
class ZapPaymentHandler(val account: Account) {
class ZapPaymentHandler(
val account: Account,
) {
@Immutable
data class Payable(
val info: ZapSplitSetup,
@@ -55,6 +59,7 @@ class ZapPaymentHandler(val account: Account) {
pollOption: Int?,
message: String,
context: Context,
showErrorIfNoLnAddress: Boolean,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
@@ -77,8 +82,9 @@ class ZapPaymentHandler(val account: Account) {
if (lud16.isNullOrBlank()) {
onError(
context.getString(R.string.missing_lud16),
context.getString(
stringRes(context, R.string.missing_lud16),
stringRes(
context,
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
),
)
@@ -92,8 +98,9 @@ class ZapPaymentHandler(val account: Account) {
if (lud16.isNullOrBlank()) {
onError(
context.getString(R.string.missing_lud16),
context.getString(
stringRes(context, R.string.missing_lud16),
stringRes(
context,
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
),
)
@@ -112,7 +119,7 @@ class ZapPaymentHandler(val account: Account) {
onProgress(0.05f)
}
assembleAllInvoices(splitZapRequestPairs.toList(), amountMilliSats, message, onError, onProgress = {
assembleAllInvoices(splitZapRequestPairs.toList(), amountMilliSats, message, showErrorIfNoLnAddress, onError, onProgress = {
onProgress(it * 0.7f + 0.05f) // keeps within range.
}, context) {
if (it.isEmpty()) {
@@ -130,14 +137,15 @@ class ZapPaymentHandler(val account: Account) {
}
} else {
onPayViaIntent(
it.map {
Payable(
info = it.key.first,
user = it.key.second.user,
amountMilliSats = it.value.zapValue,
invoice = it.value.invoice,
)
}.toImmutableList(),
it
.map {
Payable(
info = it.key.first,
user = it.key.second.user,
amountMilliSats = it.value.zapValue,
invoice = it.value.invoice,
)
}.toImmutableList(),
)
onProgress(0f)
@@ -169,6 +177,18 @@ class ZapPaymentHandler(val account: Account) {
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 ->
@@ -180,7 +200,17 @@ class ZapPaymentHandler(val account: Account) {
}
} else {
val user = LocalCache.getUserIfExists(next.lnAddressOrPubKeyHex)
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user) { zapRequestJson ->
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))
}
@@ -195,6 +225,7 @@ class ZapPaymentHandler(val account: Account) {
invoices: List<Pair<ZapSplitSetup, SignAllZapRequestsReturn>>,
totalAmountMilliSats: Long,
message: String,
showErrorIfNoLnAddress: Boolean,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
context: Context,
@@ -211,6 +242,7 @@ class ZapPaymentHandler(val account: Account) {
nostrZapRequest = splitZapRequestPair.second.zapRequestJson,
zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.first.weight, totalWeight),
message = message,
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
onError = onError,
onProgressStep = { percentStepForThisPayment ->
progressAllPayments += percentStepForThisPayment / invoices.size
@@ -250,8 +282,9 @@ class ZapPaymentHandler(val account: Account) {
progressAllPayments += 0.5f / invoices.size
onProgress(progressAllPayments)
onError(
context.getString(R.string.error_dialog_pay_invoice_error),
context.getString(
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",
@@ -278,6 +311,7 @@ class ZapPaymentHandler(val account: Account) {
nostrZapRequest: String,
zapValue: Long,
message: String,
showErrorIfNoLnAddress: Boolean = true,
onError: (String, String) -> Unit,
onProgressStep: (percent: Float) -> Unit,
context: Context,
@@ -314,15 +348,19 @@ class ZapPaymentHandler(val account: Account) {
},
)
} else {
onError(
context.getString(
R.string.missing_lud16,
),
context.getString(
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
user?.toBestDisplayName() ?: splitSetup.lnAddressOrPubKeyHex,
),
)
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,
),
)
}
}
}
@@ -332,10 +370,12 @@ class ZapPaymentHandler(val account: Account) {
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) { zapRequest ->
account.createZapRequestFor(note, pollOption, message, zapType, overrideUser, additionalRelays) { zapRequest ->
println("Zap Request " + zapRequest.toJson())
onReady(zapRequest.toJson())
}
} else {

View File

@@ -27,14 +27,15 @@ import kotlinx.coroutines.CancellationException
import java.text.NumberFormat
@Stable
data class InvoiceAmount(val invoice: String, val amount: String?)
data class InvoiceAmount(
val invoice: String,
val amount: String?,
)
object CachedLnInvoiceParser {
val lnInvoicesCache = LruCache<String, InvoiceAmount>(20)
fun cached(lnurl: String): InvoiceAmount? {
return lnInvoicesCache[lnurl]
}
fun cached(lnurl: String): InvoiceAmount? = lnInvoicesCache[lnurl]
fun parse(lnbcWord: String): InvoiceAmount? {
val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord)

View File

@@ -24,8 +24,9 @@ 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.HttpClientManager
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
@@ -34,7 +35,7 @@ import java.math.RoundingMode
import java.net.URLEncoder
import kotlin.coroutines.cancellation.CancellationException
class LightningAddressResolver() {
class LightningAddressResolver {
val client = HttpClientManager.getHttpClient()
fun assembleUrl(lnaddress: String): String? {
@@ -63,8 +64,9 @@ class LightningAddressResolver() {
if (url == null) {
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,
),
@@ -74,7 +76,8 @@ class LightningAddressResolver() {
try {
val request: Request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
@@ -84,8 +87,9 @@ class LightningAddressResolver() {
onSuccess(it.body.string())
} else {
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,
@@ -99,8 +103,9 @@ class LightningAddressResolver() {
if (e is CancellationException) throw e
e.printStackTrace()
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,
@@ -133,7 +138,8 @@ class LightningAddressResolver() {
}
val request: Request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.build()
@@ -143,8 +149,8 @@ class LightningAddressResolver() {
onSuccess(it.body.string())
} else {
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.could_not_fetch_invoice_from, lnCallback),
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(context, R.string.could_not_fetch_invoice_from, lnCallback),
)
}
}
@@ -173,8 +179,9 @@ class LightningAddressResolver() {
} catch (t: Throwable) {
if (t is CancellationException) throw t
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,
),
@@ -186,8 +193,9 @@ class LightningAddressResolver() {
if (callback == null) {
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,
),
@@ -211,8 +219,9 @@ class LightningAddressResolver() {
} catch (t: Throwable) {
if (t is CancellationException) throw t
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,
@@ -236,8 +245,9 @@ class LightningAddressResolver() {
} else {
onProgress(0.0f)
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,
@@ -253,8 +263,9 @@ class LightningAddressResolver() {
?.let { reason ->
onProgress(0.0f)
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,
@@ -265,8 +276,9 @@ class LightningAddressResolver() {
?: run {
onProgress(0.0f)
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
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,

View File

@@ -31,6 +31,7 @@ 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
@@ -44,7 +45,9 @@ import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.persistentSetOf
import java.math.BigDecimal
class EventNotificationConsumer(private val applicationContext: Context) {
class EventNotificationConsumer(
private val applicationContext: Context,
) {
suspend fun consume(event: GiftWrapEvent) {
Log.d("EventNotificationConsumer", "New Notification Arrived")
if (!LocalCache.justVerify(event)) return
@@ -52,10 +55,17 @@ class EventNotificationConsumer(private val applicationContext: Context) {
// PushNotification Wraps don't include a receiver.
// Test with all logged in accounts
var matchAccount = false
LocalPreferences.allSavedAccounts().forEach {
if (it.hasPrivKey || it.loggedInWithExternalSigner) {
if (!matchAccount && it.hasPrivKey || it.loggedInWithExternalSigner) {
LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc ->
consumeIfMatchesAccount(event, acc)
Log.d("EventNotificationConsumer", "New Notification Testing if for ${it.npub}")
try {
consumeIfMatchesAccount(event, acc)
matchAccount = true
} catch (e: Exception) {
// Not for this account.
}
}
}
}
@@ -68,7 +78,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
pushWrappedEvent.cachedGift(account.signer) { notificationEvent ->
val consumed = LocalCache.hasConsumed(notificationEvent)
val verified = LocalCache.justVerify(notificationEvent)
Log.d("EventNotificationConsumer", "New Notification Arrived for ${account.userProfile().toBestDisplayName()} consumed= $consumed && verified= $verified")
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 ->
@@ -124,7 +134,8 @@ class EventNotificationConsumer(private val applicationContext: Context) {
acc: Account,
) {
if (
event.createdAt > TimeUtils.fifteenMinutesAgo() && // old event being re-broadcasted
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
// old event being re-broadcasted
event.pubKey != acc.userProfile().pubkeyHex
) { // from the user
@@ -137,7 +148,8 @@ class EventNotificationConsumer(private val applicationContext: Context) {
(
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(chatRoom)
) && !acc.isAllHidden(chatRoom.users)
) &&
!acc.isAllHidden(chatRoom.users)
if (isKnownRoom) {
val content = chatNote.event?.content() ?: ""
@@ -149,6 +161,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
event.id,
content,
user,
event.createdAt,
userPicture,
noteUri,
applicationContext,
@@ -178,9 +191,9 @@ class EventNotificationConsumer(private val applicationContext: Context) {
(
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(it)
) && !acc.isAllHidden(it.users)
}
.toSet()
) &&
!acc.isAllHidden(it.users)
}.toSet()
note.author?.let {
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
@@ -189,7 +202,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
val userPicture = note.author?.profilePicture()
val noteUri = note.toNEvent()
notificationManager()
.sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
.sendDMNotification(event.id, content, user, event.createdAt, userPicture, noteUri, applicationContext)
}
}
}
@@ -200,51 +213,105 @@ class EventNotificationConsumer(private val applicationContext: Context) {
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 })
acc.decryptContent(noteZapped) {
val zappedContent = it.split("\n").get(0)
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 =
applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
var content =
applicationContext.getString(
val content =
stringRes(
applicationContext,
R.string.app_notification_zaps_channel_message_from,
user,
)
zappedContent?.let {
content +=
" " +
applicationContext.getString(
R.string.app_notification_zaps_channel_message_for,
zappedContent,
)
}
val userPicture = senderInfo?.first?.profilePicture()
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,
@@ -255,8 +322,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
}
fun notificationManager(): NotificationManager {
return ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
fun notificationManager(): NotificationManager =
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
as NotificationManager
}
}

View File

@@ -34,6 +34,7 @@ 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
@@ -46,14 +47,13 @@ object NotificationUtils {
dmChannel =
NotificationChannel(
applicationContext.getString(R.string.app_notification_dms_channel_id),
applicationContext.getString(R.string.app_notification_dms_channel_name),
stringRes(applicationContext, R.string.app_notification_dms_channel_id),
stringRes(applicationContext, R.string.app_notification_dms_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
)
.apply {
description =
applicationContext.getString(R.string.app_notification_dms_channel_description)
}
).apply {
description =
stringRes(applicationContext, R.string.app_notification_dms_channel_description)
}
// Register the channel with the system
val notificationManager: NotificationManager =
@@ -69,14 +69,13 @@ object NotificationUtils {
zapChannel =
NotificationChannel(
applicationContext.getString(R.string.app_notification_zaps_channel_id),
applicationContext.getString(R.string.app_notification_zaps_channel_name),
stringRes(applicationContext, R.string.app_notification_zaps_channel_id),
stringRes(applicationContext, R.string.app_notification_zaps_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
)
.apply {
description =
applicationContext.getString(R.string.app_notification_zaps_channel_description)
}
).apply {
description =
stringRes(applicationContext, R.string.app_notification_zaps_channel_description)
}
// Register the channel with the system
val notificationManager: NotificationManager =
@@ -91,17 +90,19 @@ object NotificationUtils {
id: String,
messageBody: String,
messageTitle: String,
time: Long,
pictureUrl: String?,
uri: String,
applicationContext: Context,
) {
val zapChannel = getOrCreateZapChannel(applicationContext)
val channelId = applicationContext.getString(R.string.app_notification_zaps_channel_id)
val channelId = stringRes(applicationContext, R.string.app_notification_zaps_channel_id)
sendNotification(
id,
messageBody,
messageTitle,
time,
pictureUrl,
uri,
channelId,
@@ -114,17 +115,19 @@ object NotificationUtils {
id: String,
messageBody: String,
messageTitle: String,
time: Long,
pictureUrl: String?,
uri: String,
applicationContext: Context,
) {
val dmChannel = getOrCreateDMChannel(applicationContext)
val channelId = applicationContext.getString(R.string.app_notification_dms_channel_id)
val channelId = stringRes(applicationContext, R.string.app_notification_dms_channel_id)
sendNotification(
id,
messageBody,
messageTitle,
time,
pictureUrl,
uri,
channelId,
@@ -137,6 +140,7 @@ object NotificationUtils {
id: String,
messageBody: String,
messageTitle: String,
time: Long,
pictureUrl: String?,
uri: String,
channelId: String,
@@ -152,6 +156,7 @@ object NotificationUtils {
id = id,
messageBody = messageBody,
messageTitle = messageTitle,
time = time,
picture = imageResult.drawable as? BitmapDrawable,
uri = uri,
channelId,
@@ -163,6 +168,7 @@ object NotificationUtils {
id = id,
messageBody = messageBody,
messageTitle = messageTitle,
time = time,
picture = null,
uri = uri,
channelId,
@@ -176,6 +182,7 @@ object NotificationUtils {
id: String,
messageBody: String,
messageTitle: String,
time: Long,
picture: BitmapDrawable?,
uri: String,
channelId: String,
@@ -205,13 +212,13 @@ object NotificationUtils {
// Build the notification
val builderPublic =
NotificationCompat.Builder(
applicationContext,
channelId,
)
.setSmallIcon(R.drawable.amethyst)
NotificationCompat
.Builder(
applicationContext,
channelId,
).setSmallIcon(R.drawable.amethyst)
.setContentTitle(messageTitle)
.setContentText(applicationContext.getString(R.string.app_notification_private_message))
.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
@@ -219,14 +226,15 @@ object NotificationUtils {
.setContentIntent(contentPendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setWhen(time * 1000)
// Build the notification
val builder =
NotificationCompat.Builder(
applicationContext,
channelId,
)
.setSmallIcon(R.drawable.amethyst)
NotificationCompat
.Builder(
applicationContext,
channelId,
).setSmallIcon(R.drawable.amethyst)
.setContentTitle(messageTitle)
.setContentText(messageBody)
.setLargeIcon(picture?.bitmap)
@@ -237,6 +245,7 @@ object NotificationUtils {
.setPublicVersion(builderPublic.build())
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setWhen(time * 1000)
notify(notId, builder.build())
}

View File

@@ -25,19 +25,35 @@ 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.service.HttpClientManager
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>,
) {
private fun recursiveAuthCreation(
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>,
@@ -48,12 +64,32 @@ class RegisterAccounts(
return
}
val next = remainingTos.first()
coroutineScope {
val jobs =
remainingTos.map { accountRelayPair ->
async {
val result =
withTimeoutOrNull(10000) {
suspendCancellableCoroutine { continuation ->
accountRelayPair.first.createAuthEvent(accountRelayPair.second, notificationToken) { result ->
continuation.resume(result)
}
}
}
next.first.createAuthEvent(next.second, notificationToken) {
output.add(it)
recursiveAuthCreation(notificationToken, remainingTos.filter { next != it }, output, onReady)
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
@@ -63,25 +99,48 @@ class RegisterAccounts(
onReady: (List<RelayAuthEvent>) -> Unit,
) {
val readyToSend =
accounts.mapNotNull {
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
if (acc != null && acc.isWriteable()) {
val readRelays =
acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays()
accounts
.map {
Log.d(tag, "Register Account ${it.npub}")
val relayToUse = readRelays?.firstNotNullOfOrNull { if (it.value.read) it.key else null }
if (relayToUse != null) {
Pair(acc, relayToUse)
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 {
null
emptyList<Pair<Account, String>>()
}
} else {
null
}
}
}.flatten()
val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
recursiveAuthCreation(
signAllAuths(
notificationToken,
readyToSend,
listOfAuthEvents,
@@ -101,7 +160,8 @@ class RegisterAccounts(
val body = jsonObject.toRequestBody(mediaType)
val request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url("https://push.amethyst.social/register")
.post(body)
@@ -112,12 +172,6 @@ class RegisterAccounts(
val isSucess = client.newCall(request).execute().use { it.isSuccessful }
} catch (e: java.lang.Exception) {
if (e is CancellationException) throw e
val tag =
if (BuildConfig.FLAVOR == "play") {
"FirebaseMsgService"
} else {
"UnifiedPushService"
}
Log.e(tag, "Unable to register with push server", e)
}
}

View File

@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.ots
import android.util.Log
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.ots.BitcoinExplorer
import com.vitorpamplona.quartz.ots.BlockHeader
import com.vitorpamplona.quartz.ots.exceptions.UrlException
@@ -42,7 +42,8 @@ class OkHttpBlockstreamExplorer : BitcoinExplorer {
val url = "$BLOCKSTREAM_API_URL/block/$hash"
val request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/json")
.url(url)
@@ -79,7 +80,8 @@ class OkHttpBlockstreamExplorer : BitcoinExplorer {
val url = "$BLOCKSTREAM_API_URL/block-height/$height"
val request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()

View File

@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.ots.ICalendar
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
@@ -30,14 +30,15 @@ import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException
import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException
import com.vitorpamplona.quartz.ots.exceptions.UrlException
import com.vitorpamplona.quartz.ots.http.Request
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
/**
* Class representing remote calendar server interface.
*/
class OkHttpCalendar(val url: String) : ICalendar {
class OkHttpCalendar(
val url: String,
) : ICalendar {
/**
* Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest.
*
@@ -57,7 +58,8 @@ class OkHttpCalendar(val url: String) : ICalendar {
val requestBody = digest.toRequestBody(mediaType)
val request =
okhttp3.Request.Builder()
okhttp3.Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
@@ -104,7 +106,8 @@ class OkHttpCalendar(val url: String) : ICalendar {
val url = url + "/timestamp/" + Hex.encode(commitment)
val request =
okhttp3.Request.Builder()
okhttp3.Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")

View File

@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
import com.vitorpamplona.quartz.ots.Timestamp
@@ -33,7 +33,10 @@ import java.util.concurrent.BlockingQueue
/**
* For making async calls to a calendar server
*/
class OkHttpCalendarAsyncSubmit(private val url: String, private val digest: ByteArray) : ICalendarAsyncSubmit {
class OkHttpCalendarAsyncSubmit(
private val url: String,
private val digest: ByteArray,
) : ICalendarAsyncSubmit {
private var queue: BlockingQueue<Optional<Timestamp>>? = null
fun setQueue(queue: BlockingQueue<Optional<Timestamp>>?) {
@@ -49,7 +52,8 @@ class OkHttpCalendarAsyncSubmit(private val url: String, private val digest: Byt
val requestBody = digest.toRequestBody(mediaType)
val request =
okhttp3.Request.Builder()
okhttp3.Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")

View File

@@ -25,14 +25,10 @@ import com.vitorpamplona.quartz.ots.ICalendar
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
class OkHttpCalendarBuilder : CalendarBuilder {
override fun newSyncCalendar(url: String): ICalendar {
return OkHttpCalendar(url)
}
override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url)
override fun newAsyncCalendar(
url: String,
digest: ByteArray,
): ICalendarAsyncSubmit {
return OkHttpCalendarAsyncSubmit(url, digest)
}
): 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

@@ -68,14 +68,13 @@ class MultiPlayerPlaybackManager(
private fun getCallbackIntent(
callbackUri: String,
applicationContext: Context,
): PendingIntent {
return PendingIntent.getActivity(
): 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(
@@ -86,8 +85,6 @@ class MultiPlayerPlaybackManager(
applicationContext: Context,
): MediaSession {
val existingSession = playingMap.get(id) ?: cache.get(id)
// avoids saving positions for live streams otherwise caching goes crazy
val mustCachePositions = !uri.contains(".m3u8", true)
if (existingSession != null) return existingSession
val player =
@@ -111,6 +108,9 @@ class MultiPlayerPlaybackManager(
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)
@@ -156,7 +156,7 @@ class MultiPlayerPlaybackManager(
newPosition: PositionInfo,
reason: Int,
) {
if (mustCachePositions) {
if (mustCachePositions && player.playbackState != STATE_IDLE) {
cachedPositions.add(uri, newPosition.positionMs)
}
}
@@ -180,7 +180,5 @@ class MultiPlayerPlaybackManager(
}
}
fun playingContent(): Collection<MediaSession> {
return playingMap.values
}
fun playingContent(): Collection<MediaSession> = playingMap.values
}

View File

@@ -23,51 +23,10 @@ package com.vitorpamplona.amethyst.service.playback
import android.content.Intent
import android.util.Log
import androidx.annotation.OptIn
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 androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.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 {
return nonCachingFactory.supportedTypes
}
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
if (mediaItem.mediaId.contains(".m3u8", true)) {
return nonCachingFactory.createMediaSource(mediaItem)
}
return cachingFactory.createMediaSource(mediaItem)
}
}
import com.vitorpamplona.ammolite.service.HttpClientManager
class PlaybackService : MediaSessionService() {
private var videoViewedPositionCache = VideoViewedPositionCache()
@@ -75,18 +34,12 @@ class PlaybackService : MediaSessionService() {
private var managerAllInOne: MultiPlayerPlaybackManager? = null
@OptIn(UnstableApi::class)
fun newAllInOneDataSource(): MediaSource.Factory {
// This might be needed for live kit.
// return WssOrHttpFactory(HttpClientManager.getHttpClient())
return CustomMediaSourceFactory()
}
fun lazyDS(): MultiPlayerPlaybackManager {
managerAllInOne?.let {
return it
}
val newInstance = MultiPlayerPlaybackManager(newAllInOneDataSource(), videoViewedPositionCache)
val newInstance = MultiPlayerPlaybackManager(CustomMediaSourceFactory(), videoViewedPositionCache)
managerAllInOne = newInstance
return newInstance
}
@@ -102,10 +55,11 @@ class PlaybackService : MediaSessionService() {
HttpClientManager.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
}
@OptIn(UnstableApi::class)
private fun onProxyUpdated() {
val toDestroyAllInOne = managerAllInOne
managerAllInOne = MultiPlayerPlaybackManager(newAllInOneDataSource(), videoViewedPositionCache)
managerAllInOne = MultiPlayerPlaybackManager(CustomMediaSourceFactory(), videoViewedPositionCache)
toDestroyAllInOne?.releaseAppPlayers()
}

View File

@@ -59,12 +59,12 @@ class VideoCache {
// This method should be called when proxy setting changes.
fun renewCacheFactory(client: OkHttpClient) {
cacheDataSourceFactory =
CacheDataSource.Factory()
CacheDataSource
.Factory()
.setCache(simpleCache)
.setUpstreamDataSourceFactory(
OkHttpDataSource.Factory(client),
)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
).setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
}
fun get(client: OkHttpClient): CacheDataSource.Factory {

View File

@@ -32,7 +32,5 @@ class VideoViewedPositionCache {
cachedPosition.put(uri, position)
}
fun get(uri: String): Long? {
return cachedPosition.get(uri)
}
fun get(uri: String): Long? = cachedPosition.get(uri)
}

View File

@@ -44,11 +44,7 @@ class WssDataStreamCollector : WebSocketListener() {
wssData.removeAll(wssData)
}
fun canStream(): Boolean {
return wssData.size > 0
}
fun canStream(): Boolean = wssData.size > 0
fun getNextStream(): ByteString {
return wssData.pollFirst()
}
fun getNextStream(): ByteString = wssData.pollFirst()
}

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