Compare commits

...

199 Commits

Author SHA1 Message Date
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
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
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
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
Vitor Pamplona
c71c87e946 Better rounding for zaps 2024-06-11 11:30:55 -04:00
Vitor Pamplona
9ca6151313 - Adds the NIP-65 relay to zap split tag instead of kind3 relays.
- Rounds percentages to the 2 decimals.
2024-06-11 11:23:20 -04:00
Vitor Pamplona
b322efc286 Fixes padding of the zap raiser 2024-06-11 11:22:21 -04:00
Vitor Pamplona
b775c1bf74 Avoids showing error message if devs have removed their lnadress on the Zap the Devs card. 2024-06-11 11:22:09 -04:00
Vitor Pamplona
a6b2671b1c Fixes padding of the zap split section on the master note 2024-06-11 11:20:44 -04:00
Vitor Pamplona
abc5d0ea2f Fixes border of the zap card 2024-06-11 10:46:15 -04:00
Vitor Pamplona
3c25d91b98 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-11 10:36:52 -04:00
Vitor Pamplona
3d8aace55a Merge pull request #909 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-11 10:36:44 -04:00
Vitor Pamplona
bd6202f8e1 - Adds support for AVIF images
- Removes gray border in image urls that couldn't be loaded.
2024-06-11 10:25:11 -04:00
Crowdin Bot
c1875e1389 New Crowdin translations by GitHub Action 2024-06-11 14:03:00 +00:00
Vitor Pamplona
3a63c1d1ab Fixes alignment of reactions 2024-06-11 10:00:26 -04:00
Vitor Pamplona
609faf245a Reverts save button 2024-06-10 20:01:37 -04:00
Vitor Pamplona
bfd28c4b44 Reverts save button 2024-06-10 20:01:11 -04:00
Vitor Pamplona
70a8488f12 Merge pull request #907 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-10 19:59:47 -04:00
Crowdin Bot
cda6e4722c New Crowdin translations by GitHub Action 2024-06-10 23:54:07 +00:00
Vitor Pamplona
c4ecf85618 Reverting the spotlight on the Save button 2024-06-10 19:51:52 -04:00
Vitor Pamplona
6ed1f9d369 Merge pull request #906 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-10 09:59:32 -04:00
Crowdin Bot
aea765ec54 New Crowdin translations by GitHub Action 2024-06-10 13:52:45 +00:00
Vitor Pamplona
165c3bb852 Fixes not centered Blank Notes 2024-06-10 09:50:37 -04:00
Vitor Pamplona
2e9683491f Fixes scrollable drawer for all screen sizes 2024-06-10 09:50:24 -04:00
Vitor Pamplona
eea8b99622 v0.87.6 2024-06-07 20:16:52 -04:00
Vitor Pamplona
0db6a0d27b Fixes relay drops and difficulty in connecting.
Refactors the use of relay objects to avoid creating full state relay connections only once
2024-06-07 20:13:15 -04:00
Vitor Pamplona
723e575b4b Fixes the LazyColumn for Hashtags on Profile 2024-06-07 19:28:20 -04:00
Vitor Pamplona
b182d409b6 Fixes benchmark tests 2024-06-07 18:50:32 -04:00
Vitor Pamplona
043e37030e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-07 13:46:11 -04:00
Vitor Pamplona
8d730040b8 Improved description for public inbox 2024-06-07 13:46:04 -04:00
Vitor Pamplona
794a300c74 Merge pull request #904 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-07 12:42:58 -04:00
Crowdin Bot
91fd490ab7 New Crowdin translations by GitHub Action 2024-06-07 15:54:44 +00:00
Vitor Pamplona
0e1de03db8 Only updates video position when the video is ready to play 2024-06-07 11:52:39 -04:00
Vitor Pamplona
7906a94ca0 Moves the video release to a thread without damaging ON_PAUSE, ON_RELEASE order. 2024-06-07 11:42:15 -04:00
Vitor Pamplona
6ce019a86a Larger search limits for profiles 2024-06-07 10:37:13 -04:00
Vitor Pamplona
d8939779e8 Improves UI buttons of the Image Dialog 2024-06-07 10:36:56 -04:00
Vitor Pamplona
313d87a58b Removes debug marker 2024-06-06 18:43:11 -04:00
Vitor Pamplona
057e61bfe1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-06 18:39:48 -04:00
Vitor Pamplona
c94c882c3a restructures video and image views to avoid buttons getting on top of one another. 2024-06-06 18:39:39 -04:00
Vitor Pamplona
673f632563 Refactoring 2024-06-06 12:43:59 -04:00
Vitor Pamplona
1a56b2bf68 Log messages when saving to disk. 2024-06-05 18:21:20 -04:00
Vitor Pamplona
2e9964e0e8 Merge pull request #903 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-05 16:37:28 -04:00
Crowdin Bot
b80eb32991 New Crowdin translations by GitHub Action 2024-06-05 20:30:27 +00:00
Vitor Pamplona
ad281c7607 BugFix crashing with old keyboards: Forces transformations to have middle positions before the last cursor position. 2024-06-05 16:28:44 -04:00
Vitor Pamplona
3c1a918b2d Moving attestations local cache to a flow 2024-06-05 13:24:31 -04:00
Vitor Pamplona
53e6677558 Adds Hashtag search and display to Reposted Notes. 2024-06-05 11:58:02 -04:00
Vitor Pamplona
b8deb34544 Avoid using DM relays to get chats 2024-06-05 09:34:03 -04:00
Vitor Pamplona
d9cffa87ef When broadcasting privately, avoids keeping the connection open for other activities. 2024-06-05 08:33:11 -04:00
Vitor Pamplona
ae418f9d45 v0.87.5 2024-06-04 18:46:45 -04:00
Vitor Pamplona
d10b4c6bde - Re-normalizes all relays urls before connecting to reduce duplications
- Normalizes all new relays in the edit screens.
2024-06-04 18:30:01 -04:00
Vitor Pamplona
f1e516662c Adds flare.pub videos to the media tab on Amethyst 2024-06-04 17:12:14 -04:00
Vitor Pamplona
1bf8165641 Improving the detection of image types on the Media Feed. 2024-06-04 16:38:31 -04:00
Vitor Pamplona
807af3b613 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-04 16:01:50 -04:00
Vitor Pamplona
e36ec0a0a3 removes extra char on xml 2024-06-04 16:01:37 -04:00
Vitor Pamplona
3172b69271 Merge pull request #902 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-04 15:55:27 -04:00
Crowdin Bot
81ee13bebc New Crowdin translations by GitHub Action 2024-06-04 19:47:45 +00:00
Vitor Pamplona
a6a8138c7f - Performance Improvements: Simplifies intrinsic size calculations for Image Previews and Videos
- Improves rendering of chat messages
- Fixes the jittering from resizing Videos and Images during loading.
- Fixes video cut off when loading
- Fixes blurhash covering videos in landscape
- Fixes landscape video centralization
- Fixes chat using Two Pane layout for landscape and regular for portrait
2024-06-04 15:45:59 -04:00
Vitor Pamplona
16142f61b8 normalization refactoring 2024-06-04 10:54:06 -04:00
Vitor Pamplona
7193228dd2 Fixes size of the incognitoIcon on chat messages 2024-06-03 19:32:46 -04:00
Vitor Pamplona
29b8f6916b v0.87.4 2024-06-03 17:29:43 -04:00
Vitor Pamplona
a6519d57c6 Passes the video note uri so that when the user clicks in the video popup the app brings it back to the playing screen without breaking the video. 2024-06-03 17:26:40 -04:00
Vitor Pamplona
2b2ee5bbfc Avoids using Video Cache for live streams. 2024-06-03 17:18:38 -04:00
Vitor Pamplona
62a6b7b7e0 Fixes video stream position for live streams. 2024-06-03 17:18:17 -04:00
Vitor Pamplona
5e0a1f9de1 Moves SimpleCache creation to the IO thread. 2024-06-03 17:17:26 -04:00
Vitor Pamplona
571b1bc9cf Merge pull request #900 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 14:27:47 -04:00
Crowdin Bot
a3e76225e6 New Crowdin translations by GitHub Action 2024-06-03 18:26:14 +00:00
Vitor Pamplona
4388ceb529 v0.87.3 2024-06-03 14:24:28 -04:00
Vitor Pamplona
5d4fa0d732 Adds the incoming relays to the request. 2024-06-03 14:15:12 -04:00
Vitor Pamplona
ed6825de97 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 14:04:46 -04:00
Vitor Pamplona
10f623b6b3 Forces URLs to be single line. 2024-06-03 14:04:38 -04:00
Vitor Pamplona
467e5c0e4a Fixes bug that every socket is null 2024-06-03 14:04:29 -04:00
Vitor Pamplona
09d2e9593e Adds error messages to relay information dialog 2024-06-03 14:03:58 -04:00
Vitor Pamplona
cd20c0567b Merge pull request #899 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 13:27:05 -04:00
Vitor Pamplona
db15f5e2a6 Uses the user to avoid Java removing the variable due to lack of usage. 2024-06-03 13:25:56 -04:00
Crowdin Bot
18d5c7e3f4 New Crowdin translations by GitHub Action 2024-06-03 16:08:05 +00:00
Vitor Pamplona
d42ae7c9f7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 12:06:38 -04:00
Vitor Pamplona
2b741b62e8 Merge pull request #898 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 12:06:14 -04:00
Vitor Pamplona
4e1c63d36a allows more replies to filter when using DVMs 2024-06-03 12:05:58 -04:00
Vitor Pamplona
95797171d7 Fixes observer of authors 2024-06-03 12:05:46 -04:00
Crowdin Bot
395ef9695e New Crowdin translations by GitHub Action 2024-06-03 15:49:13 +00:00
Vitor Pamplona
ad1c7088c7 Loads the DVM's NIP65 records. 2024-06-03 11:47:39 -04:00
Vitor Pamplona
eda4569b64 Only shows debug relay writes in DEBUG 2024-06-03 11:47:20 -04:00
Vitor Pamplona
fb5f1e738b Adds a user and max result params to the DVM request. 2024-06-03 11:47:00 -04:00
Vitor Pamplona
e3cca6c691 Pulls NIP-65 relays even in the first lookup for the user. 2024-06-03 11:45:57 -04:00
Vitor Pamplona
ce2b8dfa11 Sends the DVM request to the NIP65 Inbox relays. 2024-06-03 11:15:36 -04:00
Vitor Pamplona
b5faf6ffc5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-03 11:15:07 -04:00
Vitor Pamplona
4ee157f27f Merge pull request #897 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 11:14:50 -04:00
Vitor Pamplona
ad8c04b79a Loads user's relays on scrolling. 2024-06-03 11:14:33 -04:00
Crowdin Bot
c0e6273954 New Crowdin translations by GitHub Action 2024-06-03 15:04:13 +00:00
Vitor Pamplona
aebade21a6 Don't display zap splits when the post is short. 2024-06-03 11:02:23 -04:00
Vitor Pamplona
bda24c0101 Separates the Account's relay info to the UI component 2024-06-03 11:01:34 -04:00
Vitor Pamplona
3d0b461550 Moves Relay Stats to it's own cache.
Refactors the isLocalRelay function
Removes unnecessary onError methods from the Relay class
Moves byte counts from Int to Long
2024-06-03 10:42:00 -04:00
Vitor Pamplona
5900e6c49f Merge pull request #896 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-03 08:45:33 -04:00
Crowdin Bot
f29fe3ced5 New Crowdin translations by GitHub Action 2024-06-03 12:38:46 +00:00
Vitor Pamplona
bf17cc8f03 Updating Firebase and Androidx JUnit 2024-06-03 08:36:45 -04:00
Vitor Pamplona
a8d10c22d4 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-06-01 16:14:47 -04:00
Vitor Pamplona
5492ebf43f Merge pull request #893 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-06-01 16:13:05 -04:00
Crowdin Bot
d66a027270 New Crowdin translations by GitHub Action 2024-06-01 19:33:36 +00:00
328 changed files with 8635 additions and 6443 deletions

1
.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

View File

@@ -12,9 +12,9 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 371
versionName "0.87.2"
buildConfigField "String", "RELEASE_NOTES_ID", "\"863124b6592359494396c22b917f9d251ad12ac31b09ea29e0265e3c088ce9f8\""
versionCode 379
versionName "0.88.2"
buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@@ -22,6 +22,7 @@ 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
@@ -47,6 +48,136 @@ class UrlUserTagTransformationTest {
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 =
@@ -70,11 +201,11 @@ class UrlUserTagTransformationTest {
assertEquals(8, transformedText.offsetMapping.originalToTransformed(8)) // Before @
assertEquals(8, transformedText.offsetMapping.originalToTransformed(9)) // Before n
assertEquals(8, transformedText.offsetMapping.originalToTransformed(10)) // Before p
assertEquals(9, transformedText.offsetMapping.originalToTransformed(11)) // Before u
assertEquals(9, transformedText.offsetMapping.originalToTransformed(12)) // Before b
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(23, transformedText.offsetMapping.originalToTransformed(71))
assertEquals(22, transformedText.offsetMapping.originalToTransformed(71))
assertEquals(23, transformedText.offsetMapping.originalToTransformed(72))
assertEquals(0, transformedText.offsetMapping.transformedToOriginal(0))
@@ -106,12 +237,12 @@ class UrlUserTagTransformationTest {
assertEquals("New Hey @Vitor Pamplona and @Vitor Pamplona", transformedText.text.text)
assertEquals(9, transformedText.offsetMapping.originalToTransformed(11))
assertEquals(9, transformedText.offsetMapping.originalToTransformed(12))
assertEquals(8, transformedText.offsetMapping.originalToTransformed(11))
assertEquals(8, transformedText.offsetMapping.originalToTransformed(12))
assertEquals(9, transformedText.offsetMapping.originalToTransformed(13))
assertEquals(23, transformedText.offsetMapping.originalToTransformed(70)) // Before 5
assertEquals(23, transformedText.offsetMapping.originalToTransformed(71)) // Before z
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
@@ -120,7 +251,7 @@ class UrlUserTagTransformationTest {
assertEquals(28, transformedText.offsetMapping.originalToTransformed(77)) // Before @
assertEquals(28, transformedText.offsetMapping.originalToTransformed(78)) // Before n
assertEquals(68, transformedText.offsetMapping.transformedToOriginal(22)) // Before a
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

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

@@ -36,6 +36,7 @@ fun TranslatableRichTextViewer(
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
id: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) = ExpandableRichTextViewer(
@@ -46,6 +47,7 @@ fun TranslatableRichTextViewer(
tags,
backgroundColor,
id,
callbackUri,
accountViewModel,
nav,
)

View File

@@ -40,6 +40,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import kotlin.time.measureTimedValue
@@ -53,7 +54,9 @@ class Amethyst : Application() {
val videoCache: VideoCache by lazy {
val newCache = VideoCache()
newCache.initFileCache(this)
runBlocking {
newCache.initFileCache(this@Amethyst)
}
newCache
}

View File

@@ -99,14 +99,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 +141,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 +157,8 @@ object LocalPreferences {
}
savedAccounts = migrated
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply()
}
}
}
@@ -244,7 +239,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 +253,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()
@@ -340,20 +336,19 @@ object LocalPreferences {
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 +356,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 +391,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)) {
@@ -604,7 +602,7 @@ object LocalPreferences {
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,
hasDonatedInVersion = hasDonatedInVersion,
pendingAttestations = pendingAttestations ?: emptyMap(),
pendingAttestations = MutableStateFlow(pendingAttestations ?: emptyMap()),
)
// Loads from DB
@@ -618,4 +616,5 @@ object LocalPreferences {
return@with account
}
}
}
}

View File

@@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
@@ -106,10 +107,6 @@ import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal
import com.vitorpamplona.quartz.utils.DualCase
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -125,6 +122,7 @@ import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
@@ -201,10 +199,10 @@ class Account(
var filterSpamFromStrangers: Boolean = true,
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
var hasDonatedInVersion: Set<String> = setOf<String>(),
var pendingAttestations: Map<HexKey, String> = mapOf<HexKey, String>(),
var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
) {
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
var transientHiddenUsers: Set<String> = setOf()
data class PaymentRequest(
val relayUrl: String,
@@ -221,13 +219,16 @@ class Account(
@Immutable
class LiveFollowLists(
val users: ImmutableSet<String> = persistentSetOf(),
val hashtags: ImmutableSet<String> = persistentSetOf(),
val geotags: ImmutableSet<String> = persistentSetOf(),
val communities: ImmutableSet<String> = persistentSetOf(),
val users: Set<String> = emptySet(),
val hashtags: Set<String> = emptySet(),
val geotags: Set<String> = emptySet(),
val communities: Set<String> = emptySet(),
)
class ListNameNotePair(val listName: String, val event: GeneralListEvent?)
class ListNameNotePair(
val listName: String,
val event: GeneralListEvent?,
)
val connectToRelaysFlow =
combineTransform(
@@ -238,10 +239,16 @@ class Account(
userProfile().flow().relays.stateFlow,
) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile ->
val baseRelaySet = activeRelays() ?: convertLocalRelays()
val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.toSet() ?: emptySet()
val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.toSet() ?: Constants.defaultSearchRelaySet
val nip65RelaySet = (nip65RelayList.note.event as? AdvertisedRelayListEvent)?.relays()
val privateOutboxRelaySet = (privateOutBox.note.event as? PrivateOutboxRelayListEvent)?.relays() ?: emptySet()
val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: Constants.defaultSearchRelaySet
val nip65RelaySet =
(nip65RelayList.note.event as? AdvertisedRelayListEvent)?.relays()?.map {
AdvertisedRelayListEvent.AdvertisedRelayInfo(
RelayUrlFormatter.normalize(it.relayUrl),
it.type,
)
}
val privateOutboxRelaySet = (privateOutBox.note.event as? PrivateOutboxRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
// ------
// DMs
@@ -250,7 +257,7 @@ class Account(
var mappedRelaySet =
baseRelaySet.map {
if (newDMRelaySet.contains(it.url)) {
Relay(it.url, true, true, it.activeTypes + FeedType.PRIVATE_DMS)
RelaySetupInfo(it.url, true, true, it.feedTypes + FeedType.PRIVATE_DMS)
} else {
it
}
@@ -258,7 +265,7 @@ class Account(
newDMRelaySet.forEach { newUrl ->
if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + Relay(newUrl, true, true, setOf(FeedType.PRIVATE_DMS))
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newUrl, true, true, setOf(FeedType.PRIVATE_DMS))
}
}
@@ -269,7 +276,7 @@ class Account(
mappedRelaySet =
mappedRelaySet.map {
if (searchRelaySet.contains(it.url)) {
Relay(it.url, true, it.write || false, it.activeTypes + FeedType.SEARCH)
RelaySetupInfo(it.url, true, it.write || false, it.feedTypes + FeedType.SEARCH)
} else {
it
}
@@ -277,7 +284,7 @@ class Account(
searchRelaySet.forEach { newUrl ->
if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + Relay(newUrl, true, false, setOf(FeedType.SEARCH))
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newUrl, true, false, setOf(FeedType.SEARCH))
}
}
@@ -288,7 +295,7 @@ class Account(
mappedRelaySet =
mappedRelaySet.map {
if (privateOutboxRelaySet.contains(it.url)) {
Relay(it.url, true, true, it.activeTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
RelaySetupInfo(it.url, true, true, it.feedTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
} else {
it
}
@@ -296,7 +303,7 @@ class Account(
privateOutboxRelaySet.forEach { newUrl ->
if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + Relay(newUrl, true, true, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newUrl, true, true, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
}
}
@@ -307,7 +314,7 @@ class Account(
mappedRelaySet =
mappedRelaySet.map {
if (localRelayServers.contains(it.url)) {
Relay(it.url, true, true, it.activeTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
RelaySetupInfo(it.url, true, true, it.feedTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
} else {
it
}
@@ -315,7 +322,7 @@ class Account(
localRelayServers.forEach { newUrl ->
if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + Relay(newUrl, true, true, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newUrl, true, true, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
}
}
@@ -329,7 +336,7 @@ class Account(
if (nip65setup != null) {
val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ
Relay(relay.url, true, relay.write || write, relay.activeTypes + setOf(FeedType.FOLLOWS, FeedType.GLOBAL, FeedType.PUBLIC_CHATS))
RelaySetupInfo(relay.url, true, relay.write || write, relay.feedTypes + setOf(FeedType.FOLLOWS, FeedType.GLOBAL, FeedType.PUBLIC_CHATS))
} else {
relay
}
@@ -339,7 +346,7 @@ class Account(
if (mappedRelaySet.none { it.url == newNip65Setup.relayUrl }) {
val write = newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ
mappedRelaySet = mappedRelaySet + Relay(newNip65Setup.relayUrl, true, write, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS))
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newNip65Setup.relayUrl, true, write, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS))
}
}
@@ -353,10 +360,10 @@ class Account(
userProfile().flow().follows.stateFlow.transformLatest {
emit(
LiveFollowLists(
it.user.cachedFollowingKeySet().toImmutableSet(),
it.user.cachedFollowingTagSet().toImmutableSet(),
it.user.cachedFollowingGeohashSet().toImmutableSet(),
it.user.cachedFollowingCommunitiesSet().toImmutableSet(),
it.user.cachedFollowingKeySet(),
it.user.cachedFollowingTagSet(),
it.user.cachedFollowingGeohashSet(),
it.user.cachedFollowingCommunitiesSet(),
),
)
}
@@ -371,8 +378,8 @@ class Account(
}
@OptIn(ExperimentalCoroutinesApi::class)
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> {
return if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> =
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
note?.flow()?.metadata?.stateFlow?.mapLatest {
val noteEvent = it.note.event as? GeneralListEvent
@@ -381,13 +388,12 @@ class Account(
} else {
MutableStateFlow(ListNameNotePair(listName, null))
}
}
fun combinePeopleListFlows(
kind3FollowsSource: Flow<LiveFollowLists>,
peopleListFollowsSource: Flow<ListNameNotePair>,
): Flow<LiveFollowLists?> {
return combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
): Flow<LiveFollowLists?> =
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
emit(null)
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
@@ -403,7 +409,6 @@ class Account(
}
}
}
}
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
combinePeopleListFlows(liveKind3FollowsFlow, liveHomeList)
@@ -457,38 +462,41 @@ class Account(
onReady(
LiveFollowLists(
users =
(listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toImmutableSet(),
(listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet(),
hashtags =
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toImmutableSet(),
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
geotags =
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toImmutableSet(),
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
communities =
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
.map { it.toTag() }
.toImmutableSet(),
.toSet(),
),
)
}
}
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? {
return withTimeoutOrNull(1000) {
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? =
withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) {
continuation.resume(it)
}
}
}
}
@Immutable
data class LiveHiddenUsers(
val hiddenUsers: ImmutableSet<String>,
val spammers: ImmutableSet<String>,
val hiddenWords: ImmutableSet<String>,
val hiddenWordsCase: List<DualCase>,
class LiveHiddenUsers(
val hiddenUsers: Set<String>,
val spammers: Set<String>,
val hiddenWords: Set<String>,
val showSensitiveContent: Boolean?,
)
) {
// speeds up isHidden calculations
val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() }
val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() }
val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) }
}
val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy {
combineTransform(
@@ -522,25 +530,22 @@ class Account(
emit(
LiveHiddenUsers(
hiddenUsers = (resultBlockList.users + resultMuteList.users).toPersistentSet(),
hiddenWords = hiddenWords.toPersistentSet(),
hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) },
hiddenUsers = (resultBlockList.users + resultMuteList.users),
hiddenWords = hiddenWords,
spammers = localLive.account.transientHiddenUsers,
showSensitiveContent = localLive.account.showSensitiveContent,
),
)
}
.stateIn(
scope,
SharingStarted.Eagerly,
LiveHiddenUsers(
hiddenUsers = persistentSetOf(),
hiddenWords = persistentSetOf(),
hiddenWordsCase = emptyList(),
spammers = transientHiddenUsers,
showSensitiveContent = showSensitiveContent,
),
)
}.stateIn(
scope,
SharingStarted.Eagerly,
LiveHiddenUsers(
hiddenUsers = setOf(),
hiddenWords = setOf(),
spammers = transientHiddenUsers,
showSensitiveContent = showSensitiveContent,
),
)
}
val liveHiddenUsers = flowHiddenUsers.asLiveData()
@@ -591,24 +596,21 @@ class Account(
filterSpamFromStrangers = filterSpam
LocalCache.antiSpam.active = filterSpamFromStrangers
if (!filterSpamFromStrangers) {
transientHiddenUsers = persistentSetOf()
transientHiddenUsers = setOf()
}
live.invalidateData()
saveable.invalidateData()
}
fun userProfile(): User {
return userProfileCache
fun userProfile(): User =
userProfileCache
?: run {
val myUser: User = LocalCache.getOrCreateUser(keyPair.pubKey.toHexKey())
userProfileCache = myUser
myUser
}
}
fun isWriteable(): Boolean {
return keyPair.privKey != null || signer is NostrSignerExternal
}
fun isWriteable(): Boolean = keyPair.privKey != null || signer is NostrSignerExternal
fun sendKind3RelayList(relays: Map<String, ContactListEvent.ReadWrite>) {
if (!isWriteable()) return
@@ -681,24 +683,16 @@ class Account(
fun reactionTo(
note: Note,
reaction: String,
): List<Note> {
return note.reactedBy(userProfile(), reaction)
}
): List<Note> = note.reactedBy(userProfile(), reaction)
fun hasBoosted(note: Note): Boolean {
return boostsTo(note).isNotEmpty()
}
fun hasBoosted(note: Note): Boolean = boostsTo(note).isNotEmpty()
fun boostsTo(note: Note): List<Note> {
return note.boostedBy(userProfile())
}
fun boostsTo(note: Note): List<Note> = note.boostedBy(userProfile())
fun hasReacted(
note: Note,
reaction: String,
): Boolean {
return note.hasReacted(userProfile(), reaction)
}
): Boolean = note.hasReacted(userProfile(), reaction)
suspend fun reactTo(
note: Note,
@@ -713,7 +707,12 @@ class Account(
if (note.event is ChatMessageEvent) {
val event = note.event as ChatMessageEvent
val users = event.recipientsPubKey().plus(event.pubKey).toSet().toList()
val users =
event
.recipientsPubKey()
.plus(event.pubKey)
.toSet()
.toList()
if (reaction.startsWith(":")) {
val emojiUrl = EmojiUrl.decode(reaction)
@@ -774,6 +773,7 @@ class Account(
message: String = "",
zapType: LnZapEvent.ZapType,
toUser: User?,
additionalRelays: Set<String>? = null,
onReady: (LnZapRequestEvent) -> Unit,
) {
if (!isWriteable()) return
@@ -781,10 +781,7 @@ class Account(
note.event?.let { event ->
LnZapRequestEvent.create(
event,
relays =
getNIP65RelayList()?.readRelays()?.toSet()
?: userProfile().latestContactList?.relays()?.keys?.ifEmpty { null }
?: localRelays.map { it.url }.toSet(),
relays = getReceivingRelays() + (additionalRelays ?: emptySet()),
signer,
pollOption,
message,
@@ -795,18 +792,23 @@ class Account(
}
}
fun hasWalletConnectSetup(): Boolean {
return zapPaymentRequest != null
}
fun getReceivingRelays(): Set<String> =
getNIP65RelayList()?.readRelays()?.toSet()
?: userProfile()
.latestContactList
?.relays()
?.filter { it.value.read }
?.keys
?.ifEmpty { null }
?: localRelays.filter { it.read }.map { it.url }.toSet()
fun isNIP47Author(pubkeyHex: String?): Boolean {
return (getNIP47Signer().pubKey == pubkeyHex)
}
fun hasWalletConnectSetup(): Boolean = zapPaymentRequest != null
fun getNIP47Signer(): NostrSigner {
return zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) }
fun isNIP47Author(pubkeyHex: String?): Boolean = (getNIP47Signer().pubKey == pubkeyHex)
fun getNIP47Signer(): NostrSigner =
zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) }
?: signer
}
fun decryptZapPaymentResponseEvent(
zapResponseEvent: LnZapPaymentResponseEvent,
@@ -873,7 +875,11 @@ class Account(
) {
LnZapRequestEvent.create(
userPubKeyHex,
userProfile().latestContactList?.relays()?.keys?.ifEmpty { null }
userProfile()
.latestContactList
?.relays()
?.keys
?.ifEmpty { null }
?: localRelays.map { it.url }.toSet(),
signer,
message,
@@ -1004,9 +1010,9 @@ class Account(
}
suspend fun updateAttestations() {
Log.d("Pending Attestations", "Updating ${pendingAttestations.size} pending attestations")
Log.d("Pending Attestations", "Updating ${pendingAttestations.value.size} pending attestations")
pendingAttestations.toMap().forEach { pair ->
pendingAttestations.value.forEach { pair ->
val newAttestation = OtsEvent.upgrade(pair.value, pair.key)
if (pair.value != newAttestation) {
@@ -1014,7 +1020,9 @@ class Account(
LocalCache.justConsume(it, null)
Client.send(it)
pendingAttestations = pendingAttestations - pair.key
pendingAttestations.update {
it - pair.key
}
}
}
}
@@ -1022,7 +1030,7 @@ class Account(
fun hasPendingAttestations(note: Note): Boolean {
val id = note.event?.id() ?: note.idHex
return pendingAttestations.get(id) != null
return pendingAttestations.value[id] != null
}
fun timestamp(note: Note) {
@@ -1031,7 +1039,9 @@ class Account(
val id = note.event?.id() ?: note.idHex
pendingAttestations = pendingAttestations + Pair(id, OtsEvent.stamp(id))
pendingAttestations.update {
it + Pair(id, OtsEvent.stamp(id))
}
saveable.invalidateData()
}
@@ -1301,7 +1311,7 @@ class Account(
fun consumeAndSendNip95(
data: FileStorageEvent,
signedEvent: FileStorageHeaderEvent,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
): Note? {
if (!isWriteable()) return null
@@ -1327,7 +1337,7 @@ class Account(
fun sendNip95(
data: FileStorageEvent,
signedEvent: FileStorageHeaderEvent,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
) {
Client.send(data, relayList = relayList)
Client.send(signedEvent, relayList = relayList)
@@ -1335,7 +1345,7 @@ class Account(
fun sendHeader(
signedEvent: FileHeaderEvent,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
onReady: (Note) -> Unit,
) {
Client.send(signedEvent, relayList = relayList)
@@ -1379,7 +1389,7 @@ class Account(
alt: String?,
sensitiveContent: Boolean,
originalHash: String? = null,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
onReady: (Note) -> Unit,
) {
if (!isWriteable()) return
@@ -1414,7 +1424,7 @@ class Account(
zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean,
zapRaiserAmount: Long? = null,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
geohash: String? = null,
nip94attachments: List<Event>? = null,
draftTag: String?,
@@ -1487,7 +1497,7 @@ class Account(
root: String?,
directMentions: Set<HexKey>,
forkedFrom: Event?,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
@@ -1576,7 +1586,7 @@ class Account(
root: String?,
directMentions: Set<HexKey>,
forkedFrom: Event?,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
@@ -1644,7 +1654,7 @@ class Account(
originalNote: Note,
notify: HexKey?,
summary: String? = null,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
) {
if (!isWriteable()) return
@@ -1674,7 +1684,7 @@ class Account(
zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean,
zapRaiserAmount: Long? = null,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
@@ -1994,7 +2004,8 @@ class Account(
if (receiver != null) {
val relayList =
(
LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver))
LocalCache
.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver))
?.event as? ChatMessageRelayListEvent
)?.relays()?.ifEmpty { null }
@@ -2185,9 +2196,7 @@ class Account(
relay: Relay,
challenge: String,
onReady: (RelayAuthEvent) -> Unit,
) {
return createAuthEvent(relay.url, challenge, onReady = onReady)
}
) = createAuthEvent(relay.url, challenge, onReady = onReady)
fun createAuthEvent(
relayUrl: String,
@@ -2255,13 +2264,9 @@ class Account(
return LocalCache.getOrCreateAddressableNote(aTag)
}
fun getBlockList(): PeopleListEvent? {
return getBlockListNote().event as? PeopleListEvent
}
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
fun getMuteList(): MuteListEvent? {
return getMuteListNote().event as? MuteListEvent
}
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
fun hideWord(word: String) {
val muteList = getMuteList()
@@ -2372,7 +2377,7 @@ class Account(
}
}
transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet()
transientHiddenUsers = (transientHiddenUsers - pubkeyHex)
live.invalidateData()
saveable.invalidateData()
}
@@ -2462,8 +2467,14 @@ class Account(
dvmPublicKey: String,
onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit,
) {
NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey, signer) {
Client.send(it)
NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey, signer.pubKey, getReceivingRelays(), signer) {
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(dvmPublicKey))?.event as? AdvertisedRelayListEvent)?.readRelays()
if (relayList != null) {
Client.sendPrivately(it, relayList)
} else {
Client.send(it)
}
LocalCache.justConsume(it, null)
onReady(it)
}
@@ -2487,9 +2498,7 @@ class Account(
return event.cachedGossip(signer, onReady)
}
fun cachedDecryptContent(note: Note): String? {
return cachedDecryptContent(note.event)
}
fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event)
fun cachedDecryptContent(event: EventInterface?): String? {
if (event == null) return null
@@ -2569,9 +2578,7 @@ class Account(
fun preferenceBetween(
source: String,
target: String,
): String? {
return languagePreferences.get("$source,$target")
}
): String? = languagePreferences.get("$source,$target")
private fun updateContactListTo(newContactList: ContactListEvent?) {
if (newContactList == null || newContactList.tags.isEmpty()) return
@@ -2584,56 +2591,46 @@ class Account(
}
// Takes a User's relay list and adds the types of feeds they are active for.
fun activeRelays(): Array<Relay>? {
fun activeRelays(): Array<RelaySetupInfo>? {
val usersRelayList =
userProfile().latestContactList?.relays()?.map {
val url = RelayUrlFormatter.normalize(it.key)
val localFeedTypes =
localRelays.firstOrNull { localRelay -> localRelay.url == it.key }?.feedTypes
localRelays.firstOrNull { localRelay -> RelayUrlFormatter.normalize(localRelay.url) == url }?.feedTypes
?: Constants.defaultRelays
.filter { defaultRelay -> defaultRelay.url == it.key }
.filter { defaultRelay -> defaultRelay.url == url }
.firstOrNull()
?.feedTypes
?: FeedType.values().toSet()
Relay(it.key, it.value.read, it.value.write, localFeedTypes)
RelaySetupInfo(url, it.value.read, it.value.write, localFeedTypes)
} ?: return null
return usersRelayList.toTypedArray()
}
fun convertLocalRelays(): Array<Relay> {
return localRelays.map { Relay(it.url, it.read, it.write, it.feedTypes) }.toTypedArray()
}
fun convertLocalRelays(): Array<RelaySetupInfo> = localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray()
fun activeGlobalRelays(): Array<String> {
return connectToRelays.value
.filter { it.activeTypes.contains(FeedType.GLOBAL) }
fun activeGlobalRelays(): Array<String> =
connectToRelays.value
.filter { it.feedTypes.contains(FeedType.GLOBAL) }
.map { it.url }
.toTypedArray()
}
fun activeWriteRelays(): List<Relay> {
return connectToRelays.value.filter { it.write }
}
fun activeWriteRelays(): List<RelaySetupInfo> = connectToRelays.value.filter { it.write }
fun isAllHidden(users: Set<HexKey>): Boolean {
return users.all { isHidden(it) }
}
fun isAllHidden(users: Set<HexKey>): Boolean = users.all { isHidden(it) }
fun isHidden(user: User) = isHidden(user.pubkeyHex)
fun isHidden(userHex: String): Boolean {
return flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
fun isHidden(userHex: String): Boolean =
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
flowHiddenUsers.value.spammers.contains(userHex)
}
fun followingKeySet(): Set<HexKey> {
return userProfile().cachedFollowingKeySet()
}
fun followingKeySet(): Set<HexKey> = userProfile().cachedFollowingKeySet()
fun followingTagSet(): Set<HexKey> {
return userProfile().cachedFollowingTagSet()
}
fun followingTagSet(): Set<HexKey> = userProfile().cachedFollowingTagSet()
fun isAcceptable(user: User): Boolean {
if (userProfile().pubkeyHex == user.pubkeyHex) {
@@ -2645,11 +2642,14 @@ class Account(
}
if (!warnAboutPostsWithReports) {
return !isHidden(user) && // if user hasn't hided this author
return !isHidden(user) &&
// if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() // if user has not reported this post
}
return !isHidden(user) && // if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() && // if user has not reported this post
return !isHidden(user) &&
// if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() &&
// if user has not reported this post
user.countReportAuthorsBy(followingKeySet()) < 5
}
@@ -2657,20 +2657,18 @@ class Account(
if (!warnAboutPostsWithReports) {
return !note.hasReportsBy(userProfile())
}
return !note.hasReportsBy(userProfile()) && // if user has not reported this post
return !note.hasReportsBy(userProfile()) &&
// if user has not reported this post
note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users
}
fun isFollowing(user: User): Boolean {
return user.pubkeyHex in followingKeySet()
}
fun isFollowing(user: User): Boolean = user.pubkeyHex in followingKeySet()
fun isFollowing(user: HexKey): Boolean {
return user in followingKeySet()
}
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
fun isAcceptable(note: Note): Boolean {
return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author
return note.author?.let { isAcceptable(it) } ?: true &&
// if user hasn't hided this author
isAcceptableDirect(note) &&
(
(note.event !is RepostEvent && note.event !is GenericRepostEvent) ||
@@ -2695,8 +2693,7 @@ class Account(
note.reportsBy(followsPlusMe) +
(note.author?.reportsBy(followsPlusMe) ?: emptyList()) +
innerReports
)
.toSet()
).toSet()
}
fun saveKind3RelayList(value: List<RelaySetupInfo>) {
@@ -2710,19 +2707,14 @@ class Account(
}
}
fun getDMRelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
fun getDMRelayListNote(): AddressableNote =
LocalCache.getOrCreateAddressableNote(
ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getDMRelayListFlow(): StateFlow<NoteState> {
return getDMRelayListNote().flow().metadata.stateFlow
}
fun getDMRelayListFlow(): StateFlow<NoteState> = getDMRelayListNote().flow().metadata.stateFlow
fun getDMRelayList(): ChatMessageRelayListEvent? {
return getDMRelayListNote().event as? ChatMessageRelayListEvent
}
fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent
fun saveDMRelayList(dmRelays: List<String>) {
if (!isWriteable()) return
@@ -2748,19 +2740,14 @@ class Account(
}
}
fun getPrivateOutboxRelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
fun getPrivateOutboxRelayListNote(): AddressableNote =
LocalCache.getOrCreateAddressableNote(
PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> {
return getPrivateOutboxRelayListNote().flow().metadata.stateFlow
}
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> = getPrivateOutboxRelayListNote().flow().metadata.stateFlow
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? {
return getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
}
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
fun savePrivateOutboxRelayList(relays: List<String>) {
if (!isWriteable()) return
@@ -2787,19 +2774,14 @@ class Account(
}
}
fun getSearchRelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
fun getSearchRelayListNote(): AddressableNote =
LocalCache.getOrCreateAddressableNote(
SearchRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getSearchRelayListFlow(): StateFlow<NoteState> {
return getSearchRelayListNote().flow().metadata.stateFlow
}
fun getSearchRelayListFlow(): StateFlow<NoteState> = getSearchRelayListNote().flow().metadata.stateFlow
fun getSearchRelayList(): SearchRelayListEvent? {
return getSearchRelayListNote().event as? SearchRelayListEvent
}
fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent
fun saveSearchRelayList(searchRelays: List<String>) {
if (!isWriteable()) return
@@ -2826,19 +2808,14 @@ class Account(
}
}
fun getNIP65RelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
fun getNIP65RelayListNote(): AddressableNote =
LocalCache.getOrCreateAddressableNote(
AdvertisedRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getNIP65RelayListFlow(): StateFlow<NoteState> {
return getNIP65RelayListNote().flow().metadata.stateFlow
}
fun getNIP65RelayListFlow(): StateFlow<NoteState> = getNIP65RelayListNote().flow().metadata.stateFlow
fun getNIP65RelayList(): AdvertisedRelayListEvent? {
return getNIP65RelayListNote().event as? AdvertisedRelayListEvent
}
fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent
fun sendNip65RelayList(relays: List<AdvertisedRelayListEvent.AdvertisedRelayInfo>) {
if (!isWriteable()) return
@@ -2865,17 +2842,11 @@ class Account(
}
}
fun getFileServersList(): FileServersEvent? {
return getFileServersNote().event as? FileServersEvent
}
fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent
fun getFileServersListFlow(): StateFlow<NoteState> {
return getFileServersNote().flow().metadata.stateFlow
}
fun getFileServersListFlow(): StateFlow<NoteState> = getFileServersNote().flow().metadata.stateFlow
fun getFileServersNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))
}
fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))
fun sendFileServersList(servers: List<String>) {
if (!isWriteable()) return
@@ -2937,13 +2908,9 @@ class Account(
}
}
fun loadLastRead(route: String): Long {
return lastReadPerRoute[route] ?: 0
}
fun loadLastRead(route: String): Long = lastReadPerRoute[route] ?: 0
fun hasDonatedInThisVersion(): Boolean {
return hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
}
fun hasDonatedInThisVersion(): Boolean = hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
fun markDonatedInThisVersion() {
hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME
@@ -2964,7 +2931,7 @@ class Account(
it.cache.spamMessages.snapshot().values.forEach {
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex).toImmutableSet()
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex)
live.invalidateData()
}
}
@@ -2985,8 +2952,9 @@ class Account(
}
}
class AccountLiveData(private val account: Account) :
LiveData<AccountState>(AccountState(account)) {
class AccountLiveData(
private val account: Account,
) : LiveData<AccountState>(AccountState(account)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.Default)
@@ -3003,4 +2971,6 @@ class AccountLiveData(private val account: Account) :
}
}
@Immutable class AccountState(val account: Account)
@Immutable class AccountState(
val account: Account,
)

View File

@@ -26,6 +26,7 @@ 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.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32
@@ -69,10 +70,6 @@ class AntiSpamFilter {
if (
(recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null
) {
Log.w(
"Potential SPAM Message for sharing",
"${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, null)}",
)
Log.w(
"Potential SPAM Message",
"${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}",
@@ -81,6 +78,10 @@ class AntiSpamFilter {
// Log down offenders
logOffender(hash, event)
if (relay != null) {
RelayStats.newSpam(relay.url, "https://njump.me/${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, relay.url)}")
}
liveSpam.invalidateData()
return true
@@ -100,7 +101,7 @@ class AntiSpamFilter {
spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id)))
} else {
val spammer = spamMessages.get(hashCode)
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
spammer.duplicatedMessages += event.id
}
}

View File

@@ -209,13 +209,13 @@ object LocalCache {
} as LatestByKindAndAuthor<T>
}
fun updateObservables(event: Event) {
val observablesOfKind = observablesByKindAndETag[event.kind()] ?: return
private fun updateObservables(event: Event) {
val observablesOfKind = observablesByKindAndETag[event.kind] ?: return
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? {
@@ -450,7 +450,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -474,6 +473,8 @@ object LocalCache {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
// Log.d("TN", "New Response ${event.taggedEvents().joinToString(", ") { it }}}")
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
@@ -623,7 +624,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -648,7 +648,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -673,7 +672,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -711,7 +709,6 @@ object LocalCache {
if (note.event?.id() == event.id()) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -746,7 +743,6 @@ object LocalCache {
if (note.event?.id() == event.id()) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -804,7 +800,7 @@ 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>()
@@ -827,7 +823,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -1533,7 +1528,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}
@@ -1572,7 +1566,6 @@ object LocalCache {
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return
}

View File

@@ -75,7 +75,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 +95,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 +107,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
@@ -163,14 +165,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 +185,6 @@ open class Note(val idHex: String) {
} else {
null
}
}
open fun address(): ATag? = null
@@ -259,7 +258,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 +292,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 +401,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 +418,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 +524,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 +627,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 +642,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 +654,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 +665,8 @@ open class Note(val idHex: String) {
)
}
fun isNewThread(): Boolean {
return (
fun isNewThread(): Boolean =
(
event is RepostEvent ||
event is GenericRepostEvent ||
replyTo == null ||
@@ -685,29 +674,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 +695,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 +740,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 +841,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 +878,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 +889,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 +899,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 +918,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 +931,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 +955,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 +984,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,17 +1007,20 @@ class NoteLoadingLiveData<Y>(val note: Note, initialValue: Y?) : MediatorLiveDat
}
}
@Immutable class NoteState(val note: Note)
@Immutable class NoteState(
val note: Note,
)
object RelayBriefInfoCache {
val cache = LruCache<String, RelayBriefInfo?>(50)
@Immutable
data class RelayBriefInfo(
class RelayBriefInfo(
val url: String,
val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern(),
val favIcon: String = "https://$displayUrl/favicon.ico".intern(),
)
) {
val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern()
val favIcon: String = "https://$displayUrl/favicon.ico".intern()
}
fun get(url: String): RelayBriefInfo {
val info = cache[url]

View File

@@ -28,12 +28,5 @@ data class RelaySetupInfo(
val url: String,
val read: Boolean,
val write: Boolean,
val errorCount: Int = 0,
val downloadCountInBytes: Int = 0,
val uploadCountInBytes: Int = 0,
val spamCount: Int = 0,
val feedTypes: Set<FeedType>,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
)

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),
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

@@ -28,14 +28,18 @@ 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)
val newUrls = RichTextParser().parseText(content, tags, callbackUri)
richTextCache.put(content, newUrls)
newUrls
}

View File

@@ -28,6 +28,7 @@ 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.quartz.events.Event
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
@@ -46,9 +47,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 +149,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 +167,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 +187,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 +213,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 +228,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 +257,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

@@ -103,6 +103,16 @@ object HttpClientManager {
}
}
fun getHttpClientForUrl(url: String): OkHttpClient {
// TODO: How to identify relays on the local network?
val isLocalHost = url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost")
return if (isLocalHost) {
getHttpClient(false)
} else {
getHttpClient()
}
}
fun getHttpClient(useProxy: Boolean = true): OkHttpClient {
return if (useProxy) {
if (this.defaultHttpClient == null) {

View File

@@ -25,12 +25,9 @@ import com.vitorpamplona.amethyst.BuildConfig
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 +43,7 @@ class Nip05NostrAddressVerifier() {
suspend fun fetchNip05Json(
nip05: String,
onSuccess: (String) -> Unit,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
) = withContext(Dispatchers.IO) {
checkNotInMainThread()
@@ -60,52 +57,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

@@ -33,14 +33,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 +62,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,10 +128,14 @@ class Nip11Retriever {
checkNotInMainThread()
try {
val request: Request =
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
val isLocalHost = dirtyUrl.startsWith("ws://127.0.0.1") || dirtyUrl.startsWith("ws://localhost")
Request
.Builder()
.header("Accept", "application/nostr+json")
.url(url)
.build()
HttpClientManager.getHttpClient(useProxy = !isLocalHost)
HttpClientManager
.getHttpClientForUrl(dirtyUrl)
.newCall(request)
.enqueue(
object : Callback {

View File

@@ -27,6 +27,7 @@ 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.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@@ -76,8 +77,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
val latestEOSEs = EOSEAccount()
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
fun createAccountContactListFilter(): TypedFilter {
return TypedFilter(
fun createAccountContactListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
@@ -86,10 +87,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
limit = 1,
),
)
}
fun createAccountMetadataFilter(): TypedFilter {
return TypedFilter(
fun createAccountMetadataFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
@@ -98,10 +98,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
limit = 1,
),
)
}
fun createAccountRelayListFilter(): TypedFilter {
return TypedFilter(
fun createAccountRelayListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
@@ -110,12 +109,11 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
limit = 10,
),
)
}
fun createOtherAccountsBaseFilter(): TypedFilter? {
if (otherAccounts.isEmpty()) return null
return TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds =
@@ -136,8 +134,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
)
}
fun createAccountSettingsFilter(): TypedFilter {
return TypedFilter(
fun createAccountSettingsFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
@@ -146,10 +144,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
limit = 100,
),
)
}
fun createAccountReportsFilter(): TypedFilter {
return TypedFilter(
fun createAccountReportsFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
@@ -162,10 +159,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
?.relayList,
),
)
}
fun createAccountLastPostsListFilter(): TypedFilter {
return TypedFilter(
fun createAccountLastPostsListFilter(): TypedFilter =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
@@ -173,7 +169,6 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
limit = 400,
),
)
}
fun createNotificationFilter(): TypedFilter {
val since =
@@ -404,8 +399,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
}
}
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 +414,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountSettingsFilter(),
createAccountLastPostsListFilter(),
createOtherAccountsBaseFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
} else {
// just the basics.
accountChannel.typedFilters =
@@ -429,10 +423,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountContactListFilter(),
createAccountRelayListFilter(),
createAccountSettingsFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}
override fun auth(
relay: Relay,

View File

@@ -52,7 +52,7 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
filter =
JsonFilter(
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()]
@@ -103,7 +103,6 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
listOfNotNull(
createMessagesToMeFilter(),
createMessagesFromMeFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}

View File

@@ -21,8 +21,8 @@
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.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@@ -80,7 +80,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
return TypedFilter(
// Metadata comes from any relay
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(ChannelCreateEvent.KIND),
@@ -99,7 +99,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
return followingEvents.map {
TypedFilter(
// Metadata comes from any relay
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(ChannelMetadataEvent.KIND),

View File

@@ -92,19 +92,6 @@ abstract class NostrDataSource(val debugName: String) {
}
}
override fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
) {
// if (subscriptions.containsKey(subscriptionId)) {
// Log.e(
// this@NostrDataSource.javaClass.simpleName,
// "Relay OnError ${relay.url}: ${error.message}"
// )
// }
}
override fun onRelayStateChange(
type: Relay.StateType,
relay: Relay,
@@ -207,14 +194,12 @@ abstract class NostrDataSource(val debugName: String) {
private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateFilters() {
scope.launch(Dispatchers.IO) {
bundler.invalidate {
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
bundler.invalidate {
// println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
resetFiltersSuspend()
}
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
resetFiltersSuspend()
}
}

View File

@@ -120,6 +120,8 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
@@ -157,6 +159,8 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,

View File

@@ -138,7 +138,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
JsonFilter(
kinds = listOf(MetadataEvent.KIND),
search = mySearchString,
limit = 100,
limit = 1000,
),
),
TypedFilter(

View File

@@ -163,7 +163,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
tags = mapOf("e" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 1000,
limit = 10000,
),
),
TypedFilter(
@@ -178,8 +178,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
),
tags = mapOf("e" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 10,
limit = 100,
),
),
)
@@ -245,7 +244,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
eventsToWatch.forEach {
val eose = it.lastReactionsDownloadTime[relayUrl]
if (eose == null) {
it.lastReactionsDownloadTime = it.lastReactionsDownloadTime + Pair(relayUrl, EOSETime(time))
it.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
@@ -254,7 +253,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
addressesToWatch.forEach {
val eose = it.lastReactionsDownloadTime[relayUrl]
if (eose == null) {
it.lastReactionsDownloadTime = it.lastReactionsDownloadTime + Pair(relayUrl, EOSETime(time))
it.lastReactionsDownloadTime += Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}

View File

@@ -25,12 +25,13 @@ 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.quartz.events.AdvertisedRelayListEvent
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>()
private var usersToWatch = setOf<User>()
fun createUserMetadataFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null
@@ -44,7 +45,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND),
kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND),
authors = firstTimers,
),
),
@@ -67,7 +68,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND),
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND),
authors = groupIds,
since = minEOSEs,
),
@@ -82,8 +83,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
),
),
)
}
.flatten()
}.flatten()
}
val userChannel =
@@ -109,8 +109,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
listOfNotNull(
createUserMetadataFilter(),
createUserMetadataStatusReportFilter(),
)
.flatten()
).flatten()
.ifEmpty { null }
}

View File

@@ -96,6 +96,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
JsonFilter(
kinds = listOf(LnZapEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 200,
),
)
}
@@ -178,7 +179,6 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
createAcceptedAwardsFilter(),
createReceivedAwardsFilter(),
createBookmarksFilter(),
)
.ifEmpty { null }
).ifEmpty { null }
}
}

View File

@@ -28,10 +28,15 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif")
val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet()
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
lateinit var account: Account
@@ -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 =
@@ -68,9 +71,9 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
filter =
JsonFilter(
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
@@ -89,14 +92,14 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND),
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 =
@@ -117,14 +120,14 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
types = setOf(FeedType.GLOBAL),
filter =
JsonFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND),
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 =

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

@@ -26,6 +26,7 @@ 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.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
@@ -124,7 +127,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 +141,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() ?: ""
@@ -178,9 +183,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) {
@@ -223,17 +228,19 @@ class EventNotificationConsumer(private val applicationContext: Context) {
val user = senderInfo.first.toBestDisplayName()
var title =
applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
var content =
applicationContext.getString(
stringRes(
applicationContext,
R.string.app_notification_zaps_channel_message_from,
user,
)
zappedContent?.let {
content +=
" " +
applicationContext.getString(
stringRes(
applicationContext,
R.string.app_notification_zaps_channel_message_for,
zappedContent,
)
@@ -255,8 +262,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 =
@@ -96,7 +95,7 @@ object NotificationUtils {
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,
@@ -119,7 +118,7 @@ object NotificationUtils {
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,
@@ -205,13 +204,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
@@ -222,11 +221,11 @@ object NotificationUtils {
// 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)

View File

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

@@ -86,6 +86,8 @@ 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 =
@@ -115,7 +117,9 @@ class MultiPlayerPlaybackManager(
playingMap.put(id, mediaSession)
} else {
player.setWakeMode(C.WAKE_MODE_NONE)
cachedPositions.add(uri, player.currentPosition)
if (mustCachePositions) {
cachedPositions.add(uri, player.currentPosition)
}
cache.put(id, mediaSession)
playingMap.remove(id, mediaSession)
}
@@ -125,20 +129,22 @@ class MultiPlayerPlaybackManager(
when (playbackState) {
STATE_IDLE -> {
// only saves if it wqs playing
if (abs(player.currentPosition) > 1) {
if (mustCachePositions && abs(player.currentPosition) > 1) {
cachedPositions.add(uri, player.currentPosition)
}
}
STATE_READY -> {
cachedPositions.get(uri)?.let { lastPosition ->
if (abs(player.currentPosition - lastPosition) > 5 * 60) {
player.seekTo(lastPosition)
if (mustCachePositions) {
cachedPositions.get(uri)?.let { lastPosition ->
if (abs(player.currentPosition - lastPosition) > 5 * 60) {
player.seekTo(lastPosition)
}
}
}
}
else -> {
// only saves if it wqs playing
if (abs(player.currentPosition) > 1) {
if (mustCachePositions && abs(player.currentPosition) > 1) {
cachedPositions.add(uri, player.currentPosition)
}
}
@@ -150,7 +156,9 @@ class MultiPlayerPlaybackManager(
newPosition: PositionInfo,
reason: Int,
) {
cachedPositions.add(uri, newPosition.positionMs)
if (mustCachePositions && player.playbackState != STATE_IDLE) {
cachedPositions.add(uri, newPosition.positionMs)
}
}
},
)

View File

@@ -34,41 +34,38 @@ import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.service.HttpClientManager
import okhttp3.OkHttpClient
class WssOrHttpFactory(httpClient: OkHttpClient) : MediaSource.Factory {
@UnstableApi
val http = DefaultMediaSourceFactory(OkHttpDataSource.Factory(httpClient))
/**
* 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()))
@UnstableApi
val wss = DefaultMediaSourceFactory(WssStreamDataSource.Factory(httpClient))
@OptIn(UnstableApi::class)
override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory {
http.setDrmSessionManagerProvider(drmSessionManagerProvider)
wss.setDrmSessionManagerProvider(drmSessionManagerProvider)
cachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
nonCachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
return this
}
@OptIn(UnstableApi::class)
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory {
http.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
wss.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
cachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
nonCachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
return this
}
@OptIn(UnstableApi::class)
override fun getSupportedTypes(): IntArray {
return http.supportedTypes
return nonCachingFactory.supportedTypes
}
@OptIn(UnstableApi::class)
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
return if (mediaItem.mediaId.startsWith("wss")) {
wss.createMediaSource(mediaItem)
} else {
http.createMediaSource(mediaItem)
if (mediaItem.mediaId.contains(".m3u8", true)) {
return nonCachingFactory.createMediaSource(mediaItem)
}
return cachingFactory.createMediaSource(mediaItem)
}
}
@@ -81,7 +78,7 @@ class PlaybackService : MediaSessionService() {
fun newAllInOneDataSource(): MediaSource.Factory {
// This might be needed for live kit.
// return WssOrHttpFactory(HttpClientManager.getHttpClient())
return DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
return CustomMediaSourceFactory()
}
fun lazyDS(): MultiPlayerPlaybackManager {

View File

@@ -27,6 +27,8 @@ import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import java.io.File
@@ -41,16 +43,17 @@ class VideoCache {
lateinit var cacheDataSourceFactory: CacheDataSource.Factory
@Synchronized
fun initFileCache(context: Context) {
suspend fun initFileCache(context: Context) {
exoDatabaseProvider = StandaloneDatabaseProvider(context)
simpleCache =
SimpleCache(
File(context.cacheDir, "exoplayer"),
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider,
)
withContext(Dispatchers.IO) {
simpleCache =
SimpleCache(
File(context.cacheDir, "exoplayer"),
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider,
)
}
}
// This method should be called when proxy setting changes.

View File

@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service.relays
import android.util.Log
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
@@ -41,10 +42,10 @@ object Client : RelayPool.Listener {
@Synchronized
fun reconnect(
relays: Array<Relay>?,
relays: Array<RelaySetupInfo>?,
onlyIfChanged: Boolean = false,
) {
Log.d("Relay", "Relay Pool Reconnecting to ${relays?.size} relays: \n${relays?.joinToString("\n") { it.url + " " + it.read + " " + it.write + " " + it.activeTypes.joinToString(",") { it.name } }}")
Log.d("Relay", "Relay Pool Reconnecting to ${relays?.size} relays: \n${relays?.joinToString("\n") { it.url + " " + it.read + " " + it.write + " " + it.feedTypes.joinToString(",") { it.name } }}")
checkNotInMainThread()
if (onlyIfChanged) {
@@ -56,10 +57,11 @@ object Client : RelayPool.Listener {
}
if (relays != null) {
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.feedTypes) }
RelayPool.register(this)
RelayPool.loadRelays(relays.toList())
RelayPool.loadRelays(newRelays)
RelayPool.requestAndWatch()
this.relays = relays
this.relays = newRelays.toTypedArray()
}
}
} else {
@@ -70,15 +72,16 @@ object Client : RelayPool.Listener {
}
if (relays != null) {
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.feedTypes) }
RelayPool.register(this)
RelayPool.loadRelays(relays.toList())
RelayPool.loadRelays(newRelays)
RelayPool.requestAndWatch()
this.relays = relays
this.relays = newRelays.toTypedArray()
}
}
}
fun isSameRelaySetConfig(newRelayConfig: Array<Relay>?): Boolean {
fun isSameRelaySetConfig(newRelayConfig: Array<RelaySetupInfo>?): Boolean {
if (relays.size != newRelayConfig?.size) return false
relays.forEach { oldRelayInfo ->
@@ -142,7 +145,7 @@ object Client : RelayPool.Listener {
signedEvent: EventInterface,
relay: String? = null,
feedTypes: Set<FeedType>? = null,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
onDone: (() -> Unit)? = null,
) {
checkNotInMainThread()
@@ -165,7 +168,7 @@ object Client : RelayPool.Listener {
checkNotInMainThread()
relayList.forEach { relayUrl ->
RelayPool.getOrCreateRelay(relayUrl, setOf(FeedType.PRIVATE_DMS), { }) {
RelayPool.getOrCreateRelay(relayUrl, emptySet(), { }) {
it.sendOverride(signedEvent)
}
}
@@ -194,19 +197,6 @@ object Client : RelayPool.Listener {
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
) {
// Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly.
GlobalScope.launch(Dispatchers.Default) {
listeners.forEach { it.onError(error, subscriptionId, relay) }
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onRelayStateChange(
type: Relay.StateType,
@@ -284,13 +274,6 @@ object Client : RelayPool.Listener {
afterEOSE: Boolean,
) = Unit
/** A new or repeat message was received */
open fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
) = Unit
/** Connected to or disconnected from a relay */
open fun onRelayStateChange(
type: Relay.StateType,

View File

@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service.relays
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
object Constants {
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
@@ -36,21 +37,26 @@ object Constants {
val defaultRelays =
arrayOf(
// Free relays for only DMs, Chats and Follows due to the amount of spam
RelaySetupInfo("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://nostr.oxtr.dev", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://nostr.fmt.wiz.biz", read = true, write = false, feedTypes = activeTypesChats),
RelaySetupInfo("wss://relay.damus.io", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.bitcoiner.social"), read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.nostr.bg"), read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.oxtr.dev"), read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.fmt.wiz.biz"), read = true, write = false, feedTypes = activeTypesChats),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.damus.io"), read = true, write = true, feedTypes = activeTypes),
// Global
RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypesGlobalChats),
RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypesGlobalChats),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.mom"), read = true, write = true, feedTypes = activeTypesGlobalChats),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nos.lol"), read = true, write = true, feedTypes = activeTypesGlobalChats),
// Paid relays
RelaySetupInfo("wss://nostr.wine", read = true, write = false, feedTypes = activeTypesGlobalChats),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.wine"), read = true, write = false, feedTypes = activeTypesGlobalChats),
// Supporting NIP-50
RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo("wss://nostr.wine", read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo("wss://relay.noswhere.com", read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.nostr.band"), read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.wine"), read = true, write = false, feedTypes = activeTypesSearch),
RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.noswhere.com"), read = true, write = false, feedTypes = activeTypesSearch),
)
val defaultSearchRelaySet = setOf("wss://relay.nostr.band", "wss://nostr.wine", "wss://relay.noswhere.com")
val defaultSearchRelaySet =
setOf(
RelayUrlFormatter.normalize("wss://relay.nostr.band"),
RelayUrlFormatter.normalize("wss://nostr.wine"),
RelayUrlFormatter.normalize("wss://relay.noswhere.com"),
)
}

View File

@@ -23,14 +23,15 @@ package com.vitorpamplona.amethyst.service.relays
import android.util.Log
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.RelayAuthEvent
import com.vitorpamplona.quartz.events.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import kotlinx.coroutines.CancellationException
import okhttp3.Request
import okhttp3.Response
@@ -59,38 +60,24 @@ class Relay(
val write: Boolean = true,
val activeTypes: Set<FeedType> = FeedType.values().toSet(),
) {
val brief = RelayBriefInfoCache.get(url)
companion object {
// waits 3 minutes to reconnect once things fail
const val RECONNECTING_IN_SECONDS = 60 * 3
}
private val httpClient =
if (url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost")) {
HttpClientManager.getHttpClient(false)
} else {
HttpClientManager.getHttpClient()
}
val brief = RelayBriefInfoCache.get(url)
private var listeners = setOf<Listener>()
private var socket: WebSocket? = null
private var isReady: Boolean = false
private var usingCompression: Boolean = false
var eventDownloadCounterInBytes = 0
var eventUploadCounterInBytes = 0
private var lastConnectTentative: Long = 0L
var spamCounter = 0
var errorCounter = 0
var pingInMs: Long? = null
private var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
var lastConnectTentative: Long = 0L
var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
val authResponse = mutableMapOf<HexKey, Boolean>()
val sendWhenReady = mutableListOf<EventInterface>()
private val authResponse = mutableMapOf<HexKey, Boolean>()
private val sendWhenReady = mutableListOf<EventInterface>()
fun register(listener: Listener) {
listeners = listeners.plus(listener)
@@ -100,9 +87,7 @@ class Relay(
listeners = listeners.minus(listener)
}
fun isConnected(): Boolean {
return socket != null
}
fun isConnected(): Boolean = socket != null
fun connect() {
connectAndRun {
@@ -116,7 +101,7 @@ class Relay(
private var connectingBlock = AtomicBoolean()
fun connectAndRun(onConnected: (Relay) -> Unit) {
Log.d("Relay", "Relay.connect $url connecting: ${connectingBlock.get()} hasProxy: ${this.httpClient.proxy != null}")
Log.d("Relay", "Relay.connect $url isAlreadyConnecting: ${connectingBlock.get()}")
// BRB is crashing OkHttp Deflater object :(
if (url.contains("brb.io")) return
@@ -136,25 +121,28 @@ class Relay(
lastConnectTentative = TimeUtils.now()
val request =
Request.Builder()
Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url.trim())
.build()
socket = httpClient.newWebSocket(request, RelayListener(onConnected))
socket = HttpClientManager.getHttpClientForUrl(url).newWebSocket(request, RelayListener(onConnected))
} catch (e: Exception) {
if (e is CancellationException) throw e
errorCounter++
RelayStats.newError(url, e.message)
markConnectionAsClosed()
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace()
} finally {
connectingBlock.set(false)
}
}
inner class RelayListener(val onConnected: (Relay) -> Unit) : WebSocketListener() {
inner class RelayListener(
val onConnected: (Relay) -> Unit,
) : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: Response,
@@ -187,7 +175,7 @@ class Relay(
) {
checkNotInMainThread()
eventDownloadCounterInBytes += text.bytesUsedInMemory()
RelayStats.addBytesReceived(url, text.bytesUsedInMemory())
try {
processNewRelayMessage(text)
@@ -239,21 +227,25 @@ class Relay(
) {
checkNotInMainThread()
errorCounter++
socket?.cancel() // 1000, "Normal close"
// checks if this is an actual failure. Closing the socket generates an onFailure as well.
if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) {
RelayStats.newError(url, response?.message ?: t.message)
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response ${t.message} $socket")
t.printStackTrace()
listeners.forEach {
it.onError(
this@Relay,
"",
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t),
)
}
}
// Failures disconnect the relay.
markConnectionAsClosed()
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response")
t.printStackTrace()
listeners.forEach {
it.onError(
this@Relay,
"",
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t),
)
}
}
}
@@ -263,8 +255,9 @@ class Relay(
) {
this.resetEOSEStatuses()
this.isReady = true
this.pingInMs = pingInMs
this.usingCompression = usingCompression
RelayStats.setPing(url, pingInMs)
}
fun markConnectionAsClosed() {
@@ -306,6 +299,8 @@ class Relay(
val message = msgArray.get(1).asText()
Log.w("Relay", "Relay onNotice $url, $message")
RelayStats.newNotice(url, message)
it.onError(this@Relay, message, Error("Relay sent notice: $message"))
}
"OK" ->
@@ -336,7 +331,9 @@ class Relay(
it.onNotify(this@Relay, msgArray[1].asText())
}
"CLOSED" -> listeners.forEach { Log.w("Relay", "Relay onClosed $url, $newMessage") }
else ->
else -> {
RelayStats.newError(url, "Unsupported message: $newMessage")
listeners.forEach {
Log.w("Relay", "Unsupported message: $newMessage")
it.onError(
@@ -345,6 +342,7 @@ class Relay(
Error("Unknown type $type on channel. Msg was $newMessage"),
)
}
}
}
}
@@ -389,11 +387,9 @@ class Relay(
it.filter.toJson(url)
}
// Log.d("Relay", "onFilterSent $url $requestId $request")
writeToSocket(request)
socket?.send(request)
eventUploadCounterInBytes += request.bytesUsedInMemory()
resetEOSEStatuses()
afterEOSEPerSubscription[requestId] = false
}
}
} else {
@@ -457,30 +453,9 @@ class Relay(
checkNotInMainThread()
if (signedEvent is RelayAuthEvent) {
authResponse.put(signedEvent.id, false)
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
sendAuth(signedEvent)
} else {
val event = """["EVENT",${signedEvent.toJson()}]"""
if (isConnected()) {
if (isReady) {
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
}
} else {
// sends all filters after connection is successful.
connectAndRun {
checkNotInMainThread()
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
// Sends everything.
renewFilters()
}
}
sendEvent(signedEvent)
}
}
@@ -488,18 +463,12 @@ class Relay(
checkNotInMainThread()
if (signedEvent is RelayAuthEvent) {
authResponse.put(signedEvent.id, false)
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
sendAuth(signedEvent)
} else {
if (write) {
val event = """["EVENT",${signedEvent.toJson()}]"""
if (isConnected()) {
if (isReady) {
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
} else {
synchronized(sendWhenReady) {
sendWhenReady.add(signedEvent)
@@ -508,10 +477,7 @@ class Relay(
} else {
// sends all filters after connection is successful.
connectAndRun {
checkNotInMainThread()
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
// Sends everything.
renewFilters()
@@ -521,20 +487,49 @@ class Relay(
}
}
fun close(subscriptionId: String) {
checkNotInMainThread()
val msg = """["CLOSE","$subscriptionId"]"""
// Log.d("Relay", "Close Subscription $url $msg")
socket?.send(msg)
private fun sendAuth(signedEvent: RelayAuthEvent) {
authResponse.put(signedEvent.id, false)
writeToSocket("""["AUTH",${signedEvent.toJson()}]""")
}
fun isSameRelayConfig(other: Relay): Boolean {
return url == other.url &&
private fun sendEvent(signedEvent: EventInterface) {
if (isConnected()) {
if (isReady) {
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
}
} else {
// sends all filters after connection is successful.
connectAndRun {
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
// Sends everything.
renewFilters()
}
}
}
private fun writeToSocket(str: String) {
socket?.let {
checkNotInMainThread()
it.send(str)
RelayStats.addBytesSent(url, str.bytesUsedInMemory())
if (BuildConfig.DEBUG) {
Log.d("Relay", "Relay send $url $str")
}
}
}
fun close(subscriptionId: String) {
writeToSocket("""["CLOSE","$subscriptionId"]""")
}
fun isSameRelayConfig(other: RelaySetupInfo): Boolean =
url == other.url &&
write == other.write &&
read == other.read &&
activeTypes == other.activeTypes
}
activeTypes == other.feedTypes
enum class StateType {
// Websocket connected

View File

@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service.relays
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
@@ -91,6 +92,7 @@ object RelayPool : Relay.Listener {
feedTypes: Set<FeedType>?,
onConnected: (Relay) -> Unit,
onDone: (() -> Unit)?,
timeout: Long = 60000,
) {
val relay = Relay(url, true, true, feedTypes ?: emptySet())
addRelay(relay)
@@ -103,7 +105,7 @@ object RelayPool : Relay.Listener {
onConnected(relay)
GlobalScope.launch(Dispatchers.IO) {
delay(60000) // waits for a reply
delay(timeout) // waits for a reply
relay.disconnect()
removeRelay(relay)
@@ -147,7 +149,7 @@ object RelayPool : Relay.Listener {
}
fun sendToSelectedRelays(
list: List<Relay>,
list: List<RelaySetupInfo>,
signedEvent: EventInterface,
) {
list.forEach { relay -> relays.filter { it.url == relay.url }.forEach { it.sendOverride(signedEvent) } }
@@ -197,12 +199,6 @@ object RelayPool : Relay.Listener {
afterEOSE: Boolean,
)
fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
)
fun onRelayStateChange(
type: Relay.StateType,
relay: Relay,
@@ -241,7 +237,6 @@ object RelayPool : Relay.Listener {
subscriptionId: String,
error: Error,
) {
listeners.forEach { it.onError(error, subscriptionId, relay) }
updateStatus()
}

View File

@@ -0,0 +1,138 @@
/**
* 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.relays
import androidx.collection.LruCache
import com.vitorpamplona.quartz.utils.TimeUtils
object RelayStats {
private val innerCache = mutableMapOf<String, RelayStat>()
fun get(url: String): RelayStat {
return innerCache.getOrPut(url) { RelayStat() }
}
fun addBytesReceived(
url: String,
bytesUsedInMemory: Int,
) {
get(url).addBytesReceived(bytesUsedInMemory)
}
fun addBytesSent(
url: String,
bytesUsedInMemory: Int,
) {
get(url).addBytesSent(bytesUsedInMemory)
}
fun newError(
url: String,
error: String?,
) {
get(url).newError(error)
}
fun newNotice(
url: String,
notice: String?,
) {
get(url).newNotice(notice)
}
fun setPing(
url: String,
pingInMs: Long,
) {
get(url).pingInMs = pingInMs
}
fun newSpam(
url: String,
explanation: String,
) {
get(url).newSpam(explanation)
}
}
class RelayStat(
var receivedBytes: Long = 0L,
var sentBytes: Long = 0L,
var spamCounter: Long = 0L,
var errorCounter: Long = 0L,
var pingInMs: Long = 0L,
) {
val messages = LruCache<RelayDebugMessage, RelayDebugMessage>(100)
fun newNotice(notice: String?) {
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.NOTICE,
message = notice ?: "No error message provided",
)
messages.put(debugMessage, debugMessage)
}
fun newError(error: String?) {
errorCounter++
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.ERROR,
message = error ?: "No error message provided",
)
messages.put(debugMessage, debugMessage)
}
fun addBytesReceived(bytesUsedInMemory: Int) {
receivedBytes += bytesUsedInMemory
}
fun addBytesSent(bytesUsedInMemory: Int) {
sentBytes += bytesUsedInMemory
}
fun newSpam(spamDescriptor: String) {
spamCounter++
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.SPAM,
message = spamDescriptor,
)
messages.put(debugMessage, debugMessage)
}
}
class RelayDebugMessage(
val type: RelayDebugMessageType,
val message: String,
val time: Long = TimeUtils.now(),
)
enum class RelayDebugMessageType {
SPAM,
NOTICE,
ERROR,
}

View File

@@ -45,6 +45,10 @@ fun prepareSharedViewModel(act: MainActivity): SharedPreferencesViewModel {
sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures)
}
LaunchedEffect(key1 = displayFeatures, key2 = windowSizeClass) {
sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures)
}
LaunchedEffect(act.isOnMobileDataState) {
sharedPreferencesViewModel.updateConnectivityStatusState(act.isOnMobileDataState)
}

View File

@@ -90,6 +90,11 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
val locales = this.applicationContext.resources.configuration.locales
if (!locales.isEmpty) {
checkLanguage(locales.get(0).language)
}
Log.d("Lifetime Event", "MainActivity.onResume")
// starts muted every time
@@ -263,8 +268,8 @@ class GetMediaActivityResultContract : ActivityResultContracts.GetContent() {
}
}
fun uriToRoute(uri: String?): String? {
return if (uri.equals("nostr:Notifications", true)) {
fun uriToRoute(uri: String?): String? =
if (uri.equals("nostr:Notifications", true)) {
Route.Notification.route.replace("{scrollToTop}", "true")
} else {
if (uri?.startsWith("nostr:Hashtag?id=") == true) {
@@ -317,4 +322,3 @@ fun uriToRoute(uri: String?): String? {
null
}
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui
import android.content.Context
import android.util.LruCache
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
/**
* Cache for stringResource because it seems to be > 1ms function in some phones
*/
private val resourceCache = LruCache<Int, String>(300)
private var resourceCacheLanguage: String? = null
fun checkLanguage(currentLanguage: String) {
if (resourceCacheLanguage == null) {
resourceCacheLanguage = currentLanguage
} else {
if (resourceCacheLanguage != currentLanguage) {
resourceCacheLanguage = currentLanguage
resourceCache.evictAll()
}
}
}
@Composable
fun stringRes(id: Int): String = resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) }
@Composable
fun stringRes(
id: Int,
vararg args: Any,
): String =
String
.format(
LocalConfiguration.current.locales.get(0),
resourceCache.get(id) ?: stringResource(id),
*args,
).also { resourceCache.put(id, it) }
fun stringRes(
ctx: Context,
id: Int,
): String = resourceCache.get(id) ?: ctx.getString(id).also { resourceCache.put(id, it) }
fun stringRes(
ctx: Context,
id: Int,
vararg args: Any?,
): String {
val res = ctx.resources
return String
.format(
res.configuration.locales.get(0),
resourceCache.get(id) ?: res.getString(id),
*args,
).also { resourceCache.put(id, it) }
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun <T> CrossfadeIfEnabled(
targetState: T,
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(),
label: String = "Crossfade",
accountViewModel: AccountViewModel,
content: @Composable (T) -> Unit,
) {
if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) {
Box(modifier) {
content(targetState)
}
} else {
Crossfade(targetState, modifier, animationSpec, label, content)
}
}

View File

@@ -78,7 +78,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextDirection
@@ -98,6 +97,7 @@ import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
@@ -189,7 +189,7 @@ fun EditPostView(
) {
Icon(
painter = painterResource(R.drawable.relays),
contentDescription = stringResource(id = R.string.relay_list_selector),
contentDescription = stringRes(id = R.string.relay_list_selector),
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
@@ -236,8 +236,7 @@ fun EditPostView(
top = pad.calculateTopPadding(),
end = Size10dp,
bottom = pad.calculateBottomPadding(),
)
.fillMaxSize(),
).fillMaxSize(),
) {
Column(
modifier =
@@ -304,11 +303,13 @@ fun EditPostView(
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
VideoView(
myUrlPreview,
mimeType = null,
roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel,
)
} else {
LoadUrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
LoadUrlPreview(myUrlPreview, myUrlPreview, null, accountViewModel)
}
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colorScheme.background
@@ -323,7 +324,7 @@ fun EditPostView(
nav = nav,
)
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, null, accountViewModel)
}
}
}
@@ -363,8 +364,8 @@ fun EditPostView(
lud16,
user.pubkeyHex,
accountViewModel.account,
stringResource(id = R.string.lightning_invoice),
stringResource(id = R.string.lightning_create_and_add_invoice),
stringRes(id = R.string.lightning_invoice),
stringRes(id = R.string.lightning_create_and_add_invoice),
onSuccess = {
postViewModel.message =
TextFieldValue(postViewModel.message.text + "\n\n" + it)
@@ -384,7 +385,7 @@ fun EditPostView(
) {
Column {
Text(
text = stringResource(R.string.message_to_author),
text = stringRes(R.string.message_to_author),
fontSize = 18.sp,
fontWeight = FontWeight.W500,
)
@@ -397,7 +398,7 @@ fun EditPostView(
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.message_to_author_placeholder),
text = stringRes(R.string.message_to_author_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -512,8 +513,7 @@ private fun MessageField(postViewModel: EditPostViewModel) {
width = 1.dp,
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(8.dp),
)
.focusRequester(focusRequester)
).focusRequester(focusRequester)
.onFocusChanged {
if (it.isFocused) {
keyboardController?.show()
@@ -521,7 +521,7 @@ private fun MessageField(postViewModel: EditPostViewModel) {
},
placeholder = {
Text(
text = stringResource(R.string.what_s_on_your_mind),
text = stringRes(R.string.what_s_on_your_mind),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -546,14 +546,14 @@ private fun AddLnInvoiceButton(
if (!isLnInvoiceActive) {
Icon(
imageVector = Icons.Default.CurrencyBitcoin,
contentDescription = stringResource(id = R.string.add_bitcoin_invoice),
contentDescription = stringRes(id = R.string.add_bitcoin_invoice),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
} else {
Icon(
imageVector = Icons.Default.CurrencyBitcoin,
contentDescription = stringResource(id = R.string.cancel_bitcoin_invoice),
contentDescription = stringRes(id = R.string.cancel_bitcoin_invoice),
modifier = Modifier.size(20.dp),
tint = BitcoinOrange,
)

View File

@@ -36,11 +36,11 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.events.FileHeaderEvent
@@ -109,11 +109,11 @@ open class EditPostViewModel() : ViewModel() {
editedFromNote = edit
}
fun sendPost(relayList: List<Relay>? = null) {
fun sendPost(relayList: List<RelaySetupInfo>? = null) {
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) }
}
suspend fun innerSendPost(relayList: List<Relay>? = null) {
suspend fun innerSendPost(relayList: List<RelaySetupInfo>? = null) {
if (accountViewModel == null) {
cancel()
return

View File

@@ -29,6 +29,8 @@ import android.os.Environment
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager
import kotlinx.coroutines.CancellationException
@@ -45,6 +47,33 @@ import java.io.File
import java.util.UUID
object ImageSaver {
fun saveImage(
videoUri: String?,
mimeType: String?,
localContext: Context,
onSuccess: () -> Any?,
onError: (Throwable) -> Any?,
) {
if (videoUri != null) {
if (!videoUri.startsWith("file")) {
saveImage(
context = localContext,
url = videoUri,
onSuccess = onSuccess,
onError = onError,
)
} else {
saveImage(
context = localContext,
localFile = videoUri.toUri().toFile(),
mimeType = mimeType,
onSuccess = onSuccess,
onError = onError,
)
}
}
}
/**
* Saves the image to the gallery. May require a storage permission.
*

View File

@@ -34,8 +34,8 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@@ -64,7 +64,7 @@ fun InformationDialog(
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringResource(R.string.error_dialog_button_ok))
Text(stringRes(R.string.error_dialog_button_ok))
}
}
},

View File

@@ -59,10 +59,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -73,6 +71,7 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
@@ -83,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.note.SearchIcon
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchBarViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
@@ -161,7 +161,7 @@ fun JoinUserOrChannelView(
)
Text(
text = stringResource(R.string.channel_list_join_conversation),
text = stringRes(R.string.channel_list_join_conversation),
fontWeight = FontWeight.Bold,
)
@@ -278,13 +278,13 @@ private fun SearchEditTextForJoin(
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.channel_list_user_or_group_id)) },
label = { Text(text = stringRes(R.string.channel_list_user_or_group_id)) },
value = searchBarViewModel.searchValue,
onValueChange = {
searchBarViewModel.updateSearchValue(it)
scope.launch(Dispatchers.IO) { searchTextChanges.trySend(it) }
},
leadingIcon = { SearchIcon(modifier = Size20Modifier, Color.Unspecified) },
leadingIcon = { SearchIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText) },
modifier =
Modifier
.weight(1f, true)
@@ -297,7 +297,7 @@ private fun SearchEditTextForJoin(
},
placeholder = {
Text(
text = stringResource(R.string.channel_list_user_or_group_id_demo),
text = stringRes(R.string.channel_list_user_or_group_id_demo),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -311,7 +311,7 @@ private fun SearchEditTextForJoin(
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = stringResource(R.string.clear),
contentDescription = stringRes(R.string.clear),
)
}
}
@@ -331,11 +331,6 @@ private fun RenderSearchResults(
val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle()
val channels by searchBarViewModel.searchResultsChannels.collectAsStateWithLifecycle()
val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value
}
Row(
modifier =
Modifier
@@ -367,7 +362,11 @@ private fun RenderSearchResults(
channels,
key = { _, item -> "c" + item.idHex },
) { _, item ->
RenderChannel(item, automaticallyShowProfilePicture) {
RenderChannel(
item,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
) {
nav("Channel/${item.idHex}")
searchBarViewModel.clear()
}
@@ -385,6 +384,7 @@ private fun RenderSearchResults(
private fun RenderChannel(
item: com.vitorpamplona.amethyst.model.Channel,
loadProfilePicture: Boolean,
loadRobohash: Boolean,
onClick: () -> Unit,
) {
val hasNewMessages = remember { mutableStateOf(false) }
@@ -403,6 +403,7 @@ private fun RenderChannel(
hasNewMessages,
onClick = onClick,
loadProfilePicture = loadProfilePicture,
loadRobohash = loadRobohash,
)
}
@@ -414,14 +415,15 @@ fun UserComposeForChat(
) {
Row(
modifier =
Modifier.clickable(
onClick = onClick,
).padding(
start = 12.dp,
end = 12.dp,
top = 10.dp,
bottom = 10.dp,
),
Modifier
.clickable(
onClick = onClick,
).padding(
start = 12.dp,
end = 12.dp,
top = 10.dp,
bottom = 10.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
ClickableUserPicture(baseUser, Size55dp, accountViewModel)
@@ -432,7 +434,7 @@ fun UserComposeForChat(
.padding(start = 10.dp)
.weight(1f),
) {
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser) }
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser, accountViewModel = accountViewModel) }
DisplayUserAboutInfo(baseUser)
}

View File

@@ -38,7 +38,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
@@ -48,6 +47,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
@@ -87,20 +87,21 @@ fun NewChannelView(
postViewModel.create()
onClose()
},
postViewModel.channelName.value.text.isNotBlank(),
postViewModel.channelName.value.text
.isNotBlank(),
)
}
Spacer(modifier = Modifier.height(15.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.channel_name)) },
label = { Text(text = stringRes(R.string.channel_name)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.channelName.value,
onValueChange = { postViewModel.channelName.value = it },
placeholder = {
Text(
text = stringResource(R.string.my_awesome_group),
text = stringRes(R.string.my_awesome_group),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -114,7 +115,7 @@ fun NewChannelView(
Spacer(modifier = Modifier.height(15.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.picture_url)) },
label = { Text(text = stringRes(R.string.picture_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.channelPicture.value,
onValueChange = { postViewModel.channelPicture.value = it },
@@ -129,13 +130,13 @@ fun NewChannelView(
Spacer(modifier = Modifier.height(15.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.description)) },
label = { Text(text = stringRes(R.string.description)) },
modifier = Modifier.fillMaxWidth().height(100.dp),
value = postViewModel.channelDescription.value,
onValueChange = { postViewModel.channelDescription.value = it },
placeholder = {
Text(
text = stringResource(R.string.about_us),
text = stringRes(R.string.about_us),
color = MaterialTheme.colorScheme.placeholderText,
)
},

View File

@@ -31,11 +31,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -80,7 +81,7 @@ open class NewMediaModel : ViewModel() {
fun upload(
context: Context,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
) {
isUploadingImage = true
@@ -114,7 +115,7 @@ open class NewMediaModel : ViewModel() {
}
?: run {
viewModelScope.launch {
onError(context.getString(R.string.could_not_open_the_compressed_file))
onError(stringRes(context, R.string.could_not_open_the_compressed_file))
isUploadingImage = false
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
@@ -153,7 +154,7 @@ open class NewMediaModel : ViewModel() {
isUploadingImage = false
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
onError(context.getString(R.string.failed_to_upload_media, e.message))
onError(stringRes(context, R.string.failed_to_upload_media, e.message))
}
}
}
@@ -162,7 +163,7 @@ open class NewMediaModel : ViewModel() {
isUploadingImage = false
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
onError(context.getString(R.string.error_when_compressing_media, it))
onError(stringRes(context, R.string.error_when_compressing_media, it))
},
)
}
@@ -179,16 +180,14 @@ open class NewMediaModel : ViewModel() {
selectedServer = ServerOption(defaultServer(), false)
}
fun canPost(): Boolean {
return !isUploadingImage && galleryUri != null && selectedServer != null
}
fun canPost(): Boolean = !isUploadingImage && galleryUri != null && selectedServer != null
suspend fun createNIP94Record(
uploadingResult: Nip96Uploader.PartialEvent,
localContentType: String?,
alt: String,
sensitiveContent: Boolean,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
context: Context,
) {
uploadingPercentage.value = 0.40f
@@ -197,11 +196,20 @@ open class NewMediaModel : ViewModel() {
val imageUrl = uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1)
val remoteMimeType =
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null }
uploadingResult.tags
?.firstOrNull { it.size > 1 && it[0] == "m" }
?.get(1)
?.ifBlank { null }
val originalHash =
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "ox" }?.get(1)?.ifBlank { null }
uploadingResult.tags
?.firstOrNull { it.size > 1 && it[0] == "ox" }
?.get(1)
?.ifBlank { null }
val dim =
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null }
uploadingResult.tags
?.firstOrNull { it.size > 1 && it[0] == "dim" }
?.get(1)
?.ifBlank { null }
val magnet =
uploadingResult.tags
?.firstOrNull { it.size > 1 && it[0] == "magnet" }
@@ -214,7 +222,7 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
isUploadingImage = false
onError(context.getString(R.string.server_did_not_provide_a_url_after_uploading))
onError(stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
return
}
@@ -254,7 +262,7 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
isUploadingImage = false
onError(context.getString(R.string.could_not_prepare_local_file_to_upload, it))
onError(stringRes(context, R.string.could_not_prepare_local_file_to_upload, it))
},
)
} else {
@@ -263,7 +271,7 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
isUploadingImage = false
onError(context.getString(R.string.could_not_download_from_the_server))
onError(stringRes(context, R.string.could_not_download_from_the_server))
}
}
@@ -272,7 +280,7 @@ open class NewMediaModel : ViewModel() {
mimeType: String?,
alt: String,
sensitiveContent: Boolean,
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
context: Context,
) {
if (bytes.size > 80000) {
@@ -312,7 +320,7 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.00f
isUploadingImage = false
cancel()
onError(context.getString(R.string.could_not_prepare_local_file_to_upload, it))
onError(stringRes(context, R.string.could_not_prepare_local_file_to_upload, it))
},
)
}

View File

@@ -58,7 +58,6 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -70,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException
@@ -93,7 +93,7 @@ fun NewMediaView(
LaunchedEffect(uri) {
val mediaType = resolver.getType(uri) ?: ""
postViewModel.load(account, uri, mediaType) {
accountViewModel.toast(context.getString(R.string.failed_to_upload_media_no_details), it)
accountViewModel.toast(stringRes(context, R.string.failed_to_upload_media_no_details), it)
}
}
@@ -124,7 +124,8 @@ fun NewMediaView(
Column(
modifier =
Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
Modifier
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
.fillMaxWidth()
.fillMaxHeight()
.imePadding(),
@@ -194,7 +195,7 @@ fun ImageVideoPost(
ServerOption(
Nip96MediaServers.ServerName(
"NIP95",
stringResource(id = R.string.upload_server_relays_nip95),
stringRes(id = R.string.upload_server_relays_nip95),
),
true,
),
@@ -209,7 +210,8 @@ fun ImageVideoPost(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.fillMaxWidth()
Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
) {
@@ -219,7 +221,8 @@ fun ImageVideoPost(
contentDescription = postViewModel.galleryUri.toString(),
contentScale = ContentScale.FillWidth,
modifier =
Modifier.padding(top = 4.dp)
Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
)
@@ -251,7 +254,9 @@ fun ImageVideoPost(
postViewModel.galleryUri?.let {
VideoView(
videoUri = it.toString(),
mimeType = postViewModel.mediaType,
roundedCorner = false,
isFiniteHeight = false,
accountViewModel = accountViewModel,
)
}
@@ -263,7 +268,7 @@ fun ImageVideoPost(
modifier = Modifier.fillMaxWidth(),
) {
TextSpinner(
label = stringResource(id = R.string.file_server),
label = stringRes(id = R.string.file_server),
placeholder =
fileServers
.firstOrNull { it.server == accountViewModel.account.defaultFileServer }
@@ -294,13 +299,13 @@ fun ImageVideoPost(
modifier = Modifier.fillMaxWidth().windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.content_description)) },
label = { Text(text = stringRes(R.string.content_description)) },
modifier = Modifier.fillMaxWidth().windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
value = postViewModel.alt,
onValueChange = { postViewModel.alt = it },
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
text = stringRes(R.string.content_description_example),
color = MaterialTheme.colorScheme.placeholderText,
)
},

View File

@@ -36,12 +36,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.CancellationException
@@ -87,13 +87,13 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
colors = if (pollViewModel.isValidClosedAt.value) colorValid else colorInValid,
label = {
Text(
text = stringResource(R.string.poll_closing_time),
text = stringRes(R.string.poll_closing_time),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_closing_time_days),
text = stringRes(R.string.poll_closing_time_days),
color = MaterialTheme.colorScheme.placeholderText,
)
},

View File

@@ -36,12 +36,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.CancellationException
@@ -87,13 +87,13 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
colors = if (pollViewModel.isValidConsensusThreshold.value) colorValid else colorInValid,
label = {
Text(
text = stringResource(R.string.poll_consensus_threshold),
text = stringRes(R.string.poll_consensus_threshold),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_consensus_threshold_percent),
text = stringRes(R.string.poll_consensus_threshold_percent),
color = MaterialTheme.colorScheme.placeholderText,
)
},

View File

@@ -31,10 +31,10 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
@@ -51,7 +51,7 @@ fun NewPollOption(
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.clear),
contentDescription = stringRes(R.string.clear),
)
}
}
@@ -64,13 +64,13 @@ fun NewPollOption(
},
label = {
Text(
text = stringResource(R.string.poll_option_index).format(optionIndex + 1),
text = stringRes(R.string.poll_option_index).format(optionIndex + 1),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_option_description),
text = stringRes(R.string.poll_option_description),
color = MaterialTheme.colorScheme.placeholderText,
)
},

View File

@@ -37,13 +37,13 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@OptIn(ExperimentalComposeUiApi::class)
@@ -74,7 +74,7 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
onValueChange = { pollViewModel.updateMessage(it) },
label = {
Text(
text = stringResource(R.string.poll_primary_description),
text = stringRes(R.string.poll_primary_description),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -90,7 +90,7 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
},
placeholder = {
Text(
text = stringResource(R.string.poll_primary_description),
text = stringRes(R.string.poll_primary_description),
color = MaterialTheme.colorScheme.placeholderText,
)
},

View File

@@ -26,10 +26,10 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
@@ -51,13 +51,13 @@ fun NewPollRecipientsField(
enabled = false,
label = {
Text(
text = stringResource(R.string.poll_zap_recipients),
text = stringRes(R.string.poll_zap_recipients),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_zap_recipients),
text = stringRes(R.string.poll_zap_recipients),
color = MaterialTheme.colorScheme.placeholderText,
)
},

View File

@@ -34,11 +34,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -67,13 +67,13 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid,
label = {
Text(
text = stringResource(R.string.poll_zap_value_min),
text = stringRes(R.string.poll_zap_value_min),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringResource(R.string.sats),
text = stringRes(R.string.sats),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -89,13 +89,13 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid,
label = {
Text(
text = stringResource(R.string.poll_zap_value_max),
text = stringRes(R.string.poll_zap_value_max),
color = MaterialTheme.colorScheme.placeholderText,
)
},
placeholder = {
Text(
text = stringResource(R.string.sats),
text = stringRes(R.string.sats),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -107,7 +107,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
horizontalArrangement = Arrangement.Center,
) {
Text(
text = stringResource(R.string.poll_zap_value_min_max_explainer),
text = stringRes(R.string.poll_zap_value_min_max_explainer),
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp),
)

View File

@@ -105,7 +105,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -150,6 +149,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -276,7 +276,7 @@ fun NewPostView(
) {
Icon(
painter = painterResource(R.drawable.relays),
contentDescription = stringResource(id = R.string.relay_list_selector),
contentDescription = stringRes(id = R.string.relay_list_selector),
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
@@ -324,8 +324,7 @@ fun NewPostView(
top = pad.calculateTopPadding(),
end = Size10dp,
bottom = pad.calculateBottomPadding(),
)
.fillMaxSize(),
).fillMaxSize(),
) {
Column(
modifier =
@@ -425,11 +424,13 @@ fun NewPostView(
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
VideoView(
myUrlPreview,
mimeType = null,
roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel,
)
} else {
LoadUrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
LoadUrlPreview(myUrlPreview, myUrlPreview, null, accountViewModel)
}
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colorScheme.background
@@ -444,7 +445,7 @@ fun NewPostView(
nav = nav,
)
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, null, accountViewModel)
}
}
}
@@ -509,8 +510,8 @@ fun NewPostView(
lud16,
accountViewModel.account.userProfile().pubkeyHex,
accountViewModel.account,
stringResource(id = R.string.lightning_invoice),
stringResource(id = R.string.lightning_create_and_add_invoice),
stringRes(id = R.string.lightning_invoice),
stringRes(id = R.string.lightning_create_and_add_invoice),
onSuccess = {
postViewModel.insertAtCursor(it)
postViewModel.wantsInvoice = false
@@ -529,7 +530,7 @@ fun NewPostView(
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
) {
ZapRaiserRequest(
stringResource(id = R.string.zapraiser),
stringRes(id = R.string.zapraiser),
postViewModel,
)
}
@@ -573,7 +574,7 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
if (postViewModel.canUsePoll) {
// These should be hashtag recommendations the user selects in the future.
// val hashtag = stringResource(R.string.poll_hashtag)
// val hashtag = stringRes(R.string.poll_hashtag)
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
AddPollButton(postViewModel.wantsPoll) {
postViewModel.wantsPoll = !postViewModel.wantsPoll
@@ -674,8 +675,7 @@ private fun MessageField(postViewModel: NewPostViewModel) {
width = 1.dp,
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(8.dp),
)
.focusRequester(focusRequester)
).focusRequester(focusRequester)
.onFocusChanged {
if (it.isFocused) {
keyboardController?.show()
@@ -685,9 +685,9 @@ private fun MessageField(postViewModel: NewPostViewModel) {
Text(
text =
if (postViewModel.wantsProduct) {
stringResource(R.string.description)
stringRes(R.string.description)
} else {
stringResource(R.string.what_s_on_your_mind)
stringRes(R.string.what_s_on_your_mind)
},
color = MaterialTheme.colorScheme.placeholderText,
)
@@ -721,7 +721,7 @@ fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) {
) {
Icon(
imageVector = Icons.Default.VisibilityOff,
contentDescription = stringResource(id = R.string.content_warning),
contentDescription = stringRes(id = R.string.content_warning),
modifier =
Modifier
.size(18.dp)
@@ -730,7 +730,7 @@ fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) {
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringResource(id = R.string.content_warning),
contentDescription = stringRes(id = R.string.content_warning),
modifier =
Modifier
.size(10.dp)
@@ -740,7 +740,7 @@ fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) {
}
Text(
text = stringResource(R.string.add_sensitive_content_label),
text = stringRes(R.string.add_sensitive_content_label),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp),
@@ -750,7 +750,7 @@ fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) {
HorizontalDivider(thickness = DividerThickness)
Text(
text = stringResource(R.string.add_sensitive_content_explainer),
text = stringRes(R.string.add_sensitive_content_explainer),
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp),
)
@@ -767,7 +767,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.messages_new_message_to),
text = stringRes(R.string.messages_new_message_to),
fontSize = Font14SP,
fontWeight = FontWeight.W500,
)
@@ -778,7 +778,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.messages_new_message_to_caption),
text = stringRes(R.string.messages_new_message_to_caption),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -801,7 +801,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.messages_new_message_subject),
text = stringRes(R.string.messages_new_message_subject),
fontSize = Font14SP,
fontWeight = FontWeight.W500,
)
@@ -812,7 +812,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.messages_new_message_subject_caption),
text = stringRes(R.string.messages_new_message_subject_caption),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -842,7 +842,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.classifieds_title),
text = stringRes(R.string.classifieds_title),
fontSize = Font14SP,
fontWeight = FontWeight.W500,
)
@@ -855,7 +855,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.classifieds_title_placeholder),
text = stringRes(R.string.classifieds_title_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -878,7 +878,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.classifieds_price),
text = stringRes(R.string.classifieds_price),
fontSize = Font14SP,
fontWeight = FontWeight.W500,
)
@@ -915,7 +915,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.classifieds_condition),
text = stringRes(R.string.classifieds_condition),
fontSize = Font14SP,
fontWeight = FontWeight.W500,
)
@@ -924,23 +924,23 @@ fun SellProduct(postViewModel: NewPostViewModel) {
listOf(
Triple(
ClassifiedsEvent.CONDITION.NEW,
stringResource(id = R.string.classifieds_condition_new),
stringResource(id = R.string.classifieds_condition_new_explainer),
stringRes(id = R.string.classifieds_condition_new),
stringRes(id = R.string.classifieds_condition_new_explainer),
),
Triple(
ClassifiedsEvent.CONDITION.USED_LIKE_NEW,
stringResource(id = R.string.classifieds_condition_like_new),
stringResource(id = R.string.classifieds_condition_like_new_explainer),
stringRes(id = R.string.classifieds_condition_like_new),
stringRes(id = R.string.classifieds_condition_like_new_explainer),
),
Triple(
ClassifiedsEvent.CONDITION.USED_GOOD,
stringResource(id = R.string.classifieds_condition_good),
stringResource(id = R.string.classifieds_condition_good_explainer),
stringRes(id = R.string.classifieds_condition_good),
stringRes(id = R.string.classifieds_condition_good_explainer),
),
Triple(
ClassifiedsEvent.CONDITION.USED_FAIR,
stringResource(id = R.string.classifieds_condition_fair),
stringResource(id = R.string.classifieds_condition_fair_explainer),
stringRes(id = R.string.classifieds_condition_fair),
stringRes(id = R.string.classifieds_condition_fair_explainer),
),
)
@@ -982,7 +982,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.classifieds_category),
text = stringRes(R.string.classifieds_category),
fontSize = Font14SP,
fontWeight = FontWeight.W500,
)
@@ -1007,7 +1007,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
R.string.classifieds_category_other,
)
val categoryTypes = categoryList.map { Triple(it, stringResource(id = it), null) }
val categoryTypes = categoryList.map { Triple(it, stringRes(id = it), null) }
val categoryOptions =
remember {
@@ -1048,7 +1048,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.classifieds_location),
text = stringRes(R.string.classifieds_location),
fontSize = Font14SP,
fontWeight = FontWeight.W500,
)
@@ -1061,7 +1061,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.classifieds_location_placeholder),
text = stringRes(R.string.classifieds_location_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -1099,21 +1099,21 @@ fun FowardZapTo(
ZapSplitIcon()
Text(
text = stringResource(R.string.zap_split_title),
text = stringRes(R.string.zap_split_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(horizontal = 10.dp).weight(1f),
)
OutlinedButton(onClick = { postViewModel.updateZapFromText() }) {
Text(text = stringResource(R.string.load_from_text))
Text(text = stringRes(R.string.load_from_text))
}
}
HorizontalDivider(thickness = DividerThickness)
Text(
text = stringResource(R.string.zap_split_explainer),
text = stringRes(R.string.zap_split_explainer),
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp),
)
@@ -1128,7 +1128,7 @@ fun FowardZapTo(
Spacer(modifier = DoubleHorzSpacer)
Column(modifier = Modifier.weight(1f)) {
UsernameDisplay(splitItem.key)
UsernameDisplay(splitItem.key, accountViewModel = accountViewModel)
Text(
text = String.format("%.0f%%", splitItem.percentage * 100),
maxLines = 1,
@@ -1154,11 +1154,11 @@ fun FowardZapTo(
OutlinedTextField(
value = postViewModel.forwardZapToEditting,
onValueChange = { postViewModel.updateZapForwardTo(it) },
label = { Text(text = stringResource(R.string.zap_split_search_and_add_user)) },
label = { Text(text = stringRes(R.string.zap_split_search_and_add_user)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.zap_split_search_and_add_user_placeholder),
text = stringRes(R.string.zap_split_search_and_add_user_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -1205,7 +1205,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
}
Text(
text = stringResource(R.string.geohash_title),
text = stringRes(R.string.geohash_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp),
@@ -1217,7 +1217,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
HorizontalDivider(thickness = DividerThickness)
Text(
text = stringResource(R.string.geohash_explainer),
text = stringRes(R.string.geohash_explainer),
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp),
)
@@ -1275,7 +1275,7 @@ fun Notifying(
FlowRow(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
if (!mentions.isNullOrEmpty()) {
Text(
stringResource(R.string.reply_notify),
stringRes(R.string.reply_notify),
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.align(CenterVertically),
@@ -1348,7 +1348,7 @@ private fun AddZapraiserButton(
)
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.add_zapraiser),
contentDescription = stringRes(R.string.add_zapraiser),
modifier =
Modifier
.size(13.dp)
@@ -1367,7 +1367,7 @@ private fun AddZapraiserButton(
)
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.cancel_zapraiser),
contentDescription = stringRes(R.string.cancel_zapraiser),
modifier =
Modifier
.size(13.dp)
@@ -1390,14 +1390,14 @@ fun AddGeoHash(
if (!postViewModel.wantsToAddGeoHash) {
Icon(
imageVector = Icons.Default.LocationOff,
contentDescription = stringResource(id = R.string.add_location),
contentDescription = stringRes(id = R.string.add_location),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
} else {
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = stringResource(id = R.string.remove_location),
contentDescription = stringRes(id = R.string.remove_location),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary,
)
@@ -1416,14 +1416,14 @@ private fun AddLnInvoiceButton(
if (!isLnInvoiceActive) {
Icon(
imageVector = Icons.Default.CurrencyBitcoin,
contentDescription = stringResource(id = R.string.add_bitcoin_invoice),
contentDescription = stringRes(id = R.string.add_bitcoin_invoice),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
} else {
Icon(
imageVector = Icons.Default.CurrencyBitcoin,
contentDescription = stringResource(id = R.string.cancel_bitcoin_invoice),
contentDescription = stringRes(id = R.string.cancel_bitcoin_invoice),
modifier = Modifier.size(20.dp),
tint = BitcoinOrange,
)
@@ -1458,14 +1458,14 @@ private fun AddClassifiedsButton(
if (!postViewModel.wantsProduct) {
Icon(
imageVector = Icons.Default.Sell,
contentDescription = stringResource(R.string.classifieds),
contentDescription = stringRes(R.string.classifieds),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
} else {
Icon(
imageVector = Icons.Default.Sell,
contentDescription = stringResource(id = R.string.cancel_classifieds),
contentDescription = stringRes(id = R.string.cancel_classifieds),
modifier = Modifier.size(20.dp),
tint = BitcoinOrange,
)
@@ -1489,7 +1489,7 @@ private fun MarkAsSensitive(
if (!postViewModel.wantsToMarkAsSensitive) {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = stringResource(R.string.add_content_warning),
contentDescription = stringRes(R.string.add_content_warning),
modifier =
Modifier
.size(18.dp)
@@ -1498,7 +1498,7 @@ private fun MarkAsSensitive(
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringResource(R.string.add_content_warning),
contentDescription = stringRes(R.string.add_content_warning),
modifier =
Modifier
.size(10.dp)
@@ -1508,7 +1508,7 @@ private fun MarkAsSensitive(
} else {
Icon(
imageVector = Icons.Default.VisibilityOff,
contentDescription = stringResource(id = R.string.remove_content_warning),
contentDescription = stringRes(id = R.string.remove_content_warning),
modifier =
Modifier
.size(18.dp)
@@ -1517,7 +1517,7 @@ private fun MarkAsSensitive(
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringResource(id = R.string.remove_content_warning),
contentDescription = stringRes(id = R.string.remove_content_warning),
modifier =
Modifier
.size(10.dp)
@@ -1550,7 +1550,7 @@ fun PostButton(
enabled = isActive,
onClick = onPost,
) {
Text(text = stringResource(R.string.post))
Text(text = stringRes(R.string.post))
}
}
@@ -1565,7 +1565,7 @@ fun SaveButton(
modifier = modifier,
onClick = onPost,
) {
Text(text = stringResource(R.string.save))
Text(text = stringRes(R.string.save))
}
}
@@ -1588,7 +1588,7 @@ fun CreateButton(
containerColor = if (isActive) MaterialTheme.colorScheme.primary else Color.Gray,
),
) {
Text(text = stringResource(R.string.create), color = Color.White)
Text(text = stringRes(R.string.create), color = Color.White)
}
}
@@ -1613,7 +1613,7 @@ fun ImageVideoDescription(
ServerOption(
Nip96MediaServers.ServerName(
"NIP95",
stringResource(id = R.string.upload_server_relays_nip95),
stringRes(id = R.string.upload_server_relays_nip95),
),
true,
),
@@ -1655,7 +1655,7 @@ fun ImageVideoDescription(
) {
Text(
text =
stringResource(
stringRes(
if (isImage) {
R.string.content_description_add_image
} else {
@@ -1736,7 +1736,7 @@ fun ImageVideoDescription(
)
}
} else {
VideoView(uri.toString(), roundedCorner = true, accountViewModel = accountViewModel)
VideoView(uri.toString(), roundedCorner = true, isFiniteHeight = false, mimeType = mediaType, accountViewModel = accountViewModel)
}
}
@@ -1745,7 +1745,7 @@ fun ImageVideoDescription(
modifier = Modifier.fillMaxWidth(),
) {
TextSpinner(
label = stringResource(id = R.string.file_server),
label = stringRes(id = R.string.file_server),
placeholder =
fileServers
.firstOrNull { it.server == accountViewModel.account.defaultFileServer }
@@ -1785,7 +1785,7 @@ fun ImageVideoDescription(
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.content_description)) },
label = { Text(text = stringRes(R.string.content_description)) },
modifier =
Modifier
.fillMaxWidth()
@@ -1794,7 +1794,7 @@ fun ImageVideoDescription(
onValueChange = { message = it },
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
text = stringRes(R.string.content_description_example),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -1817,7 +1817,7 @@ fun ImageVideoDescription(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(text = stringResource(R.string.add_content), color = Color.White, fontSize = 20.sp)
Text(text = stringRes(R.string.add_content), color = Color.White, fontSize = 20.sp)
}
}
}
@@ -1851,12 +1851,12 @@ fun SettingSwitchItem(
verticalArrangement = Arrangement.spacedBy(Size5dp),
) {
Text(
text = stringResource(id = title),
text = stringRes(id = title),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = stringResource(id = description),
text = stringRes(id = description),
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 2,

View File

@@ -40,12 +40,12 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.LocationUtil
import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.amethyst.ui.components.Split
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -79,6 +79,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID
import kotlin.math.round
enum class UserSuggestionAnchor {
MAIN_MESSAGE,
@@ -438,7 +439,7 @@ open class NewPostViewModel() : ViewModel() {
urlPreview = findUrlInMessage()
}
fun sendPost(relayList: List<Relay>? = null) {
fun sendPost(relayList: List<RelaySetupInfo>? = null) {
viewModelScope.launch(Dispatchers.IO) {
innerSendPost(relayList, null)
accountViewModel?.deleteDraft(draftTag)
@@ -446,18 +447,18 @@ open class NewPostViewModel() : ViewModel() {
}
}
fun sendDraft(relayList: List<Relay>? = null) {
fun sendDraft(relayList: List<RelaySetupInfo>? = null) {
viewModelScope.launch {
sendDraftSync(relayList)
}
}
suspend fun sendDraftSync(relayList: List<Relay>? = null) {
suspend fun sendDraftSync(relayList: List<RelaySetupInfo>? = null) {
innerSendPost(relayList, draftTag)
}
private suspend fun innerSendPost(
relayList: List<Relay>? = null,
relayList: List<RelaySetupInfo>? = null,
localDraft: String?,
) = withContext(Dispatchers.IO) {
if (accountViewModel == null) {
@@ -474,13 +475,21 @@ open class NewPostViewModel() : ViewModel() {
val zapReceiver =
if (wantsForwardZapTo) {
forwardZapTo.items.map {
ZapSplitSetup(
lnAddressOrPubKeyHex = it.key.pubkeyHex,
relay = it.key.relaysBeingUsed.keys.firstOrNull(),
weight = it.percentage.toDouble(),
isLnAddress = false,
)
forwardZapTo.items.mapNotNull { split ->
if (split.percentage > 0.00001) {
val homeRelay =
accountViewModel?.getRelayListFor(split.key)?.writeRelays()?.firstOrNull()
?: split.key.relaysBeingUsed.keys.firstOrNull { !it.contains("localhost") }
ZapSplitSetup(
lnAddressOrPubKeyHex = split.key.pubkeyHex,
relay = homeRelay,
weight = round(split.percentage.toDouble() * 10000) / 10000,
isLnAddress = false,
)
} else {
null
}
}
} else {
null

View File

@@ -40,7 +40,6 @@ import androidx.compose.runtime.LaunchedEffect
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.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -48,6 +47,7 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -108,13 +108,13 @@ fun NewUserMetadataView(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()),
) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.display_name)) },
label = { Text(text = stringRes(R.string.display_name)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.displayName.value,
onValueChange = { postViewModel.displayName.value = it },
placeholder = {
Text(
text = stringResource(R.string.my_display_name),
text = stringRes(R.string.my_display_name),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -128,13 +128,13 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.about_me)) },
label = { Text(text = stringRes(R.string.about_me)) },
modifier = Modifier.fillMaxWidth().height(100.dp),
value = postViewModel.about.value,
onValueChange = { postViewModel.about.value = it },
placeholder = {
Text(
text = stringResource(id = R.string.about_me),
text = stringRes(id = R.string.about_me),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -148,7 +148,7 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.avatar_url)) },
label = { Text(text = stringRes(R.string.avatar_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.picture.value,
onValueChange = { postViewModel.picture.value = it },
@@ -173,7 +173,7 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.banner_url)) },
label = { Text(text = stringRes(R.string.banner_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.banner.value,
onValueChange = { postViewModel.banner.value = it },
@@ -198,7 +198,7 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.website_url)) },
label = { Text(text = stringRes(R.string.website_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.website.value,
onValueChange = { postViewModel.website.value = it },
@@ -214,7 +214,7 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.nip_05)) },
label = { Text(text = stringRes(R.string.nip_05)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.nip05.value,
onValueChange = { postViewModel.nip05.value = it },
@@ -229,7 +229,7 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.ln_address)) },
label = { Text(text = stringRes(R.string.ln_address)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnAddress.value,
onValueChange = { postViewModel.lnAddress.value = it },
@@ -245,13 +245,13 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.ln_url_outdated)) },
label = { Text(text = stringRes(R.string.ln_url_outdated)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnURL.value,
onValueChange = { postViewModel.lnURL.value = it },
placeholder = {
Text(
text = stringResource(R.string.lnurl),
text = stringRes(R.string.lnurl),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -260,13 +260,13 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.twitter)) },
label = { Text(text = stringRes(R.string.twitter)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.twitter.value,
onValueChange = { postViewModel.twitter.value = it },
placeholder = {
Text(
text = stringResource(R.string.twitter_proof_url_template),
text = stringRes(R.string.twitter_proof_url_template),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -275,13 +275,13 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.mastodon)) },
label = { Text(text = stringRes(R.string.mastodon)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.mastodon.value,
onValueChange = { postViewModel.mastodon.value = it },
placeholder = {
Text(
text = stringResource(R.string.mastodon_proof_url_template),
text = stringRes(R.string.mastodon_proof_url_template),
color = MaterialTheme.colorScheme.placeholderText,
)
},
@@ -290,13 +290,13 @@ fun NewUserMetadataView(
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.github)) },
label = { Text(text = stringRes(R.string.github)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.github.value,
onValueChange = { postViewModel.github.value = it },
placeholder = {
Text(
text = stringResource(R.string.github_proof_url_template),
text = stringRes(R.string.github_proof_url_template),
color = MaterialTheme.colorScheme.placeholderText,
)
},

View File

@@ -38,10 +38,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.events.EmptyTagList
@@ -63,15 +63,16 @@ fun NotifyRequestDialog(
val background = remember { mutableStateOf(defaultBackground) }
TranslatableRichTextViewer(
textContent,
content = textContent,
canPreview = true,
quotesLeft = 1,
Modifier.fillMaxWidth(),
EmptyTagList,
background,
textContent,
accountViewModel,
nav,
modifier = Modifier.fillMaxWidth(),
tags = EmptyTagList,
backgroundColor = background,
id = textContent,
callbackUri = null,
accountViewModel = accountViewModel,
nav = nav,
)
},
confirmButton = {
@@ -88,7 +89,7 @@ fun NotifyRequestDialog(
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringResource(R.string.error_dialog_button_ok))
Text(stringRes(R.string.error_dialog_button_ok))
}
}
},

View File

@@ -47,17 +47,18 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
data class RelayList(
val relay: Relay,
val relay: RelaySetupInfo,
val relayInfo: RelayBriefInfoCache.RelayBriefInfo,
val isSelected: Boolean,
)
@@ -69,9 +70,9 @@ data class RelayInfoDialog(
@Composable
fun RelaySelectionDialog(
preSelectedList: ImmutableList<Relay>,
preSelectedList: ImmutableList<RelaySetupInfo>,
onClose: () -> Unit,
onPost: (list: ImmutableList<Relay>) -> Unit,
onPost: (list: ImmutableList<RelaySetupInfo>) -> Unit,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -141,7 +142,7 @@ fun RelaySelectionDialog(
}
RelaySwitch(
text = context.getString(R.string.select_deselect_all),
text = stringRes(context, R.string.select_deselect_all),
checked = selected,
onClick = {
selected = !selected
@@ -185,25 +186,29 @@ fun RelaySelectionDialog(
val msg =
when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
@@ -211,7 +216,7 @@ fun RelaySelectionDialog(
}
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
stringRes(context, R.string.unable_to_download_relay_document),
msg,
)
},

View File

@@ -28,11 +28,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import kotlinx.coroutines.launch
import java.io.File
@@ -54,22 +54,22 @@ fun SaveToGallery(url: String) {
url = url,
onSuccess = {
scope.launch {
Toast.makeText(
localContext,
localContext.getString(R.string.image_saved_to_the_gallery),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
localContext,
stringRes(localContext, R.string.image_saved_to_the_gallery),
Toast.LENGTH_SHORT,
).show()
}
},
onError = {
scope.launch {
Toast.makeText(
localContext,
localContext.getString(R.string.failed_to_save_the_image),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
localContext,
stringRes(localContext, R.string.failed_to_save_the_image),
Toast.LENGTH_SHORT,
).show()
}
},
)
@@ -96,7 +96,7 @@ fun SaveToGallery(url: String) {
}
},
) {
Text(text = stringResource(id = R.string.save))
Text(text = stringRes(id = R.string.save))
}
}
@@ -116,22 +116,22 @@ fun SaveToGallery(
mimeType = mimeType,
onSuccess = {
scope.launch {
Toast.makeText(
localContext,
localContext.getString(R.string.image_saved_to_the_gallery),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
localContext,
stringRes(localContext, R.string.image_saved_to_the_gallery),
Toast.LENGTH_SHORT,
).show()
}
},
onError = {
scope.launch {
Toast.makeText(
localContext,
localContext.getString(R.string.failed_to_save_the_image),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
localContext,
stringRes(localContext, R.string.failed_to_save_the_image),
Toast.LENGTH_SHORT,
).show()
}
},
)
@@ -159,6 +159,6 @@ fun SaveToGallery(
},
shape = ButtonBorder,
) {
Text(text = stringResource(id = R.string.save))
Text(text = stringRes(id = R.string.save))
}
}

View File

@@ -50,7 +50,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -58,6 +57,7 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.GetMediaActivityResultContract
import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import java.util.concurrent.atomic.AtomicBoolean
@@ -114,7 +114,7 @@ private fun UploadBoxButton(
if (!isUploading) {
Icon(
imageVector = Icons.Default.AddPhotoAlternate,
contentDescription = stringResource(id = R.string.upload_image),
contentDescription = stringRes(id = R.string.upload_image),
modifier = Modifier.height(25.dp),
tint = tint,
)
@@ -137,8 +137,7 @@ val DefaultAnimationColors =
Color(0xFFFCAF45),
Color(0xFFFFDC80),
Color(0xFF5851D8),
)
.toImmutableList()
).toImmutableList()
@Composable
fun LoadingAnimation(
@@ -164,7 +163,8 @@ fun LoadingAnimation(
CircularProgressIndicator(
modifier =
Modifier.size(size = indicatorSize)
Modifier
.size(size = indicatorSize)
.rotate(degrees = rotateAnimation)
.border(
width = 4.dp,

View File

@@ -34,7 +34,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.quartz.encoders.decodePublicKey
import com.vitorpamplona.quartz.encoders.toHexKey
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.roundToInt
data class RangesChanges(val original: TextRange, val modified: TextRange)
@@ -55,71 +54,69 @@ fun buildAnnotatedStringWithUrlHighlighting(
val builderBefore = StringBuilder() // important to correctly measure Tag start and end
val builderAfter = StringBuilder() // important to correctly measure Tag start and end
append(
text
.split('\n').joinToString("\n") { paragraph: String ->
paragraph
.split(' ').joinToString(" ") { word: String ->
try {
if (word.startsWith("@npub") && word.length >= 64) {
val keyB32 = word.substring(0, 64)
val restOfWord = word.substring(64)
text.text.split('\n').joinToString("\n") { paragraph: String ->
paragraph.split(' ').joinToString(" ") { word: String ->
try {
if (word.startsWith("@npub") && word.length >= 64) {
val keyB32 = word.substring(0, 64)
val restOfWord = word.substring(64)
val startIndex = builderBefore.toString().length
val startIndex = builderBefore.toString().length
builderBefore.append(
"$keyB32$restOfWord ",
) // accounts for the \n at the end of each paragraph
builderBefore.append(
"$keyB32$restOfWord ",
) // accounts for the \n at the end of each paragraph
val endIndex = startIndex + keyB32.length
val endIndex = startIndex + keyB32.length
val key = decodePublicKey(keyB32.removePrefix("@"))
val user = LocalCache.getOrCreateUser(key.toHexKey())
val key = decodePublicKey(keyB32.removePrefix("@"))
val user = LocalCache.getOrCreateUser(key.toHexKey())
val newWord = "@${user.toBestDisplayName()}"
val startNew = builderAfter.toString().length
val newWord = "@${user.toBestDisplayName()}"
val startNew = builderAfter.toString().length
builderAfter.append(
"$newWord$restOfWord ",
) // accounts for the \n at the end of each paragraph
builderAfter.append(
"$newWord$restOfWord ",
) // accounts for the \n at the end of each paragraph
substitutions.add(
RangesChanges(
TextRange(startIndex, endIndex),
TextRange(startNew, startNew + newWord.length),
),
)
newWord + restOfWord
} else if (Patterns.WEB_URL.matcher(word).matches()) {
val startIndex = builderBefore.toString().length
val endIndex = startIndex + word.length
substitutions.add(
RangesChanges(
TextRange(startIndex, endIndex),
TextRange(startNew, startNew + newWord.length),
),
)
newWord + restOfWord
} else if (Patterns.WEB_URL.matcher(word).matches()) {
val startIndex = builderBefore.toString().length
val endIndex = startIndex + word.length
val startNew = builderAfter.toString().length
val endNew = startNew + word.length
val startNew = builderAfter.toString().length
val endNew = startNew + word.length
substitutions.add(
RangesChanges(
TextRange(startIndex, endIndex),
TextRange(startNew, endNew),
),
)
substitutions.add(
RangesChanges(
TextRange(startIndex, endIndex),
TextRange(startNew, endNew),
),
)
builderBefore.append("$word ")
builderAfter.append("$word ")
word
} else {
builderBefore.append("$word ")
builderAfter.append("$word ")
word
}
} catch (e: Exception) {
if (e is CancellationException) throw e
// if it can't parse the key, don't try to change.
builderBefore.append("$word ")
builderAfter.append("$word ")
word
}
builderBefore.append("$word ")
builderAfter.append("$word ")
word
} else {
builderBefore.append("$word ")
builderAfter.append("$word ")
word
}
},
} catch (e: Exception) {
if (e is CancellationException) throw e
// if it can't parse the key, don't try to change.
builderBefore.append("$word ")
builderAfter.append("$word ")
word
}
}
},
)
substitutions.forEach {
@@ -144,16 +141,15 @@ fun buildAnnotatedStringWithUrlHighlighting(
if (inInsideRange != null) {
val percentInRange =
(offset - inInsideRange.original.start) / (inInsideRange.original.length.toFloat())
return (inInsideRange.modified.start + inInsideRange.modified.length * percentInRange)
.roundToInt()
return (inInsideRange.modified.start + inInsideRange.modified.length * percentInRange).toInt()
}
val lastRangeThrough = substitutions.lastOrNull { offset >= it.original.end }
if (lastRangeThrough != null) {
return lastRangeThrough.modified.end + (offset - lastRangeThrough.original.end)
return if (lastRangeThrough != null) {
lastRangeThrough.modified.end + (offset - lastRangeThrough.original.end)
} else {
return offset
offset
}
}
@@ -164,16 +160,15 @@ fun buildAnnotatedStringWithUrlHighlighting(
if (inInsideRange != null) {
val percentInRange =
(offset - inInsideRange.modified.start) / (inInsideRange.modified.length.toFloat())
return (inInsideRange.original.start + inInsideRange.original.length * percentInRange)
.roundToInt()
return (inInsideRange.original.start + inInsideRange.original.length * percentInRange).toInt()
}
val lastRangeThrough = substitutions.lastOrNull { offset >= it.modified.end }
if (lastRangeThrough != null) {
return lastRangeThrough.original.end + (offset - lastRangeThrough.modified.end)
return if (lastRangeThrough != null) {
lastRangeThrough.original.end + (offset - lastRangeThrough.modified.end)
} else {
return offset
offset
}
}
}

View File

@@ -37,7 +37,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -46,6 +45,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
@@ -76,7 +76,7 @@ fun AddDMRelayListDialog(
) {
Spacer(modifier = StdHorzSpacer)
Text(stringResource(R.string.dm_relays_title))
Text(stringRes(R.string.dm_relays_title))
SaveButton(
onPost = {
@@ -126,13 +126,13 @@ private fun Explanation() {
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(id = R.string.dm_relays_not_found_editing),
text = stringRes(id = R.string.dm_relays_not_found_editing),
)
Spacer(modifier = StdVertSpacer)
Text(
text = stringResource(id = R.string.dm_relays_not_found_examples),
text = stringRes(id = R.string.dm_relays_not_found_examples),
)
}
}

View File

@@ -37,7 +37,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -46,6 +45,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
@@ -76,7 +76,7 @@ fun AddSearchRelayListDialog(
) {
Spacer(modifier = StdHorzSpacer)
Text(stringResource(R.string.search_relays_title))
Text(stringRes(R.string.search_relays_title))
SaveButton(
onPost = {
@@ -126,13 +126,13 @@ private fun Explanation() {
Card(modifier = MaterialTheme.colorScheme.imageModifier) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(id = R.string.search_relays_not_found_editing),
text = stringRes(id = R.string.search_relays_not_found_editing),
)
Spacer(modifier = StdVertSpacer)
Text(
text = stringResource(id = R.string.search_relays_not_found_examples),
text = stringRes(id = R.string.search_relays_not_found_examples),
)
}
}

View File

@@ -40,7 +40,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -48,9 +47,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.RelayStat
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
@@ -109,7 +110,7 @@ fun AllRelayListView(
Spacer(modifier = MinHorzSpacer)
Text(
text = stringResource(R.string.relay_settings),
text = stringRes(R.string.relay_settings),
style = MaterialTheme.typography.titleLarge,
)
@@ -165,8 +166,8 @@ fun AllRelayListView(
LazyColumn(contentPadding = FeedPadding) {
item {
SettingsCategory(
stringResource(R.string.public_home_section),
stringResource(R.string.public_home_section_explainer),
stringRes(R.string.public_home_section),
stringRes(R.string.public_home_section_explainer),
Modifier.padding(bottom = 8.dp),
)
}
@@ -174,32 +175,32 @@ fun AllRelayListView(
item {
SettingsCategory(
stringResource(R.string.public_notif_section),
stringResource(R.string.public_notif_section_explainer),
stringRes(R.string.public_notif_section),
stringRes(R.string.public_notif_section_explainer),
)
}
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, onClose, nav)
item {
SettingsCategory(
stringResource(R.string.private_inbox_section),
stringResource(R.string.private_inbox_section_explainer),
stringRes(R.string.private_inbox_section),
stringRes(R.string.private_inbox_section_explainer),
)
}
renderDMItems(dmFeedState, dmViewModel, accountViewModel, onClose, nav)
item {
SettingsCategory(
stringResource(R.string.private_outbox_section),
stringResource(R.string.private_outbox_section_explainer),
stringRes(R.string.private_outbox_section),
stringRes(R.string.private_outbox_section_explainer),
)
}
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, onClose, nav)
item {
SettingsCategoryWithButton(
stringResource(R.string.search_section),
stringResource(R.string.search_section_explainer),
stringRes(R.string.search_section),
stringRes(R.string.search_section_explainer),
action = {
ResetSearchRelays(searchViewModel)
},
@@ -209,16 +210,16 @@ fun AllRelayListView(
item {
SettingsCategory(
stringResource(R.string.local_section),
stringResource(R.string.local_section_explainer),
stringRes(R.string.local_section),
stringRes(R.string.local_section_explainer),
)
}
renderLocalItems(localFeedState, localViewModel, accountViewModel, onClose, nav)
item {
SettingsCategoryWithButton(
stringResource(R.string.kind_3_section),
stringResource(R.string.kind_3_section_description),
stringRes(R.string.kind_3_section),
stringRes(R.string.kind_3_section_description),
action = {
ResetKind3Relays(kind3ViewModel)
},
@@ -236,11 +237,11 @@ fun ResetKind3Relays(postViewModel: Kind3RelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
Constants.defaultRelays.forEach { postViewModel.addRelay(it) }
postViewModel.addAll(Constants.defaultRelays)
postViewModel.loadRelayDocuments()
},
) {
Text(stringResource(R.string.default_relays))
Text(stringRes(R.string.default_relays))
}
}
@@ -249,11 +250,11 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it)) }
Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
postViewModel.loadRelayDocuments()
},
) {
Text(stringResource(R.string.default_relays))
Text(stringRes(R.string.default_relays))
}
}

View File

@@ -22,14 +22,25 @@ package com.vitorpamplona.amethyst.ui.actions.relays
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayStat
@Immutable
data class BasicRelaySetupInfo(
val url: String,
val errorCount: Int = 0,
val downloadCountInBytes: Int = 0,
val uploadCountInBytes: Int = 0,
val spamCount: Int = 0,
val relayStat: RelayStat,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
@Immutable
data class Kind3BasicRelaySetupInfo(
val url: String,
val read: Boolean,
val write: Boolean,
val feedTypes: Set<FeedType>,
val relayStat: RelayStat,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)

View File

@@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
fun BasicRelaySetupInfoClickableRow(
item: BasicRelaySetupInfo,
loadProfilePicture: Boolean,
loadRobohash: Boolean,
onDelete: (BasicRelaySetupInfo) -> Unit,
onClick: () -> Unit,
accountViewModel: AccountViewModel,
@@ -65,6 +66,7 @@ fun BasicRelaySetupInfoClickableRow(
item.briefInfo.displayUrl,
iconUrlFromRelayInfoDoc ?: item.briefInfo.favIcon,
loadProfilePicture,
loadRobohash,
MaterialTheme.colorScheme.largeRelayIconModifier,
)
}

View File

@@ -27,10 +27,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun BasicRelaySetupInfoDialog(
@@ -52,14 +54,10 @@ fun BasicRelaySetupInfoDialog(
)
}
val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value
}
BasicRelaySetupInfoClickableRow(
item = item,
loadProfilePicture = automaticallyShowProfilePicture,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
onDelete = onDelete,
accountViewModel = accountViewModel,
onClick = {
@@ -72,28 +70,32 @@ fun BasicRelaySetupInfoDialog(
val msg =
when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
@@ -101,7 +103,7 @@ fun BasicRelaySetupInfoDialog(
}
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
stringRes(context, R.string.unable_to_download_relay_document),
msg,
)
},

View File

@@ -24,7 +24,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -78,20 +79,11 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
val relayList = getRelayList() ?: emptyList()
relayList.map { relayUrl ->
val liveRelay = RelayPool.getRelay(relayUrl)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
BasicRelaySetupInfo(
relayUrl,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
RelayUrlFormatter.normalize(relayUrl),
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
}.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
}
}

View File

@@ -28,6 +28,14 @@ fun countToHumanReadableBytes(counter: Int) =
else -> "$counter"
}
fun countToHumanReadableBytes(counter: Long) =
when {
counter >= 1000000000 -> "${Math.round(counter / 1000000000f)} GB"
counter >= 1000000 -> "${Math.round(counter / 1000000f)} MB"
counter >= 1000 -> "${Math.round(counter / 1000f)} KB"
else -> "$counter"
}
fun countToHumanReadable(
counter: Int,
str: String,
@@ -37,3 +45,13 @@ fun countToHumanReadable(
counter >= 1000 -> "${Math.round(counter / 1000f)}K $str"
else -> "$counter $str"
}
fun countToHumanReadable(
counter: Long,
str: String,
) = when {
counter >= 1000000000 -> "${Math.round(counter / 1000000000f)}G $str"
counter >= 1000000 -> "${Math.round(counter / 1000000f)}M $str"
counter >= 1000 -> "${Math.round(counter / 1000f)}K $str"
else -> "$counter $str"
}

View File

@@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel

View File

@@ -61,21 +61,22 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayStat
import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@@ -95,7 +96,7 @@ import kotlinx.coroutines.launch
@Composable
fun Kind3RelayListView(
feedState: List<RelaySetupInfo>,
feedState: List<Kind3BasicRelaySetupInfo>,
postViewModel: Kind3RelayListViewModel,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
@@ -110,7 +111,7 @@ fun Kind3RelayListView(
}
fun LazyListScope.renderKind3Items(
feedState: List<RelaySetupInfo>,
feedState: List<Kind3BasicRelaySetupInfo>,
postViewModel: Kind3RelayListViewModel,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
@@ -146,15 +147,19 @@ fun LazyListScope.renderKind3Items(
fun ServerConfigPreview() {
ClickableRelayItem(
loadProfilePicture = true,
loadRobohash = false,
item =
RelaySetupInfo(
Kind3BasicRelaySetupInfo(
url = "nostr.mom",
read = true,
write = true,
errorCount = 23,
downloadCountInBytes = 10000,
uploadCountInBytes = 10000000,
spamCount = 10,
relayStat =
RelayStat(
errorCounter = 23,
receivedBytes = 10000,
sentBytes = 10000000,
spamCounter = 10,
),
feedTypes = Constants.activeTypesGlobalChats,
paidRelay = true,
),
@@ -172,15 +177,15 @@ fun ServerConfigPreview() {
@Composable
fun LoadRelayInfo(
item: RelaySetupInfo,
onToggleDownload: (RelaySetupInfo) -> Unit,
onToggleUpload: (RelaySetupInfo) -> Unit,
onToggleFollows: (RelaySetupInfo) -> Unit,
onTogglePrivateDMs: (RelaySetupInfo) -> Unit,
onTogglePublicChats: (RelaySetupInfo) -> Unit,
onToggleGlobal: (RelaySetupInfo) -> Unit,
onToggleSearch: (RelaySetupInfo) -> Unit,
onDelete: (RelaySetupInfo) -> Unit,
item: Kind3BasicRelaySetupInfo,
onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleSearch: (Kind3BasicRelaySetupInfo) -> Unit,
onDelete: (Kind3BasicRelaySetupInfo) -> Unit,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -197,14 +202,10 @@ fun LoadRelayInfo(
)
}
val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value
}
ClickableRelayItem(
item = item,
loadProfilePicture = automaticallyShowProfilePicture,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
onToggleDownload = onToggleDownload,
onToggleUpload = onToggleUpload,
onToggleFollows = onToggleFollows,
@@ -223,28 +224,32 @@ fun LoadRelayInfo(
val msg =
when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
context.getString(
stringRes(
context,
R.string.relay_information_document_error_assemble_url,
url,
exceptionMessage,
@@ -252,7 +257,7 @@ fun LoadRelayInfo(
}
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
stringRes(context, R.string.unable_to_download_relay_document),
msg,
)
},
@@ -263,16 +268,17 @@ fun LoadRelayInfo(
@Composable
fun ClickableRelayItem(
item: RelaySetupInfo,
item: Kind3BasicRelaySetupInfo,
loadProfilePicture: Boolean,
onToggleDownload: (RelaySetupInfo) -> Unit,
onToggleUpload: (RelaySetupInfo) -> Unit,
onToggleFollows: (RelaySetupInfo) -> Unit,
onTogglePrivateDMs: (RelaySetupInfo) -> Unit,
onTogglePublicChats: (RelaySetupInfo) -> Unit,
onToggleGlobal: (RelaySetupInfo) -> Unit,
onToggleSearch: (RelaySetupInfo) -> Unit,
onDelete: (RelaySetupInfo) -> Unit,
loadRobohash: Boolean,
onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleSearch: (Kind3BasicRelaySetupInfo) -> Unit,
onDelete: (Kind3BasicRelaySetupInfo) -> Unit,
onClick: () -> Unit,
) {
Column(Modifier.fillMaxWidth()) {
@@ -290,6 +296,7 @@ fun ClickableRelayItem(
item.briefInfo.displayUrl,
iconUrlFromRelayInfoDoc ?: item.briefInfo.favIcon,
loadProfilePicture,
loadRobohash,
MaterialTheme.colorScheme.largeRelayIconModifier,
)
}
@@ -333,9 +340,9 @@ fun ClickableRelayItem(
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun StatusRow(
item: RelaySetupInfo,
onToggleDownload: (RelaySetupInfo) -> Unit,
onToggleUpload: (RelaySetupInfo) -> Unit,
item: Kind3BasicRelaySetupInfo,
onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit,
modifier: Modifier,
) {
val scope = rememberCoroutineScope()
@@ -343,19 +350,20 @@ private fun StatusRow(
Icon(
imageVector = Icons.Default.Download,
contentDescription = stringResource(R.string.read_from_relay),
contentDescription = stringRes(R.string.read_from_relay),
modifier =
Modifier.size(15.dp)
Modifier
.size(15.dp)
.combinedClickable(
onClick = { onToggleDownload(item) },
onLongClick = {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.read_from_relay),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.read_from_relay),
Toast.LENGTH_SHORT,
).show()
}
},
),
@@ -370,7 +378,7 @@ private fun StatusRow(
)
Text(
text = countToHumanReadableBytes(item.downloadCountInBytes),
text = countToHumanReadableBytes(item.relayStat.receivedBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -379,19 +387,20 @@ private fun StatusRow(
Icon(
imageVector = Icons.Default.Upload,
stringResource(R.string.write_to_relay),
stringRes(R.string.write_to_relay),
modifier =
Modifier.size(15.dp)
Modifier
.size(15.dp)
.combinedClickable(
onClick = { onToggleUpload(item) },
onLongClick = {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.write_to_relay),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.write_to_relay),
Toast.LENGTH_SHORT,
).show()
}
},
),
@@ -406,7 +415,7 @@ private fun StatusRow(
)
Text(
text = countToHumanReadableBytes(item.uploadCountInBytes),
text = countToHumanReadableBytes(item.relayStat.sentBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -415,24 +424,25 @@ private fun StatusRow(
Icon(
imageVector = Icons.Default.SyncProblem,
stringResource(R.string.errors),
stringRes(R.string.errors),
modifier =
Modifier.size(15.dp)
Modifier
.size(15.dp)
.combinedClickable(
onClick = {},
onLongClick = {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.errors),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.errors),
Toast.LENGTH_SHORT,
).show()
}
},
),
tint =
if (item.errorCount > 0) {
if (item.relayStat.errorCounter > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
@@ -440,7 +450,7 @@ private fun StatusRow(
)
Text(
text = countToHumanReadable(item.errorCount, "errors"),
text = countToHumanReadable(item.relayStat.errorCounter, "errors"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -449,24 +459,25 @@ private fun StatusRow(
Icon(
imageVector = Icons.Default.DeleteSweep,
stringResource(R.string.spam),
stringRes(R.string.spam),
modifier =
Modifier.size(15.dp)
Modifier
.size(15.dp)
.combinedClickable(
onClick = {},
onLongClick = {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.spam),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.spam),
Toast.LENGTH_SHORT,
).show()
}
},
),
tint =
if (item.spamCount > 0) {
if (item.relayStat.spamCounter > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
@@ -474,7 +485,7 @@ private fun StatusRow(
)
Text(
text = countToHumanReadable(item.spamCount, "spam"),
text = countToHumanReadable(item.relayStat.spamCounter, "spam"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -485,17 +496,17 @@ private fun StatusRow(
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun ActiveToggles(
item: RelaySetupInfo,
onToggleFollows: (RelaySetupInfo) -> Unit,
onTogglePrivateDMs: (RelaySetupInfo) -> Unit,
onTogglePublicChats: (RelaySetupInfo) -> Unit,
onToggleGlobal: (RelaySetupInfo) -> Unit,
item: Kind3BasicRelaySetupInfo,
onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit,
onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit,
onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit,
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
Text(
text = stringResource(id = R.string.active_for),
text = stringRes(id = R.string.active_for),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.placeholderText,
@@ -509,20 +520,21 @@ private fun ActiveToggles(
) {
Icon(
painterResource(R.drawable.ic_home),
stringResource(R.string.home_feed),
stringRes(R.string.home_feed),
modifier =
Modifier.padding(horizontal = 5.dp)
Modifier
.padding(horizontal = 5.dp)
.size(15.dp)
.combinedClickable(
onClick = { onToggleFollows(item) },
onLongClick = {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.home_feed),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.home_feed),
Toast.LENGTH_SHORT,
).show()
}
},
),
@@ -542,20 +554,21 @@ private fun ActiveToggles(
) {
Icon(
painterResource(R.drawable.ic_dm),
stringResource(R.string.private_message_feed),
stringRes(R.string.private_message_feed),
modifier =
Modifier.padding(horizontal = 5.dp)
Modifier
.padding(horizontal = 5.dp)
.size(15.dp)
.combinedClickable(
onClick = { onTogglePrivateDMs(item) },
onLongClick = {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.private_message_feed),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.private_message_feed),
Toast.LENGTH_SHORT,
).show()
}
},
),
@@ -575,20 +588,21 @@ private fun ActiveToggles(
) {
Icon(
imageVector = Icons.Default.Groups,
contentDescription = stringResource(R.string.public_chat_feed),
contentDescription = stringRes(R.string.public_chat_feed),
modifier =
Modifier.padding(horizontal = 5.dp)
Modifier
.padding(horizontal = 5.dp)
.size(15.dp)
.combinedClickable(
onClick = { onTogglePublicChats(item) },
onLongClick = {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.public_chat_feed),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.public_chat_feed),
Toast.LENGTH_SHORT,
).show()
}
},
),
@@ -608,20 +622,21 @@ private fun ActiveToggles(
) {
Icon(
imageVector = Icons.Default.Public,
stringResource(R.string.global_feed),
stringRes(R.string.global_feed),
modifier =
Modifier.padding(horizontal = 5.dp)
Modifier
.padding(horizontal = 5.dp)
.size(15.dp)
.combinedClickable(
onClick = { onToggleGlobal(item) },
onLongClick = {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.global_feed),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.global_feed),
Toast.LENGTH_SHORT,
).show()
}
},
),
@@ -639,9 +654,9 @@ private fun ActiveToggles(
@Composable
private fun FirstLine(
item: RelaySetupInfo,
item: Kind3BasicRelaySetupInfo,
onClick: () -> Unit,
onDelete: (RelaySetupInfo) -> Unit,
onDelete: (Kind3BasicRelaySetupInfo) -> Unit,
modifier: Modifier,
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
@@ -669,7 +684,7 @@ private fun FirstLine(
) {
Icon(
imageVector = Icons.Default.Cancel,
contentDescription = stringResource(id = R.string.remove),
contentDescription = stringRes(id = R.string.remove),
modifier = Modifier.padding(start = 10.dp).size(15.dp),
tint = WarningColor,
)
@@ -680,7 +695,7 @@ private fun FirstLine(
@Composable
fun Kind3RelayEditBox(
relayToAdd: String,
onNewRelay: (RelaySetupInfo) -> Unit,
onNewRelay: (Kind3BasicRelaySetupInfo) -> Unit,
) {
var url by remember { mutableStateOf<String>(relayToAdd) }
var read by remember { mutableStateOf(true) }
@@ -688,7 +703,7 @@ fun Kind3RelayEditBox(
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.add_a_relay)) },
label = { Text(text = stringRes(R.string.add_a_relay)) },
modifier = Modifier.weight(1f),
value = url,
onValueChange = { url = it },
@@ -705,7 +720,7 @@ fun Kind3RelayEditBox(
IconButton(onClick = { read = !read }) {
Icon(
imageVector = Icons.Default.Download,
contentDescription = stringResource(id = R.string.read_from_relay),
contentDescription = stringRes(id = R.string.read_from_relay),
modifier = Modifier.size(Size35dp).padding(horizontal = 5.dp),
tint =
if (read) {
@@ -719,7 +734,7 @@ fun Kind3RelayEditBox(
IconButton(onClick = { write = !write }) {
Icon(
imageVector = Icons.Default.Upload,
contentDescription = stringResource(id = R.string.write_to_relay),
contentDescription = stringRes(id = R.string.write_to_relay),
modifier = Modifier.size(Size35dp).padding(horizontal = 5.dp),
tint =
if (write) {
@@ -733,13 +748,13 @@ fun Kind3RelayEditBox(
Button(
onClick = {
if (url.isNotBlank() && url != "/") {
val addedWSS = RelayUrlFormatter.normalize(url)
onNewRelay(
RelaySetupInfo(
addedWSS,
read,
write,
feedTypes = FeedType.values().toSet(),
Kind3BasicRelaySetupInfo(
url = RelayUrlFormatter.normalize(url),
read = read,
write = write,
feedTypes = FeedType.entries.toSet(),
relayStat = RelayStat(),
),
)
url = ""
@@ -758,7 +773,7 @@ fun Kind3RelayEditBox(
},
),
) {
Text(text = stringResource(id = R.string.add), color = Color.White)
Text(text = stringRes(id = R.string.add), color = Color.White)
}
}
}

View File

@@ -27,7 +27,8 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -38,7 +39,7 @@ import kotlinx.coroutines.launch
class Kind3RelayListViewModel : ViewModel() {
private lateinit var account: Account
private val _relays = MutableStateFlow<List<RelaySetupInfo>>(emptyList())
private val _relays = MutableStateFlow<List<Kind3BasicRelaySetupInfo>>(emptyList())
val relays = _relays.asStateFlow()
var hasModified = false
@@ -52,7 +53,16 @@ class Kind3RelayListViewModel : ViewModel() {
fun create() {
if (hasModified) {
viewModelScope.launch(Dispatchers.IO) {
account.saveKind3RelayList(relays.value)
account.saveKind3RelayList(
relays.value.map {
RelaySetupInfo(
it.url,
it.read,
it.write,
it.feedTypes,
)
},
)
clear()
}
}
@@ -80,7 +90,6 @@ class Kind3RelayListViewModel : ViewModel() {
if (relayFile != null) {
relayFile
.map {
val liveRelay = RelayPool.getRelay(it.key)
val localInfoFeedTypes =
account.localRelays
.filter { localRelay -> localRelay.url == it.key }
@@ -92,54 +101,55 @@ class Kind3RelayListViewModel : ViewModel() {
?.feedTypes
?: FeedType.values().toSet().toImmutableSet()
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
RelaySetupInfo(
it.key,
it.value.read,
it.value.write,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
localInfoFeedTypes,
Kind3BasicRelaySetupInfo(
url = RelayUrlFormatter.normalize(it.key),
read = it.value.read,
write = it.value.write,
feedTypes = localInfoFeedTypes,
relayStat = RelayStats.get(it.key),
)
}
.distinctBy { it.url }
.sortedBy { it.downloadCountInBytes }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
} else {
account.localRelays
.map {
val liveRelay = RelayPool.getRelay(it.url)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
RelaySetupInfo(
it.url,
it.read,
it.write,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
it.feedTypes,
Kind3BasicRelaySetupInfo(
url = RelayUrlFormatter.normalize(it.url),
read = it.read,
write = it.write,
feedTypes = it.feedTypes,
relayStat = RelayStats.get(it.url),
)
}
.distinctBy { it.url }
.sortedBy { it.downloadCountInBytes }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
}
}
fun addRelay(relay: RelaySetupInfo) {
fun addAll(defaultRelays: Array<RelaySetupInfo>) {
hasModified = true
_relays.update {
defaultRelays.map {
Kind3BasicRelaySetupInfo(
url = RelayUrlFormatter.normalize(it.url),
read = it.read,
write = it.write,
feedTypes = it.feedTypes,
relayStat = RelayStats.get(it.url),
)
}
.distinctBy { it.url }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
}
fun addRelay(relay: Kind3BasicRelaySetupInfo) {
if (relays.value.any { it.url == relay.url }) return
_relays.update { it.plus(relay) }
@@ -147,7 +157,7 @@ class Kind3RelayListViewModel : ViewModel() {
hasModified = true
}
fun deleteRelay(relay: RelaySetupInfo) {
fun deleteRelay(relay: Kind3BasicRelaySetupInfo) {
_relays.update { it.minus(relay) }
hasModified = true
}
@@ -157,48 +167,48 @@ class Kind3RelayListViewModel : ViewModel() {
hasModified = true
}
fun toggleDownload(relay: RelaySetupInfo) {
fun toggleDownload(relay: Kind3BasicRelaySetupInfo) {
_relays.update { it.updated(relay, relay.copy(read = !relay.read)) }
hasModified = true
}
fun toggleUpload(relay: RelaySetupInfo) {
fun toggleUpload(relay: Kind3BasicRelaySetupInfo) {
_relays.update { it.updated(relay, relay.copy(write = !relay.write)) }
hasModified = true
}
fun toggleFollows(relay: RelaySetupInfo) {
fun toggleFollows(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.FOLLOWS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleMessages(relay: RelaySetupInfo) {
fun toggleMessages(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.PRIVATE_DMS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun togglePublicChats(relay: RelaySetupInfo) {
fun togglePublicChats(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.PUBLIC_CHATS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleGlobal(relay: RelaySetupInfo) {
fun toggleGlobal(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.GLOBAL)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleSearch(relay: RelaySetupInfo) {
fun toggleSearch(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.SEARCH)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun togglePaidRelay(
relay: RelaySetupInfo,
relay: Kind3BasicRelaySetupInfo,
paid: Boolean,
) {
_relays.update { it.updated(relay, relay.copy(paidRelay = paid)) }

View File

@@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel

View File

@@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel

View File

@@ -24,7 +24,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -105,40 +106,22 @@ class Nip65RelayListViewModel : ViewModel() {
val relayList = account.getNIP65RelayList()?.writeRelays() ?: emptyList()
relayList.map { relayUrl ->
val liveRelay = RelayPool.getRelay(relayUrl)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
BasicRelaySetupInfo(
relayUrl,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
RelayUrlFormatter.normalize(relayUrl),
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
}.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
}
_notificationRelays.update {
val relayList = account.getNIP65RelayList()?.readRelays() ?: emptyList()
relayList.map { relayUrl ->
val liveRelay = RelayPool.getRelay(relayUrl)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
BasicRelaySetupInfo(
relayUrl,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
RelayUrlFormatter.normalize(relayUrl),
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
}.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
}
}

View File

@@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel

View File

@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.actions.relays
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -31,35 +30,46 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.LoadUser
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import com.vitorpamplona.quartz.events.EmptyTagList
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -70,9 +80,15 @@ fun RelayInformationDialog(
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value
val messages =
remember(relayBriefInfo) {
RelayStats
.get(url = relayBriefInfo.url)
.messages
.snapshot()
.values
.sortedByDescending { it.time }
.toImmutableList()
}
Dialog(
@@ -84,162 +100,205 @@ fun RelayInformationDialog(
),
) {
Surface {
val scrollState = rememberScrollState()
val color = mutableStateOf(Color.Transparent)
val context = LocalContext.current
Column(
modifier = Modifier.padding(10.dp).fillMaxSize().verticalScroll(scrollState),
LazyColumn(
modifier =
Modifier
.padding(10.dp)
.fillMaxSize(),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
CloseButton(onPress = { onClose() })
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = StdPadding.fillMaxWidth(),
) {
Column {
RenderRelayIcon(
relayBriefInfo.displayUrl,
relayInfo.icon ?: relayBriefInfo.favIcon,
automaticallyShowProfilePicture,
MaterialTheme.colorScheme.largeRelayIconModifier,
)
}
Spacer(modifier = DoubleHorzSpacer)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row { Title(relayInfo.name?.trim() ?: "") }
Row { SubtitleContent(relayInfo.description?.trim() ?: "") }
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
CloseButton(onPress = { onClose() })
}
}
item {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = StdPadding.fillMaxWidth(),
) {
Column {
RenderRelayIcon(
displayUrl = relayBriefInfo.displayUrl,
iconUrl = relayInfo.icon ?: relayBriefInfo.favIcon,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
iconModifier = MaterialTheme.colorScheme.largeRelayIconModifier,
)
}
Section(stringResource(R.string.owner))
Spacer(modifier = DoubleHorzSpacer)
relayInfo.pubkey?.let {
DisplayOwnerInformation(it, accountViewModel) {
onClose()
nav(it)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row { Title(relayInfo.name?.trim() ?: "") }
Row { SubtitleContent(relayInfo.description?.trim() ?: "") }
}
}
}
item {
Section(stringRes(R.string.owner))
Section(stringResource(R.string.software))
relayInfo.pubkey?.let {
DisplayOwnerInformation(it, accountViewModel) {
onClose()
nav(it)
}
}
}
item {
Section(stringRes(R.string.software))
DisplaySoftwareInformation(relayInfo)
DisplaySoftwareInformation(relayInfo)
Section(stringResource(R.string.version))
Section(stringRes(R.string.version))
SectionContent(relayInfo.version ?: "")
SectionContent(relayInfo.version ?: "")
}
item {
Section(stringRes(R.string.contact))
Section(stringResource(R.string.contact))
Box(modifier = Modifier.padding(start = 10.dp)) {
relayInfo.contact?.let {
if (it.startsWith("https:")) {
ClickableUrl(urlText = it, url = it)
} else if (it.startsWith("mailto:") || it.contains('@')) {
ClickableEmail(it)
} else {
SectionContent(it)
}
}
}
}
item {
Section(stringRes(R.string.supports))
Box(modifier = Modifier.padding(start = 10.dp)) {
relayInfo.contact?.let {
if (it.startsWith("https:")) {
ClickableUrl(urlText = it, url = it)
} else if (it.startsWith("mailto:") || it.contains('@')) {
ClickableEmail(it)
} else {
SectionContent(it)
DisplaySupportedNips(relayInfo)
}
item {
relayInfo.fees?.admission?.let {
if (it.isNotEmpty()) {
Section(stringRes(R.string.admission_fees))
it.forEach { item -> SectionContent("${item.amount?.div(1000) ?: 0} sats") }
}
}
relayInfo.payments_url?.let {
Section(stringRes(R.string.payments_url))
Box(modifier = Modifier.padding(start = 10.dp)) {
ClickableUrl(
urlText = it,
url = it,
)
}
}
}
item {
relayInfo.limitation?.let {
Section(stringRes(R.string.limitations))
val authRequiredText =
if (it.auth_required ?: false) stringRes(R.string.yes) else stringRes(R.string.no)
val paymentRequiredText =
if (it.payment_required ?: false) stringRes(R.string.yes) else stringRes(R.string.no)
val restrictedWritesText =
if (it.restricted_writes ?: false) stringRes(R.string.yes) else stringRes(R.string.no)
Column {
SectionContent(
"${stringRes(R.string.message_length)}: ${it.max_message_length ?: 0}",
)
SectionContent(
"${stringRes(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}",
)
SectionContent("${stringRes(R.string.filters)}: ${it.max_filters ?: 0}")
SectionContent(
"${stringRes(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}",
)
SectionContent("${stringRes(R.string.minimum_prefix)}: ${it.min_prefix ?: 0}")
SectionContent(
"${stringRes(R.string.maximum_event_tags)}: ${it.max_event_tags ?: 0}",
)
SectionContent(
"${stringRes(R.string.content_length)}: ${it.max_content_length ?: 0}",
)
SectionContent(
"${stringRes(R.string.max_limit)}: ${it.max_limit ?: 0}",
)
SectionContent("${stringRes(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}")
SectionContent("${stringRes(R.string.auth)}: $authRequiredText")
SectionContent("${stringRes(R.string.payment)}: $paymentRequiredText")
SectionContent("${stringRes(R.string.restricted_writes)}: $restrictedWritesText")
}
}
}
item {
relayInfo.relay_countries?.let {
Section(stringRes(R.string.countries))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
}
item {
relayInfo.language_tags?.let {
Section(stringRes(R.string.languages))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
}
item {
relayInfo.tags?.let {
Section(stringRes(R.string.tags))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
}
item {
relayInfo.posting_policy?.let {
Section(stringRes(R.string.posting_policy))
Box(Modifier.padding(10.dp)) {
ClickableUrl(
it,
it,
)
}
}
}
Section(stringResource(R.string.supports))
DisplaySupportedNips(relayInfo)
relayInfo.fees?.admission?.let {
if (it.isNotEmpty()) {
Section(stringResource(R.string.admission_fees))
it.forEach { item -> SectionContent("${item.amount?.div(1000) ?: 0} sats") }
}
item {
Section(stringRes(R.string.relay_error_messages))
}
relayInfo.payments_url?.let {
Section(stringResource(R.string.payments_url))
Box(modifier = Modifier.padding(start = 10.dp)) {
ClickableUrl(
urlText = it,
url = it,
items(messages) { msg ->
Row {
TranslatableRichTextViewer(
content =
remember {
"${timeAgo(msg.time, context)}, ${msg.type.name}: ${msg.message}"
},
canPreview = false,
quotesLeft = 0,
modifier = Modifier.fillMaxWidth(),
tags = EmptyTagList,
backgroundColor = color,
id = msg.hashCode().toString(),
accountViewModel = accountViewModel,
nav = nav,
)
}
}
relayInfo.limitation?.let {
Section(stringResource(R.string.limitations))
val authRequiredText =
if (it.auth_required ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
val paymentRequiredText =
if (it.payment_required ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
val restrictedWritesText =
if (it.restricted_writes ?: false) stringResource(R.string.yes) else stringResource(R.string.no)
Column {
SectionContent(
"${stringResource(R.string.message_length)}: ${it.max_message_length ?: 0}",
)
SectionContent(
"${stringResource(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}",
)
SectionContent("${stringResource(R.string.filters)}: ${it.max_filters ?: 0}")
SectionContent(
"${stringResource(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}",
)
SectionContent("${stringResource(R.string.minimum_prefix)}: ${it.min_prefix ?: 0}")
SectionContent(
"${stringResource(R.string.maximum_event_tags)}: ${it.max_event_tags ?: 0}",
)
SectionContent(
"${stringResource(R.string.content_length)}: ${it.max_content_length ?: 0}",
)
SectionContent(
"${stringResource(R.string.max_limit)}: ${it.max_limit ?: 0}",
)
SectionContent("${stringResource(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}")
SectionContent("${stringResource(R.string.auth)}: $authRequiredText")
SectionContent("${stringResource(R.string.payment)}: $paymentRequiredText")
SectionContent("${stringResource(R.string.restricted_writes)}: $restrictedWritesText")
}
}
relayInfo.relay_countries?.let {
Section(stringResource(R.string.countries))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
relayInfo.language_tags?.let {
Section(stringResource(R.string.languages))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
relayInfo.tags?.let {
Section(stringResource(R.string.tags))
FlowRow { it.forEach { item -> SectionContent(item) } }
}
relayInfo.posting_policy?.let {
Section(stringResource(R.string.posting_policy))
Box(Modifier.padding(10.dp)) {
ClickableUrl(
it,
it,
)
}
Spacer(modifier = StdVertSpacer)
}
}
}
@@ -290,7 +349,7 @@ private fun DisplayOwnerInformation(
nav: (String) -> Unit,
) {
LoadUser(baseUserHex = userHex, accountViewModel) {
Crossfade(it) {
CrossfadeIfEnabled(it, accountViewModel = accountViewModel) {
if (it != null) {
UserCompose(
baseUser = it,

View File

@@ -34,10 +34,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.WarningColor
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
@@ -76,7 +76,7 @@ fun RelayNameAndRemoveButton(
) {
Icon(
imageVector = Icons.Default.Cancel,
contentDescription = stringResource(id = R.string.remove),
contentDescription = stringRes(id = R.string.remove),
modifier =
Modifier
.padding(start = 10.dp)

View File

@@ -36,11 +36,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.warningColor
@@ -58,7 +58,7 @@ fun RelayStatusRow(
Icon(
imageVector = Icons.Default.Download,
contentDescription = stringResource(R.string.read_from_relay),
contentDescription = stringRes(R.string.read_from_relay),
modifier =
Modifier
.size(15.dp)
@@ -75,7 +75,7 @@ fun RelayStatusRow(
)
Text(
text = countToHumanReadableBytes(item.downloadCountInBytes),
text = countToHumanReadableBytes(item.relayStat.receivedBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -84,7 +84,7 @@ fun RelayStatusRow(
Icon(
imageVector = Icons.Default.Upload,
stringResource(R.string.write_to_relay),
stringRes(R.string.write_to_relay),
modifier =
Modifier
.size(15.dp)
@@ -101,7 +101,7 @@ fun RelayStatusRow(
)
Text(
text = countToHumanReadableBytes(item.uploadCountInBytes),
text = countToHumanReadableBytes(item.relayStat.sentBytes),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -110,7 +110,7 @@ fun RelayStatusRow(
Icon(
imageVector = Icons.Default.SyncProblem,
stringResource(R.string.errors),
stringRes(R.string.errors),
modifier =
Modifier
.size(15.dp)
@@ -124,7 +124,7 @@ fun RelayStatusRow(
},
),
tint =
if (item.errorCount > 0) {
if (item.relayStat.errorCounter > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
@@ -132,7 +132,7 @@ fun RelayStatusRow(
)
Text(
text = countToHumanReadable(item.errorCount, "errors"),
text = countToHumanReadable(item.relayStat.errorCounter, "errors"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
@@ -141,7 +141,7 @@ fun RelayStatusRow(
Icon(
imageVector = Icons.Default.DeleteSweep,
stringResource(R.string.spam),
stringRes(R.string.spam),
modifier =
Modifier
.size(15.dp)
@@ -153,17 +153,17 @@ fun RelayStatusRow(
R.string.spam_description,
)
scope.launch {
Toast.makeText(
context,
context.getString(R.string.spam),
Toast.LENGTH_SHORT,
)
.show()
Toast
.makeText(
context,
stringRes(context, R.string.spam),
Toast.LENGTH_SHORT,
).show()
}
},
),
tint =
if (item.spamCount > 0) {
if (item.relayStat.spamCounter > 0) {
MaterialTheme.colorScheme.warningColor
} else {
MaterialTheme.colorScheme.allGoodColor
@@ -171,7 +171,7 @@ fun RelayStatusRow(
)
Text(
text = countToHumanReadable(item.spamCount, "spam"),
text = countToHumanReadable(item.relayStat.spamCounter, "spam"),
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,

View File

@@ -35,8 +35,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relays.RelayStat
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -48,7 +49,7 @@ fun RelayUrlEditField(onNewRelay: (BasicRelaySetupInfo) -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Size10dp)) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.add_a_relay)) },
label = { Text(text = stringRes(R.string.add_a_relay)) },
modifier = Modifier.weight(1f),
value = url,
onValueChange = { url = it },
@@ -65,8 +66,7 @@ fun RelayUrlEditField(onNewRelay: (BasicRelaySetupInfo) -> Unit) {
Button(
onClick = {
if (url.isNotBlank() && url != "/") {
val addedWSS = RelayUrlFormatter.normalize(url)
onNewRelay(BasicRelaySetupInfo(addedWSS))
onNewRelay(BasicRelaySetupInfo(RelayUrlFormatter.normalize(url), RelayStat()))
url = ""
}
},
@@ -81,7 +81,7 @@ fun RelayUrlEditField(onNewRelay: (BasicRelaySetupInfo) -> Unit) {
},
),
) {
Text(text = stringResource(id = R.string.add), color = Color.White)
Text(text = stringRes(id = R.string.add), color = Color.White)
}
}
}

View File

@@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel

View File

@@ -38,13 +38,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
@@ -86,7 +86,7 @@ fun ChannelFabColumn(
containerColor = MaterialTheme.colorScheme.primary,
) {
Text(
text = stringResource(R.string.messages_new_message),
text = stringRes(R.string.messages_new_message),
color = Color.White,
textAlign = TextAlign.Center,
fontSize = Font12SP,
@@ -105,7 +105,7 @@ fun ChannelFabColumn(
containerColor = MaterialTheme.colorScheme.primary,
) {
Text(
text = stringResource(R.string.messages_create_public_chat),
text = stringRes(R.string.messages_create_public_chat),
color = Color.White,
textAlign = TextAlign.Center,
fontSize = Font12SP,
@@ -123,7 +123,7 @@ fun ChannelFabColumn(
) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = stringResource(R.string.messages_create_public_private_chat_desription),
contentDescription = stringRes(R.string.messages_create_public_private_chat_description),
modifier = Modifier.size(26.dp),
tint = Color.White,
)

View File

@@ -35,11 +35,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@@ -61,7 +61,7 @@ fun NewChannelButton(accountViewModel: AccountViewModel) {
) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = stringResource(R.string.new_channel),
contentDescription = stringRes(R.string.new_channel),
modifier = Modifier.size(26.dp),
tint = Color.White,
)

View File

@@ -33,13 +33,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
@Composable
@@ -73,7 +73,7 @@ fun NewCommunityNoteButton(
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
contentDescription = stringResource(id = R.string.new_community_note),
contentDescription = stringRes(id = R.string.new_community_note),
modifier = Modifier.size(26.dp),
tint = Color.White,
)

View File

@@ -46,7 +46,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -60,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -136,7 +136,7 @@ fun NewImageButton(
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
contentDescription = stringResource(id = R.string.new_short),
contentDescription = stringRes(id = R.string.new_short),
modifier = Modifier.size(26.dp),
tint = Color.White,
)
@@ -152,8 +152,7 @@ private fun ShowProgress(postViewModel: NewMediaModel) {
animateFloatAsState(
targetValue = postViewModel.uploadingPercentage.value,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
)
.value,
).value,
modifier =
Size55Modifier
.clip(CircleShape)

View File

@@ -33,11 +33,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
@Composable
@@ -59,7 +59,7 @@ fun NewNoteButton(
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
contentDescription = stringResource(R.string.new_post),
contentDescription = stringRes(R.string.new_post),
modifier = Modifier.size(26.dp),
tint = Color.White,
)

View File

@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.components
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -53,7 +52,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
@@ -69,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.model.ThemeType
import com.vitorpamplona.amethyst.service.CachedCashuProcessor
import com.vitorpamplona.amethyst.service.CashuToken
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.note.CashuIcon
import com.vitorpamplona.amethyst.ui.note.CopyIcon
@@ -76,6 +75,7 @@ import com.vitorpamplona.amethyst.ui.note.OpenInNewIcon
import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
@@ -106,7 +106,7 @@ fun CashuPreview(
}
}
Crossfade(targetState = cashuData, label = "CashuPreview") {
CrossfadeIfEnabled(targetState = cashuData, label = "CashuPreview", accountViewModel = accountViewModel) {
when (it) {
is GenericLoadable.Loaded<CashuToken> -> CashuPreview(it.loaded, accountViewModel)
is GenericLoadable.Error<CashuToken> ->
@@ -190,7 +190,7 @@ fun CashuPreview(
)
Text(
text = stringResource(R.string.cashu),
text = stringRes(R.string.cashu),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp),
@@ -200,7 +200,7 @@ fun CashuPreview(
HorizontalDivider(thickness = DividerThickness)
Text(
text = "${token.totalAmount} ${stringResource(id = R.string.sats)}",
text = "${token.totalAmount} ${stringRes(id = R.string.sats)}",
fontSize = 25.sp,
fontWeight = FontWeight.W500,
modifier =
@@ -239,7 +239,7 @@ fun CashuPreview(
Spacer(DoubleHorzSpacer)
Text(
stringResource(id = R.string.cashu_redeem_to_zap),
stringRes(id = R.string.cashu_redeem_to_zap),
color = Color.White,
fontSize = 16.sp,
)
@@ -256,7 +256,7 @@ fun CashuPreview(
startActivity(context, intent, null)
} catch (e: Exception) {
if (e is CancellationException) throw e
toast("Cashu", context.getString(R.string.cashu_no_wallet_found))
toast(stringRes(context, R.string.cashu), stringRes(context, R.string.cashu_no_wallet_found))
}
},
shape = QuoteBorder,
@@ -268,7 +268,7 @@ fun CashuPreview(
CashuIcon(Size20Modifier)
Spacer(DoubleHorzSpacer)
Text(
stringResource(id = R.string.cashu_redeem_to_cashu),
stringRes(id = R.string.cashu_redeem_to_cashu),
color = Color.White,
fontSize = 16.sp,
)
@@ -287,7 +287,7 @@ fun CashuPreview(
) {
CopyIcon(Size20Modifier, Color.White)
Spacer(DoubleHorzSpacer)
Text(stringResource(id = R.string.cashu_copy_token), color = Color.White, fontSize = 16.sp)
Text(stringRes(id = R.string.cashu_copy_token), color = Color.White, fontSize = 16.sp)
}
Spacer(modifier = StdHorzSpacer)
}
@@ -328,14 +328,14 @@ fun CashuPreviewNew(
)
Text(
text = stringResource(R.string.cashu),
text = stringRes(R.string.cashu),
fontSize = 12.sp,
modifier = Modifier.padding(start = 5.dp, bottom = 1.dp),
)
}
Text(
text = "${token.totalAmount} ${stringResource(id = R.string.sats)}",
text = "${token.totalAmount} ${stringRes(id = R.string.sats)}",
fontSize = 20.sp,
modifier = Modifier.padding(top = 5.dp),
)
@@ -378,7 +378,7 @@ fun CashuPreviewNew(
startActivity(context, intent, null)
} catch (e: Exception) {
if (e is CancellationException) throw e
toast("Cashu", context.getString(R.string.cashu_no_wallet_found))
toast(stringRes(context, R.string.cashu), stringRes(context, R.string.cashu_no_wallet_found))
}
},
shape = SmallishBorder,

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.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import com.vitorpamplona.amethyst.ui.theme.Size24dp
@Composable
fun ClickableBox(
modifier: Modifier,
onClick: () -> Unit,
content: @Composable () -> Unit,
) {
Box(
modifier.clickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = Size24dp),
onClick = onClick,
),
contentAlignment = Alignment.Center,
) {
content()
}
}

View File

@@ -470,13 +470,12 @@ fun CreateTextWithEmoji(
overflow: TextOverflow = TextOverflow.Clip,
modifier: Modifier = Modifier,
) {
val textColor =
color.takeOrElse { LocalTextStyle.current.color.takeOrElse { LocalContentColor.current } }
CustomEmojiChecker(
text,
tags,
onEmojiText = {
val textColor =
color.takeOrElse { LocalTextStyle.current.color.takeOrElse { LocalContentColor.current } }
val style =
LocalTextStyle.current
.merge(
@@ -486,12 +485,13 @@ fun CreateTextWithEmoji(
fontWeight = fontWeight,
fontSize = fontSize,
),
)
.toSpanStyle()
).toSpanStyle()
InLineIconRenderer(it, style, fontSize, maxLines, overflow, modifier)
},
onRegularText = {
val textColor =
color.takeOrElse { LocalTextStyle.current.color.takeOrElse { LocalContentColor.current } }
Text(
text = it,
color = textColor,
@@ -536,8 +536,7 @@ fun CreateTextWithEmoji(
fontWeight = fontWeight,
fontSize = fontSize,
),
)
.toSpanStyle()
).toSpanStyle()
}
InLineIconRenderer(it, style, fontSize, maxLines, overflow, modifier)
@@ -672,8 +671,7 @@ fun ClickableInLineIconRenderer(
} else {
null
}
}
.associate { it.first to it.second }
}.associate { it.first to it.second }
val annotatedText =
buildAnnotatedString {
@@ -757,8 +755,7 @@ fun InLineIconRenderer(
} else {
null
}
}
.associate { it.first to it.second }
}.associate { it.first to it.second }
val annotatedText =
remember {

View File

@@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
@Composable
fun ClickableUrl(
@@ -39,6 +40,8 @@ fun ClickableUrl(
ClickableText(
text = text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onClick = {
runCatching {
val doubleCheckedUrl = if (url.contains("://")) url else "https://$url"

View File

@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@@ -35,21 +34,27 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextDirection
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.encoders.LnWithdrawalUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun MayBeWithdrawal(lnurlWord: String) {
fun MayBeWithdrawal(
lnurlWord: String,
accountViewModel: AccountViewModel,
) {
var lnWithdrawal by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = lnurlWord) {
launch(Dispatchers.IO) { lnWithdrawal = LnWithdrawalUtil.findWithdrawal(lnurlWord) }
}
Crossfade(targetState = lnWithdrawal) {
CrossfadeIfEnabled(targetState = lnWithdrawal, accountViewModel = accountViewModel) {
if (it != null) {
ClickableWithdrawal(withdrawalString = it)
} else {
@@ -71,7 +76,7 @@ fun ClickableWithdrawal(withdrawalString: String) {
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = context.getString(R.string.error_dialog_pay_withdraw_error),
title = stringRes(context, R.string.error_dialog_pay_withdraw_error),
textContent = showErrorMessageDialog ?: "",
onDismiss = { showErrorMessageDialog = null },
)

View File

@@ -40,11 +40,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.ExpandableTextCutOffCalculator
import com.vitorpamplona.amethyst.ui.note.getGradient
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
@@ -64,6 +64,7 @@ fun ExpandableRichTextViewer(
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
id: String,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -99,6 +100,7 @@ fun ExpandableRichTextViewer(
modifier.align(Alignment.TopStart),
tags,
backgroundColor,
callbackUri,
accountViewModel,
nav,
)
@@ -108,7 +110,8 @@ fun ExpandableRichTextViewer(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier =
Modifier.align(Alignment.BottomCenter)
Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.background(getGradient(backgroundColor)),
) {
@@ -133,6 +136,6 @@ fun ShowMoreButton(onClick: () -> Unit) {
),
contentPadding = ButtonPadding,
) {
Text(text = stringResource(R.string.show_more), color = Color.White)
Text(text = stringRes(R.string.show_more), color = Color.White)
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
/**
* Create and return a new [Painter] that wraps [painter] with its [alpha], [colorFilter], or [onDraw] overwritten.
*/
fun forwardingPainter(
painter: Painter,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
onDraw: DrawScope.(ForwardingDrawInfo) -> Unit = DefaultOnDraw,
): Painter = ForwardingPainter(painter, alpha, colorFilter, onDraw)
data class ForwardingDrawInfo(
val painter: Painter,
val alpha: Float,
val colorFilter: ColorFilter?,
)
private class ForwardingPainter(
private val painter: Painter,
private var alpha: Float,
private var colorFilter: ColorFilter?,
private val onDraw: DrawScope.(ForwardingDrawInfo) -> Unit,
) : Painter() {
private var info = newInfo()
override val intrinsicSize get() = painter.intrinsicSize
override fun applyAlpha(alpha: Float): Boolean {
if (alpha != DefaultAlpha) {
this.alpha = alpha
this.info = newInfo()
}
return true
}
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
if (colorFilter == null) {
this.colorFilter = colorFilter
this.info = newInfo()
}
return true
}
override fun DrawScope.onDraw() = onDraw(info)
private fun newInfo() = ForwardingDrawInfo(painter, alpha, colorFilter)
}
private val DefaultOnDraw: DrawScope.(ForwardingDrawInfo) -> Unit = { info ->
with(info.painter) {
draw(size, info.alpha, info.colorFilter)
}
}

View File

@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -44,7 +43,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
@@ -54,8 +52,11 @@ import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.hashtags.Lightning
import com.vitorpamplona.amethyst.service.lnurl.CachedLnInvoiceParser
import com.vitorpamplona.amethyst.service.lnurl.InvoiceAmount
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
@@ -82,9 +83,12 @@ fun LoadValueFromInvoice(
}
@Composable
fun MayBeInvoicePreview(lnbcWord: String) {
fun MayBeInvoicePreview(
lnbcWord: String,
accountViewModel: AccountViewModel,
) {
LoadValueFromInvoice(lnbcWord = lnbcWord) { invoiceAmount ->
Crossfade(targetState = invoiceAmount, label = "MayBeInvoicePreview") {
CrossfadeIfEnabled(targetState = invoiceAmount, label = "MayBeInvoicePreview", accountViewModel = accountViewModel) {
if (it != null) {
InvoicePreview(it.invoice, it.amount)
} else {
@@ -108,7 +112,7 @@ fun InvoicePreview(
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = context.getString(R.string.error_dialog_pay_invoice_error),
title = stringRes(context, R.string.error_dialog_pay_invoice_error),
textContent = showErrorMessageDialog ?: "",
onDismiss = { showErrorMessageDialog = null },
)
@@ -143,7 +147,7 @@ fun InvoicePreview(
)
Text(
text = stringResource(R.string.lightning_invoice),
text = stringRes(R.string.lightning_invoice),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp),
@@ -154,7 +158,7 @@ fun InvoicePreview(
amount?.let {
Text(
text = "$it ${stringResource(id = R.string.sats)}",
text = "$it ${stringRes(id = R.string.sats)}",
fontSize = 25.sp,
fontWeight = FontWeight.W500,
modifier =
@@ -176,7 +180,7 @@ fun InvoicePreview(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(text = stringResource(R.string.pay), color = Color.White, fontSize = 20.sp)
Text(text = stringRes(R.string.pay), color = Color.White, fontSize = 20.sp)
}
}
}

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