Compare commits

...
275 Commits
Author SHA1 Message Date
Vitor Pamplona d738e5cbaf v0.79.0 2023-09-29 16:27:04 -04:00
Vitor Pamplona 386b6db5ff Fixes relay type loading from the default list when they are not present in the user's account yet. 2023-09-29 16:23:42 -04:00
Vitor Pamplona 47d57e5a44 Only adds search relays if none of the current relays are active for search. 2023-09-29 16:09:11 -04:00
Vitor Pamplona 92068ca64f Fixing the inclusion of search relays 2023-09-29 16:03:59 -04:00
Vitor Pamplona 349510e900 Updates gradle plugin 2023-09-29 15:48:24 -04:00
Vitor PamplonaandGitHub 948d0f4bea Merge pull request #627 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-29 14:41:14 -04:00
Crowdin Bot b9a5586167 New Crowdin translations by GitHub Action 2023-09-29 18:30:00 +00:00
Vitor Pamplona 54edddbd2d Migrates to Material 3 2023-09-29 13:57:10 -04:00
Vitor PamplonaandGitHub b9c83426dc Merge pull request #624 from greenart7c3/main
add settings to load profile pictures only on wifi,never or always
2023-09-27 14:30:44 -04:00
greenart7c3 40dae875b8 Merge branch 'main' of https://github.com/greenart7c3/amethyst 2023-09-27 10:48:53 -03:00
greenart7c3 e762ad5e18 add settings to load profile pictures 2023-09-27 10:48:26 -03:00
Vitor PamplonaandGitHub 9b1bd2127f Merge pull request #623 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-26 11:07:27 -04:00
Crowdin Bot 88701a97c7 New Crowdin translations by GitHub Action 2023-09-26 15:03:50 +00:00
Vitor Pamplona 600c927594 v0.78.2 2023-09-26 11:01:42 -04:00
Vitor Pamplona 1387a0f62b Adds note to make sure we know the callback of a prepare video runs in the main thread. 2023-09-26 10:56:28 -04:00
Vitor Pamplona e3df94338f Fixes the access to playback state from the wrong thread 2023-09-26 10:55:31 -04:00
Vitor Pamplona ee466aa1e4 v0.78.1 2023-09-25 18:28:48 -04:00
Vitor Pamplona 9704d61a7a more remembers 2023-09-25 18:28:35 -04:00
Vitor Pamplona 69d7e82b71 Increasing video cache from 90 to 150MB 2023-09-25 17:36:57 -04:00
Vitor Pamplona 288d80d163 only updates reports for a different note 2023-09-25 17:26:41 -04:00
Vitor Pamplona b1edf4e934 Improving the Stability of VideoView 2023-09-25 17:09:36 -04:00
Vitor Pamplona af9b0b444c Fix for not clearing the new post message. It adds a delay to allow the composer to save the viewModel after calling cancel 2023-09-25 15:29:13 -04:00
Vitor Pamplona a3865e68bd Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-25 14:27:45 -04:00
Vitor PamplonaandGitHub ed70585e90 Merge pull request #620 from jiftechnify/fix-relay-content-link
Fix contact link in relay information dialog
2023-09-25 14:27:38 -04:00
Vitor PamplonaandGitHub f707fd007b Merge pull request #622 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-25 14:26:39 -04:00
Crowdin Bot d92e30dba3 New Crowdin translations by GitHub Action 2023-09-25 18:22:35 +00:00
Vitor PamplonaandGitHub 4afa5c1789 Merge pull request #621 from greenart7c3/main
add notifications for users of amber
2023-09-25 14:21:10 -04:00
Vitor Pamplona e89e8e5d01 moves mark as read to the AccountViewModel scope. 2023-09-25 14:19:27 -04:00
greenart7c3 52f600485b add notifications for users of amber 2023-09-25 14:55:44 -03:00
jiftechnify b5a0d65f3c fix contact link in relay information dialog 2023-09-26 00:49:00 +09:00
Vitor Pamplona fc4433e7ae Adds NIP19 parser to the AccountViewModel thread scope 2023-09-25 11:35:39 -04:00
Vitor Pamplona f2b6a9bedb Avoids creating nip94 and nip95 events with blank alts 2023-09-25 11:12:02 -04:00
Vitor Pamplona 35f0b1291f Performance Improvements to PreviewCards 2023-09-25 11:04:53 -04:00
Vitor Pamplona 045699ecd9 Disable swipe to change page in the hope page 2023-09-25 10:28:08 -04:00
Vitor Pamplona 74a2b46508 Forces the presence of the name field in kind 1 as per https://github.com/nostr-protocol/nips/pull/794 2023-09-25 10:11:25 -04:00
Vitor Pamplona feaa594e52 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-25 09:55:28 -04:00
Vitor Pamplona d966541ea7 Updating dependencies 2023-09-25 09:54:58 -04:00
Vitor PamplonaandGitHub ecfc287871 Merge pull request #619 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-25 08:50:35 -04:00
Crowdin Bot c10461126a New Crowdin translations by GitHub Action 2023-09-25 12:45:27 +00:00
Vitor PamplonaandGitHub 20903cefc8 Merge pull request #618 from davotoula/main
Updated translations for cs/de/sv
2023-09-25 08:44:11 -04:00
David Kaspar e1caf40c3a Updated translations for cs/de/sv 2023-09-25 11:13:05 +02:00
Vitor Pamplona 6e70034758 v0.78.0 2023-09-24 18:40:15 -04:00
Vitor Pamplona ee30ff28f6 UI Improvements for Hidden Words 2023-09-24 18:36:43 -04:00
Vitor Pamplona 03654b5ebd Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-24 18:14:44 -04:00
Vitor Pamplona 7071174aed BugFix for restarting the relay connection when switching accounts after Amber's integration. 2023-09-24 18:12:50 -04:00
Vitor PamplonaandGitHub 2c27531056 Merge pull request #617 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-24 15:27:58 -04:00
Crowdin Bot 57d5556728 New Crowdin translations by GitHub Action 2023-09-24 19:13:23 +00:00
Vitor Pamplona f4da2ae6be Mute keywords 2023-09-24 15:10:50 -04:00
Vitor Pamplona 57dfe3af8c Reverting to have videos starting from the IO thread. 2023-09-24 10:35:51 -04:00
Vitor Pamplona 618d14a965 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-23 14:19:41 -04:00
Vitor PamplonaandGitHub ee95942289 Merge pull request #615 from greenart7c3/main
only show backup keys if account has a private key
2023-09-23 12:11:03 -04:00
Vitor Pamplona c6c15c3ec7 Adding an option to avoid immersive scrolling 2023-09-23 12:10:17 -04:00
greenart7c3 ed4c51b664 Merge branch 'main' of https://github.com/greenart7c3/amethyst 2023-09-23 12:40:17 -03:00
greenart7c3 b3f39434ee only show backup keys if account has a private key 2023-09-23 12:24:17 -03:00
Vitor Pamplona 30ded17581 v0.77.8 2023-09-22 16:57:14 -04:00
Vitor Pamplona 19557bf358 Filters zap payments outside the established min-max parameters on polls. 2023-09-22 16:50:25 -04:00
Vitor Pamplona bdc2cbbb99 Adds Mutiny button 2023-09-22 16:23:19 -04:00
Vitor Pamplona 82e2d15aaf Moves Live streaming from the top bar to the screen to avoid cancelling the video on scrolling. 2023-09-22 14:51:21 -04:00
Vitor Pamplona 6aff31886e Fixes website URLs without schema (http://, https://, etc) 2023-09-22 14:37:51 -04:00
Vitor Pamplona 7cc089f0ee Video playback creation must be run in the main thread :( 2023-09-22 14:19:29 -04:00
Vitor Pamplona 6c025f7274 Speeding up the display of unused hashtags 2023-09-22 14:19:07 -04:00
Vitor Pamplona d5220324c4 Turns out the lifecycle owner changes quite a bit and thus it's a requirement to make it the key of a Disposable Effect. 2023-09-22 13:46:30 -04:00
Vitor PamplonaandGitHub a0308bd3a1 Merge pull request #614 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-22 12:18:34 -04:00
Crowdin Bot 856b4ca2c1 New Crowdin translations by GitHub Action 2023-09-22 15:55:45 +00:00
Vitor PamplonaandGitHub 8abfd10f0c Merge pull request #613 from greenart7c3/main
update pt-br translations
2023-09-22 11:54:13 -04:00
greenart7c3andGitHub 1bc4d33bb7 Merge branch 'vitorpamplona:main' into main 2023-09-22 11:27:48 -03:00
greenart7c3 35c67f692a update pt-br translations 2023-09-22 11:11:47 -03:00
Vitor PamplonaandGitHub a319902cf7 Merge pull request #612 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-22 09:47:54 -04:00
Crowdin Bot 2bc8d26b4f New Crowdin translations by GitHub Action 2023-09-22 13:40:33 +00:00
Vitor PamplonaandGitHub 402da58391 Merge pull request #611 from greenart7c3/main
fix crash when editing account
2023-09-22 09:39:28 -04:00
Vitor PamplonaandGitHub b533afe27f Merge pull request #610 from davotoula/main
Updated translations for cs/de/sv
2023-09-22 09:39:05 -04:00
greenart7c3 6a4afb45dc fix crash when editing account 2023-09-22 05:13:47 -03:00
David Kaspar 73a718e6c7 Updated translations for cs/de/sv 2023-09-22 09:29:58 +02:00
Vitor Pamplona 140f6e3318 Refactoring to move markdown parser away from the Compose Class 2023-09-21 16:57:43 -04:00
Vitor Pamplona 073add9e71 Reduce padding of custom emojis 2023-09-21 14:30:56 -04:00
Vitor Pamplona 35ecf1f7a7 v0.77.7 2023-09-21 14:15:25 -04:00
Vitor Pamplona 71db62e5a5 Resets the bar visibility when switching screens. 2023-09-21 14:14:40 -04:00
Vitor Pamplona ff82b52a5b Removes the need for the payment popup for only one invoice. 2023-09-21 14:03:38 -04:00
Vitor Pamplona 18e81de26d v0.77.6 2023-09-21 10:38:02 -04:00
Vitor Pamplona 27f37fc158 Removes the flickering on chat screens when the bottom bar disappears. 2023-09-21 10:32:06 -04:00
Vitor Pamplona 6638ae1d7b no need for coroutines on nav anymore 2023-09-21 09:20:11 -04:00
Vitor Pamplona 42094c2a70 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-21 09:09:35 -04:00
Vitor Pamplona 932abdd412 Fixes video keep playing bug when pressing the bottom bar twice. 2023-09-21 09:09:24 -04:00
Vitor PamplonaandGitHub b4bb6f2b15 Merge pull request #608 from davotoula/main
"show more" text folding enhancement
2023-09-21 08:19:02 -04:00
David Kaspar 4c7b523de1 Merge remote-tracking branch 'origin/show_more_enhancement' into show_more_enhancement 2023-09-21 08:47:40 +02:00
David Kaspar 59608e9acc Combine new line limit with previous character checking (use the smallest) 2023-09-21 08:47:09 +02:00
David Kaspar 77afedb2d0 Introduced line limit in addition to existing character limit for "show more" folding of text 2023-09-21 08:47:09 +02:00
David Kaspar ccb2c626a7 Combine new line limit with previous character checking (use the smallest) 2023-09-21 08:27:16 +02:00
David Kaspar 9b6eec5fdd Introduced line limit in addition to existing character limit for "show more" folding of text 2023-09-21 08:11:25 +02:00
Vitor Pamplona 02786fb7d3 v0.77.5 2023-09-20 21:22:21 -04:00
Vitor Pamplona 384594ba5c - Inverts the scroll to hide for channels and chats
- Awaits half of the bar to change visibility
2023-09-20 21:21:13 -04:00
Vitor Pamplona dab02f2cbd v0.77.4 2023-09-20 19:05:00 -04:00
Vitor Pamplona 978f13fbf3 Activates sliding effect for all screens 2023-09-20 18:59:49 -04:00
Vitor Pamplona 831386a656 Using Slide in and out instead of crossfade for the top and bottom bars. 2023-09-20 18:57:11 -04:00
Vitor PamplonaandGitHub 8b7406e25a Merge pull request #607 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-20 18:41:11 -04:00
Crowdin Bot 9e7fed6646 New Crowdin translations by GitHub Action 2023-09-20 22:19:01 +00:00
Vitor PamplonaandGitHub 89d88326e8 Merge pull request #606 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-20 18:18:08 -04:00
Vitor PamplonaandGitHub f18dd02161 Merge pull request #605 from greenart7c3/main
immersive scrolling on home feed
2023-09-20 18:17:56 -04:00
Crowdin Bot e00db19485 New Crowdin translations by GitHub Action 2023-09-20 22:14:39 +00:00
Vitor Pamplona a706b71d7c Moves reaction calls to the viewModelScope 2023-09-20 18:12:41 -04:00
greenart7c3 a83b26cc6f immersive scrolling on home feed 2023-09-20 18:18:56 -03:00
Vitor Pamplona dc73843447 Fixes OK string 2023-09-20 16:43:57 -04:00
Vitor Pamplona 3e6aa2b943 Protects against null contact lists 2023-09-20 16:32:38 -04:00
Vitor Pamplona 909d3f4fbe Migrates alt descriptions from .content to alt tag. 2023-09-20 16:18:21 -04:00
Vitor Pamplona fb0748f4a7 v0.77.3 2023-09-20 15:12:23 -04:00
Vitor Pamplona 1ec4823122 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-20 15:11:56 -04:00
Vitor Pamplona cce5e0bf21 v0.77.2 2023-09-20 15:11:51 -04:00
Vitor Pamplona 0006595786 Quick fix on zap notifications 2023-09-20 15:11:35 -04:00
Vitor PamplonaandGitHub 6b884f645e Merge pull request #604 from greenart7c3/main
Change nav bar color according to the theme
2023-09-20 14:58:18 -04:00
greenart7c3 946723a80e change nav bar color 2023-09-20 15:42:21 -03:00
Vitor Pamplona 791debbaca v0.77.1 2023-09-20 14:18:07 -04:00
Vitor Pamplona 3bb5a4e50d Fixing F-droid flavor 2023-09-20 14:01:51 -04:00
Vitor Pamplona 3efa3a68fe Adds communities, hashtags and geohashes to the lists on the top navigation bar. 2023-09-20 12:23:50 -04:00
Vitor Pamplona dc314a94ff Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-20 09:38:00 -04:00
Vitor Pamplona 11b062c41f Moves zap decryption to run in a group, avoiding multiple co-routines per zap 2023-09-20 09:37:56 -04:00
Vitor Pamplona faeb2a3894 Removes the follow unfollow button from the Zap to User Compose 2023-09-20 09:37:23 -04:00
Vitor PamplonaandGitHub b1f45a346d Merge pull request #603 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-20 08:34:26 -04:00
Crowdin Bot 0b16de6a85 New Crowdin translations by GitHub Action 2023-09-20 12:32:50 +00:00
Vitor PamplonaandGitHub 66d4b75373 Merge pull request #602 from greenart7c3/main
Login improvements
2023-09-20 08:31:28 -04:00
greenart7c3 3b376a615c disable buttons if terms not accepted, fix a crash when login with amber 2023-09-20 07:46:31 -03:00
Vitor Pamplona c8da570c89 Fixes IO Calls 2023-09-19 17:44:41 -04:00
Vitor Pamplona 38f4a5b7fc Groups notifications by day first. 2023-09-19 17:32:57 -04:00
Vitor Pamplona b03ff36cf3 Improvements to the rendering of long form content. 2023-09-19 17:01:33 -04:00
Vitor Pamplona d145938652 Fixes blank summaries 2023-09-19 16:51:14 -04:00
Vitor Pamplona 1993388d9f Darker Boost Username 2023-09-19 16:47:30 -04:00
Vitor Pamplona 15e7540b26 Code clean up 2023-09-19 16:43:57 -04:00
Vitor Pamplona c2beaf5f80 Starting a Refactoring of LocalCache away from a Singleton instance. 2023-09-19 15:26:16 -04:00
Vitor Pamplona 5d395419ff Sets up default reactions to be Rocket, Hugs, Watching and Laghuing 2023-09-19 14:58:00 -04:00
Vitor Pamplona f672a51470 Removes old migration from the old preferences. 2023-09-18 17:20:46 -04:00
Vitor Pamplona f741239e23 Updates Google Play services 2023-09-18 17:15:17 -04:00
Vitor Pamplona 3442194b01 Creates Daniel's idea for reposted profile icons 2023-09-18 17:07:17 -04:00
Vitor Pamplona 0b025d7679 Adds a ZapSplit view to MasterNodes in Thread 2023-09-18 16:31:05 -04:00
Vitor Pamplona 227de74317 v0.77.0 2023-09-18 13:36:51 -04:00
Vitor Pamplona 0b9fced8bc Quick refactoring to abstract the name Amber for any external signers. 2023-09-18 13:36:36 -04:00
Vitor Pamplona 274e4f7498 Fixes merge issue on NIP24 TextNotes 2023-09-18 11:23:36 -04:00
Vitor PamplonaandGitHub f6496b5e7e Merge pull request #601 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-18 11:18:57 -04:00
Crowdin Bot ee5ff1845f New Crowdin translations by GitHub Action 2023-09-18 15:18:18 +00:00
Vitor PamplonaandGitHub c84e835ce5 Merge pull request #543 from greenart7c3/amber
Add Amber support
2023-09-18 11:16:55 -04:00
Vitor PamplonaandGitHub cdb5be2fe9 Merge branch 'main' into amber 2023-09-18 11:16:47 -04:00
Vitor Pamplona 2d0c356aa5 v0.76.2 2023-09-18 10:53:44 -04:00
Vitor PamplonaandGitHub 0a92c253b8 Merge pull request #600 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-18 10:39:22 -04:00
Crowdin Bot 92347863bc New Crowdin translations by GitHub Action 2023-09-18 14:38:52 +00:00
Vitor Pamplona a146ec5e1a Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-18 10:36:21 -04:00
Vitor Pamplona 83be43e94e Removes the ZapSplit display when the weights are zero or were incorrectly created. 2023-09-18 10:36:05 -04:00
greenart7c3 d726b3b5b5 fix createZapRequestFor userhex 2023-09-18 11:27:05 -03:00
Vitor PamplonaandGitHub c845aac8e5 Merge pull request #599 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-17 12:03:08 -04:00
Crowdin Bot 1cdf114fff New Crowdin translations by GitHub Action 2023-09-17 15:38:29 +00:00
Vitor Pamplona 4d1a99d076 Adds NIP24 Kind1 create function 2023-09-17 11:32:35 -04:00
Vitor Pamplona b1debd9879 AnimatedVisibility seems faster than Crossfade 2023-09-17 11:31:24 -04:00
Vitor Pamplona 3843917bd1 Makes sure the account is writeable before signing Auth for notifications 2023-09-16 15:33:42 -04:00
greenart7c3 2fa4281b6f fix sending messages 2023-09-16 09:11:25 -03:00
greenart7c3 c42944e15b removed AmberUtils.signEvent 2023-09-16 08:49:46 -03:00
greenart7c3 9f9f158709 fix zap splits and show message if user uninstall amber 2023-09-16 08:23:56 -03:00
greenart7c3andGitHub 3a753f0d5d Merge branch 'main' into amber 2023-09-16 08:03:54 -03:00
greenart7c3 13c94112d4 fix private zaps 2023-09-16 07:56:14 -03:00
greenart7c3 dd46f12a05 change amber from debug to release 2023-09-15 21:51:01 -03:00
greenart7c3 9a648d7e84 send the logged in npub to amber 2023-09-15 21:35:32 -03:00
greenart7c3 614dbab1fd refactor some duplicated code 2023-09-15 20:51:13 -03:00
greenart7c3 3d80ee1984 fix decryption of dms whe new message is send 2023-09-15 20:46:29 -03:00
Vitor Pamplona 536fb49bc3 0.76.1 2023-09-15 17:15:54 -04:00
Vitor Pamplona edd93141bc No need for the @ when adding users to the Split 2023-09-15 17:13:35 -04:00
Vitor Pamplona 20c2d19a9c Refining the Payment via Intent 2023-09-15 17:08:19 -04:00
Vitor PamplonaandGitHub 17b2fe9cd8 Merge pull request #596 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-15 14:22:41 -04:00
Crowdin Bot 134e7cee0c New Crowdin translations by GitHub Action 2023-09-15 16:59:32 +00:00
Vitor Pamplona 108ddffcc7 formatting 2023-09-15 12:56:50 -04:00
Vitor Pamplona 20e76daff8 Adds zap splits 2023-09-15 12:56:24 -04:00
greenart7c3 6876a7f0d2 zaps in background 2023-09-15 13:53:05 -03:00
greenart7c3andGitHub 87dbf86129 Merge branch 'main' into amber 2023-09-15 13:18:43 -03:00
greenart7c3 175d1ae33b fix zap dialog not closing 2023-09-15 12:58:58 -03:00
greenart7c3 bf9d318209 use content resolvers with amber 2023-09-15 11:56:07 -03:00
Vitor PamplonaandGitHub 7bee44c5ad Merge pull request #595 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-14 14:01:54 -04:00
Crowdin Bot a32e90aed0 New Crowdin translations by GitHub Action 2023-09-14 17:48:43 +00:00
Vitor Pamplona 310db1008e Fixes bug when verifying the checksum of LNURLs (bech32) in uppercase. 2023-09-14 13:46:29 -04:00
greenart7c3 21560d098e fix upload image 2023-09-13 19:15:00 -03:00
greenart7c3 33f7c5c82e deleted intentutils 2023-09-13 18:06:50 -03:00
greenart7c3 52f17e9f46 nwc support with amber 2023-09-13 16:23:56 -03:00
greenart7c3 1e7ffa365b change relay auth to use signevent 2023-09-13 16:08:38 -03:00
greenart7c3 e00fc83e5c fix reaction row and zaps 2023-09-13 15:54:44 -03:00
greenart7c3andGitHub a2fcb3539e Merge branch 'main' into amber 2023-09-13 13:15:45 -03:00
Vitor Pamplona cb69d056b8 Allowing playback to save any position and not only after 5 seconds. 2023-09-13 12:12:47 -04:00
Vitor Pamplona e7f950cd32 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-13 11:45:27 -04:00
Vitor Pamplona 3b2686ffeb Removes potential bug when the list contains an e tag with an a reference 2023-09-13 11:45:17 -04:00
Vitor PamplonaandGitHub 2387a609df Merge pull request #593 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-13 10:33:47 -04:00
Crowdin Bot 6457a84f4b New Crowdin translations by GitHub Action 2023-09-13 14:30:48 +00:00
Vitor Pamplona dee231d43a Moves deletion and report to be the last item on the dropdown menu: https://image.nostr.build/709c4f2753cf6a40f9f9322379394d09354acb7e88502b64be348e4a3030734b.png 2023-09-13 10:27:37 -04:00
greenart7c3 8c4af4ac85 only decrypt giftwrap in the dms screen 2023-09-13 11:10:10 -03:00
greenart7c3 58cf0d9f5d fix freeze when going to dms screen 2023-09-13 10:08:43 -03:00
greenart7c3 de5c0d4681 do not try decrypt private zaps for now 2023-09-13 09:32:24 -03:00
greenart7c3 780832d785 fix block/unblock 2023-09-13 09:22:34 -03:00
greenart7c3 b8808a17b5 fix bookmarks decryption 2023-09-13 08:38:20 -03:00
greenart7c3 5001e58c2b improve giftwrap decryption 2023-09-13 08:04:51 -03:00
greenart7c3 95514b66ea improve giftwrap decryption 2023-09-13 08:01:46 -03:00
greenart7c3 90c6d6c8f8 fix login with amber 2023-09-13 06:42:37 -03:00
Vitor Pamplona 88d2ce3ad0 Updating dependencies 2023-09-12 17:53:18 -04:00
Vitor PamplonaandGitHub 484f3a6842 Merge pull request #590 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-12 08:32:16 -04:00
Crowdin Bot 866caf8bc5 New Crowdin translations by GitHub Action 2023-09-12 12:12:41 +00:00
Vitor PamplonaandGitHub be9e3c3bf7 Merge pull request #589 from davotoula/main
Updated translations for cs/de/sv
2023-09-12 08:11:02 -04:00
David Kaspar 49a3cb1978 Updated translations for cs/de/sv 2023-09-12 11:41:43 +02:00
greenart7c3 c3ddc4f7ad fix release build not working 2023-09-11 18:12:55 -03:00
greenart7c3 ff4d704bde add support for private zaps 2023-09-11 15:30:00 -03:00
greenart7c3 a773be12c4 decrypt LnZapRequestEvent 2023-09-11 11:52:53 -03:00
greenart7c3andGitHub 7ff6f16df0 Merge branch 'main' into amber 2023-09-11 07:20:03 -03:00
Vitor PamplonaandGitHub d9053f580f Merge pull request #586 from OrigiSize/patch-1
Minor README.md edits
2023-09-09 19:37:57 -04:00
OrigiSizeandGitHub fee2764338 Minor README.md edits
Fixing grammar and flow
2023-09-09 12:33:09 -07:00
Vitor Pamplona 61cb4fdd74 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-09-09 14:42:59 -04:00
Vitor PamplonaandGitHub d89869ebff Merge pull request #585 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-09 14:40:45 -04:00
Vitor Pamplona 8518819dfa Forces valid hexes for users and notes 2023-09-09 14:39:41 -04:00
Crowdin Bot 0a8923be6b New Crowdin translations by GitHub Action 2023-09-09 18:34:33 +00:00
Vitor Pamplona cda821e5af Stops accepting space as a Valid hex char and requires an even number of chars (padding) 2023-09-09 14:33:19 -04:00
Vitor Pamplona be2402702f Forces the ZapRequest to exist when processing the LnZapEvent. 2023-09-09 14:32:27 -04:00
Vitor Pamplona d723a8ab5c Refactors the split between nip19 and the rest of the text. 2023-09-09 14:26:20 -04:00
Vitor Pamplona 5c882c81c8 Account is never null here. 2023-09-09 14:24:29 -04:00
Vitor PamplonaandGitHub 24d2709564 Merge pull request #584 from atrifat/move-broadcast-button-away
Fix vitorpamplona/amethyst#550 - change broadcast button position
2023-09-08 20:54:46 -04:00
Rif'at Ahdi R ce15bc46a7 Fix vitorpamplona/amethyst#550 - change broadcast button position 2023-09-09 08:24:17 +08:00
Vitor PamplonaandGitHub de4bc30730 Merge pull request #583 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-09-08 16:19:48 -04:00
Crowdin Bot 64f6811c00 New Crowdin translations by GitHub Action 2023-09-08 20:18:47 +00:00
greenart7c3andGitHub 36a3c4123c Merge branch 'main' into amber 2023-09-08 15:48:13 -03:00
greenart7c3 6303573dd9 support for sending and receiving nip 44 dms 2023-09-06 18:53:11 -03:00
greenart7c3 a7aa3705ef fix crash on reaction 2023-09-06 10:43:59 -03:00
greenart7c3 a64c941f36 add cache to decrypted dms 2023-09-06 09:21:55 -03:00
greenart7c3 59cde5699e add support for status 2023-09-04 18:28:11 -03:00
greenart7c3andGitHub bd836a9580 Merge branch 'main' into amber 2023-09-04 18:09:29 -03:00
greenart7c3 90aade338c sign event in background 2023-09-04 18:06:09 -03:00
greenart7c3 6d691c4741 add support for reports 2023-09-01 13:32:39 -03:00
greenart7c3 bbba27752d support for public and anon zaps 2023-09-01 12:25:42 -03:00
greenart7c3 d9cf090cc9 add support for nip94 2023-09-01 10:46:50 -03:00
greenart7c3 46effa572c refactor default values in signEvent variables 2023-09-01 10:18:49 -03:00
greenart7c3andGitHub f76ab01c11 Merge branch 'main' into amber 2023-09-01 05:12:29 -03:00
greenart7c3 d1cb699027 support for nip95 2023-08-30 16:40:11 -03:00
greenart7c3 9ee1250f3a unblock users 2023-08-30 14:15:20 -03:00
greenart7c3 7a68cf867e block users using amber 2023-08-30 11:54:36 -03:00
greenart7c3 bc8fa00608 move openAmber to AmberUtils 2023-08-30 08:29:56 -03:00
greenart7c3 329d806ffd decrypt dms of notifications 2023-08-30 08:07:45 -03:00
greenart7c3andGitHub e040d0f971 Merge branch 'main' into amber 2023-08-30 07:03:55 -03:00
greenart7c3 c223c1372b add support for amber in the follow dropdown menu 2023-08-28 14:31:20 -03:00
greenart7c3 a450dcbac1 add support for add/remove public bookmarks 2023-08-28 11:58:48 -03:00
greenart7c3 ee5fbce2ce refactor mainactivity to use IntentUtils.start 2023-08-28 09:56:12 -03:00
greenart7c3 559b88be18 fix build after merge 2023-08-28 07:18:04 -03:00
greenart7c3andGitHub dfbc5fa556 Merge branch 'main' into amber 2023-08-28 07:06:52 -03:00
greenart7c3 df1a017eb3 refactor PackageUtils.isAmberInstalled to account.loggedinwithamber 2023-08-25 11:51:22 -03:00
greenart7c3 b2b7799a0f add support for deletion events 2023-08-25 09:29:13 -03:00
greenart7c3 e817f94045 add/remove private bookmarks 2023-08-25 08:51:23 -03:00
greenart7c3 46571a6029 decrypt private bookmark 2023-08-25 06:56:49 -03:00
greenart7c3 4f4d689f5a fix build, use keyPair in textnoteevent, check if loggedInWithAmber in newpostview 2023-08-25 05:45:39 -03:00
greenart7c3andGitHub a0308938de Merge branch 'main' into amber 2023-08-25 05:27:14 -03:00
greenart7c3 b10e875941 created loggedInWithAmber in accountViewModel, changed some events to use keypair 2023-08-23 15:56:31 -03:00
greenart7c3 632400f52c add login with amber button 2023-08-23 13:43:25 -03:00
greenart7c3 b63479c45f fix build 2023-08-23 07:33:49 -03:00
greenart7c3andGitHub 7ea6495f28 Merge branch 'main' into amber 2023-08-23 07:28:58 -03:00
greenart7c3 453e63025d fix crash in PushNotificationUtils when theres no google play services 2023-08-21 15:58:01 -03:00
greenart7c3 17ecc98e1e Merge branch 'main' into amber 2023-08-21 14:59:20 -03:00
greenart7c3 f9272f5cfc implemented amber support for auth event and the notifications 2023-08-21 14:48:11 -03:00
greenart7c3 348e07a307 fix missing navScrollToTop parameter 2023-08-21 09:00:45 -03:00
greenart7c3andGitHub 2009f5bd7b Merge branch 'main' into amber 2023-08-21 08:50:56 -03:00
greenart7c3 7ea92bc068 send dms using amber 2023-08-18 10:57:17 -03:00
greenart7c3 3b07b5d8dd Refactor SignerDialog to use the rawJson and type 2023-08-18 07:55:14 -03:00
greenart7c3 f06e79cfd3 Merge branch 'main' into amber 2023-08-18 06:33:02 -03:00
greenart7c3andGitHub 584ceb1f0f Merge branch 'main' into amber 2023-08-16 17:21:47 -03:00
greenart7c3 8053119d42 decrypt dms using amber 2023-08-16 16:39:42 -03:00
greenart7c3 b8e97525eb add signer support whem replying 2023-08-16 14:34:11 -03:00
greenart7c3 66b1e4b15d add signer dialog to communities and public chats 2023-08-14 16:34:40 -03:00
greenart7c3 e6f6f64512 add signer dialog in the relay screen 2023-08-14 13:53:10 -03:00
greenart7c3 d2eea9f0ae fix merge 2023-08-14 10:51:14 -03:00
greenart7c3 012fdc8cc6 add signer support in the user metadata screen 2023-08-14 10:46:03 -03:00
greenart7c3 74054aab5a add signer support for sending notes 2023-08-14 10:46:02 -03:00
greenart7c3 11908b9e0c if amber is installed show floatingActionButton 2023-08-14 10:43:10 -03:00
greenart7c3 570cccfc62 add signer support in the profile tabs 2023-08-14 10:42:57 -03:00
greenart7c3 de9d6877e1 fix merge 2023-08-14 10:42:57 -03:00
greenart7c3 c31fa116ca add signerdialog to boosts 2023-08-14 10:42:57 -03:00
greenart7c3 10b15226a1 added SignerDialog in the like reaction 2023-08-14 10:42:57 -03:00
greenart7c3 7a46376bc1 added SignerDialog in the hashtagscreen 2023-08-14 10:42:57 -03:00
greenart7c3 b761aaf970 sign unfollow event 2023-08-14 10:42:57 -03:00
greenart7c3 3a08db2dfb add signerDialog in the profile screen follow button 2023-08-14 10:42:57 -03:00
greenart7c3 41fd2e087c use rememberLauncherForActivityResult to send the intent and get the result back 2023-08-14 10:42:57 -03:00
greenart7c3 ba476bcdca created SignerDialog 2023-08-14 10:42:56 -03:00
224 changed files with 11207 additions and 3846 deletions
+32
View File
@@ -0,0 +1,32 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>
+5 -5
View File
@@ -106,7 +106,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
This is a native Android app made with Kotlin and Jetpack Compose.
The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays.
The overall architecture consists in the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
The overall architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to
@@ -172,7 +172,7 @@ For the Play build:
keytool -genkey -v -keystore <my-release-key.keystore> -alias <alias_name> -keyalg RSA -keysize 2048 -validity 10000
openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.jks.base64.txt
```
2. Create 4 Secret Key variables on your GitHub repository and fill in with the signing key information
2. Create four Secret Key variables on your GitHub repository and fill in the signing key information
- `KEY_ALIAS` <- `<alias_name>`
- `KEY_PASSWORD` <- `<your password>`
- `KEY_STORE_PASSWORD` <- `<your key store password>`
@@ -192,15 +192,15 @@ The relay also learns which public keys you are requesting, meaning your public
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of Private Zaps and Private DMs.
# DM Privacy #
While the content of direct messages (DMs) is only visible to you, and your DM nostr counterparty, everyone can see that and when you and your counterparty are DM-ing each other.
While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
# Visibility & Permanence of Your Content on nostr
## Information Visibility ##
Content that you share can be shared to other relays.
Information that you share is publicly visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
## Information Permanence ##
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted.
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted.
# Screenshots
+6 -6
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 294
versionName "0.76.0"
versionCode 309
versionName "0.79.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -102,7 +102,7 @@ dependencies {
// Observe Live data as State
implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version"
implementation "androidx.compose.material:material:$compose_ui_version"
implementation "androidx.compose.material3:material3:${material3_version}"
implementation "androidx.compose.material:material-icons-extended:$compose_ui_version"
// Lifecycle
@@ -112,7 +112,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Zoomable images
implementation "net.engawapg.lib:zoomable:1.5.0"
implementation 'net.engawapg.lib:zoomable:1.5.1'
// Biometrics
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
@@ -155,7 +155,7 @@ dependencies {
// Markdown (With fix for full-image bleeds)
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:a0954aba63')
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:a0954aba63')
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material3:a0954aba63')
implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:a0954aba63')
// Language picker and Theme chooser
@@ -192,7 +192,7 @@ dependencies {
implementation 'id.zelory:compressor:3.0.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'io.mockk:mockk:1.13.7'
testImplementation 'io.mockk:mockk:1.13.8'
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha01'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.RichTextParser
import com.vitorpamplona.amethyst.service.RichTextViewerState
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.EmptyTagList
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@@ -666,7 +666,7 @@ class RichTextParserTest {
@Test
fun testTextToParse() {
val state = RichTextParser().parseText(textToParse, ImmutableListOfLists())
val state = RichTextParser().parseText(textToParse, EmptyTagList)
Assert.assertEquals(
"relay.shitforce.one, relayable.org, universe.nostrich.land, nos.lol, universe.nostrich.land?lang=zh, universe.nostrich.land?lang=en, relay.damus.io, relay.nostr.wirednet.jp, offchain.pub, nostr.rocks, relay.wellorder.net, nostr.oxtr.dev, universe.nostrich.land?lang=ja, relay.mostr.pub, nostr.bitcoiner.social, Nostr-Check.com, MR.Rabbit, Ancap.su, zapper.lol, smies.me, baller.hodl",
state.urlSet.joinToString(", ")
@@ -3989,7 +3989,7 @@ class RichTextParserTest {
@Test
fun testShortTextToParse() {
val state = RichTextParser().parseText("Hi, how are you doing? ", ImmutableListOfLists())
val state = RichTextParser().parseText("Hi, how are you doing? ", EmptyTagList)
Assert.assertTrue(state.urlSet.isEmpty())
Assert.assertTrue(state.imagesForPager.isEmpty())
Assert.assertTrue(state.imageList.isEmpty())
@@ -3999,7 +3999,7 @@ class RichTextParserTest {
@Test
fun testShortNewLinesTextToParse() {
val state = RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", ImmutableListOfLists())
val state = RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList)
Assert.assertTrue(state.urlSet.isEmpty())
Assert.assertTrue(state.imagesForPager.isEmpty())
Assert.assertTrue(state.imageList.isEmpty())
@@ -4019,7 +4019,7 @@ See how it can be done here: https://lnshort.it/live-stream-embeds/
https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg
""".trimIndent()
val state = RichTextParser().parseText(text, ImmutableListOfLists())
val state = RichTextParser().parseText(text, EmptyTagList)
Assert.assertEquals("https://lnshort.it/live-stream-embeds/", state.urlSet.firstOrNull())
Assert.assertEquals("https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", state.imagesForPager.keys.firstOrNull())
Assert.assertEquals("https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", state.imageList.firstOrNull()?.url)
@@ -4080,7 +4080,7 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
fun testNewLineAfterImage() {
val text = "Thats it ! Thats the #note https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg "
val state = RichTextParser().parseText(text, ImmutableListOfLists())
val state = RichTextParser().parseText(text, EmptyTagList)
printStateForDebug(state)
@@ -4106,7 +4106,7 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
fun testSapceAfterImage() {
val text = "Thats it! https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg Thats the #note"
val state = RichTextParser().parseText(text, ImmutableListOfLists())
val state = RichTextParser().parseText(text, EmptyTagList)
printStateForDebug(state)
@@ -2,7 +2,8 @@ package com.vitorpamplona.amethyst.service.notifications
import com.vitorpamplona.amethyst.AccountInfo
class PushNotificationUtils {
fun init(accounts: List<AccountInfo>) {
object PushNotificationUtils {
var hasInit: Boolean = true
suspend fun init(accounts: List<AccountInfo>) {
}
}
+2
View File
@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="org.torproject.android"/>
<package android:name="com.greenart7c3.nostrsigner"/>
<package android:name="com.greenart7c3.nostrsigner.debug"/>
</queries>
<!-- Doesn't require a camera -->
@@ -0,0 +1,120 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
import androidx.compose.ui.unit.Velocity
/**
* A nested scroll modifier that provides scroll events to [state].
*
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
* order to receive scroll events. For example:
*
* @sample androidx.compose.material.samples.PullRefreshSample
*
* @param state The [PullRefreshState] associated with this pull-to-refresh component.
* The state will be updated by this modifier.
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored.
*/
// TODO(b/244423199): Move pullRefresh into its own material library similar to material-ripple.
fun Modifier.pullRefresh(
state: PullRefreshState,
enabled: Boolean = true
) = inspectable(
inspectorInfo = debugInspectorInfo {
name = "pullRefresh"
properties["state"] = state
properties["enabled"] = enabled
}
) {
Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
}
/**
* A nested scroll modifier that provides [onPull] and [onRelease] callbacks to aid building custom
* pull refresh components.
*
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
* order to receive scroll events. For example:
*
* @sample androidx.compose.material.samples.CustomPullRefreshSample
*
* @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument.
* Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling
* down despite being at the top of a scrollable component), whereas negative delta (swiping up) is
* dispatched first (in case it is needed to push the indicator back up), and then the unconsumed
* delta is passed on to the child. The callback returns how much delta was consumed.
* @param onRelease Callback for when drag is released, takes float flingVelocity as argument.
* The callback returns how much velocity was consumed - in most cases this should only consume
* velocity if pull refresh has been dragged already and the velocity is positive (the fling is
* downwards), as an upwards fling should typically still scroll a scrollable component beneath the
* pullRefresh. This is invoked before any remaining velocity is passed to the child.
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither
* [onPull] nor [onRelease] will be invoked.
*/
fun Modifier.pullRefresh(
onPull: (pullDelta: Float) -> Float,
onRelease: suspend (flingVelocity: Float) -> Float,
enabled: Boolean = true
) = inspectable(
inspectorInfo = debugInspectorInfo {
name = "pullRefresh"
properties["onPull"] = onPull
properties["onRelease"] = onRelease
properties["enabled"] = enabled
}
) {
Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
}
private class PullRefreshNestedScrollConnection(
private val onPull: (pullDelta: Float) -> Float,
private val onRelease: suspend (flingVelocity: Float) -> Float,
private val enabled: Boolean
) : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset = when {
!enabled -> Offset.Zero
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
else -> Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = when {
!enabled -> Offset.Zero
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
else -> Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
return Velocity(0f, onRelease(available.y))
}
}
@@ -0,0 +1,238 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
/**
* The default indicator for Compose pull-to-refresh, based on Android's SwipeRefreshLayout.
*
* @sample androidx.compose.material.samples.PullRefreshSample
*
* @param refreshing A boolean representing whether a refresh is occurring.
* @param state The [PullRefreshState] which controls where and how the indicator will be drawn.
* @param modifier Modifiers for the indicator.
* @param backgroundColor The color of the indicator's background.
* @param contentColor The color of the indicator's arc and arrow.
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
*/
// TODO(b/244423199): Consider whether the state parameter should be replaced with lambdas to
// enable people to use this indicator with custom pull-to-refresh components.
@Composable
fun PullRefreshIndicator(
refreshing: Boolean,
state: PullRefreshState,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(backgroundColor),
scale: Boolean = false
) {
val showElevation by remember(refreshing, state) {
derivedStateOf { refreshing || state.position > 0.5f }
}
Surface(
modifier = modifier
.size(IndicatorSize)
.pullRefreshIndicatorTransform(state, scale),
shape = SpinnerShape,
color = backgroundColor,
shadowElevation = if (showElevation) Elevation else 0.dp
) {
Crossfade(
targetState = refreshing,
animationSpec = tween(durationMillis = CrossfadeDurationMs)
) { refreshing ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
if (refreshing) {
CircularProgressIndicator(
color = contentColor,
strokeWidth = StrokeWidth,
modifier = Modifier.size(spinnerSize)
)
} else {
CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize))
}
}
}
}
}
/**
* Modifier.size MUST be specified.
*/
@Composable
private fun CircularArrowIndicator(
state: PullRefreshState,
color: Color,
modifier: Modifier
) {
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
val targetAlpha by remember(state) {
derivedStateOf {
if (state.progress >= 1f) MaxAlpha else MinAlpha
}
}
val alphaState = animateFloatAsState(targetValue = targetAlpha, animationSpec = AlphaTween)
// Empty semantics for tests
Canvas(modifier.semantics {}) {
val values = ArrowValues(state.progress)
val alpha = alphaState.value
rotate(degrees = values.rotation) {
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f
val arcBounds = Rect(
size.center.x - arcRadius,
size.center.y - arcRadius,
size.center.x + arcRadius,
size.center.y + arcRadius
)
drawArc(
color = color,
alpha = alpha,
startAngle = values.startAngle,
sweepAngle = values.endAngle - values.startAngle,
useCenter = false,
topLeft = arcBounds.topLeft,
size = arcBounds.size,
style = Stroke(
width = StrokeWidth.toPx(),
cap = StrokeCap.Square
)
)
drawArrow(path, arcBounds, color, alpha, values)
}
}
}
@Immutable
private class ArrowValues(
val rotation: Float,
val startAngle: Float,
val endAngle: Float,
val scale: Float
)
private fun ArrowValues(progress: Float): ArrowValues {
// Discard first 40% of progress. Scale remaining progress to full range between 0 and 100%.
val adjustedPercent = max(min(1f, progress) - 0.4f, 0f) * 5 / 3
// How far beyond the threshold pull has gone, as a percentage of the threshold.
val overshootPercent = abs(progress) - 1.0f
// Limit the overshoot to 200%. Linear between 0 and 200.
val linearTension = overshootPercent.coerceIn(0f, 2f)
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
val tensionPercent = linearTension - linearTension.pow(2) / 4
// Calculations based on SwipeRefreshLayout specification.
val endTrim = adjustedPercent * MaxProgressArc
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
val startAngle = rotation * 360
val endAngle = (rotation + endTrim) * 360
val scale = min(1f, adjustedPercent)
return ArrowValues(rotation, startAngle, endAngle, scale)
}
private fun DrawScope.drawArrow(
arrow: Path,
bounds: Rect,
color: Color,
alpha: Float,
values: ArrowValues
) {
arrow.reset()
arrow.moveTo(0f, 0f) // Move to left corner
arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
// Line to tip of arrow
arrow.lineTo(
x = ArrowWidth.toPx() * values.scale / 2,
y = ArrowHeight.toPx() * values.scale
)
val radius = min(bounds.width, bounds.height) / 2f
val inset = ArrowWidth.toPx() * values.scale / 2f
arrow.translate(
Offset(
x = radius + bounds.center.x - inset,
y = bounds.center.y + StrokeWidth.toPx() / 2f
)
)
arrow.close()
rotate(degrees = values.endAngle) {
drawPath(path = arrow, color = color, alpha = alpha)
}
}
private const val CrossfadeDurationMs = 100
private const val MaxProgressArc = 0.8f
private val IndicatorSize = 40.dp
private val SpinnerShape = CircleShape
private val ArcRadius = 7.5.dp
private val StrokeWidth = 2.5.dp
private val ArrowWidth = 10.dp
private val ArrowHeight = 5.dp
private val Elevation = 6.dp
// Values taken from SwipeRefreshLayout
private const val MinAlpha = 0.3f
private const val MaxAlpha = 1f
private val AlphaTween = tween<Float>(300, easing = LinearEasing)
@@ -0,0 +1,75 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
/**
* A modifier for translating the position and scaling the size of a pull-to-refresh indicator
* based on the given [PullRefreshState].
*
* @sample androidx.compose.material.samples.PullRefreshIndicatorTransformSample
*
* @param state The [PullRefreshState] which determines the position of the indicator.
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
*/
// TODO: Consider whether the state parameter should be replaced with lambdas.
fun Modifier.pullRefreshIndicatorTransform(
state: PullRefreshState,
scale: Boolean = false
) = inspectable(
inspectorInfo = debugInspectorInfo {
name = "pullRefreshIndicatorTransform"
properties["state"] = state
properties["scale"] = scale
}
) {
Modifier
// Essentially we only want to clip the at the top, so the indicator will not appear when
// the position is 0. It is preferable to clip the indicator as opposed to the layout that
// contains the indicator, as this would also end up clipping shadows drawn by items in a
// list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE
// for the other dimensions to allow for more room for elevation / arbitrary indicators - we
// only ever really want to clip at the top edge.
.drawWithContent {
clipRect(
top = 0f,
left = -Float.MAX_VALUE,
right = Float.MAX_VALUE,
bottom = Float.MAX_VALUE
) {
this@drawWithContent.drawContent()
}
}
.graphicsLayer {
translationY = state.position - size.height
if (scale && !state.refreshing) {
val scaleFraction = LinearOutSlowInEasing
.transform(state.position / state.threshold)
.coerceIn(0f, 1f)
scaleX = scaleFraction
scaleY = scaleFraction
}
}
}
@@ -0,0 +1,228 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.animation.core.animate
import androidx.compose.foundation.MutatorMutex
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.pow
/**
* Creates a [PullRefreshState] that is remembered across compositions.
*
* Changes to [refreshing] will result in [PullRefreshState] being updated.
*
* @sample androidx.compose.material.samples.PullRefreshSample
*
* @param refreshing A boolean representing whether a refresh is currently occurring.
* @param onRefresh The function to be called to trigger a refresh.
* @param refreshThreshold The threshold below which, if a release
* occurs, [onRefresh] will be called.
* @param refreshingOffset The offset at which the indicator will be drawn while refreshing. This
* offset corresponds to the position of the bottom of the indicator.
*/
@Composable
fun rememberPullRefreshState(
refreshing: Boolean,
onRefresh: () -> Unit,
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset
): PullRefreshState {
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
val scope = rememberCoroutineScope()
val onRefreshState = rememberUpdatedState(onRefresh)
val thresholdPx: Float
val refreshingOffsetPx: Float
with(LocalDensity.current) {
thresholdPx = refreshThreshold.toPx()
refreshingOffsetPx = refreshingOffset.toPx()
}
val state = remember(scope) {
PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx)
}
SideEffect {
state.setRefreshing(refreshing)
state.setThreshold(thresholdPx)
state.setRefreshingOffset(refreshingOffsetPx)
}
return state
}
/**
* A state object that can be used in conjunction with [pullRefresh] to add pull-to-refresh
* behaviour to a scroll component. Based on Android's SwipeRefreshLayout.
*
* Provides [progress], a float representing how far the user has pulled as a percentage of the
* refreshThreshold. Values of one or less indicate that the user has not yet pulled past the
* threshold. Values greater than one indicate how far past the threshold the user has pulled.
*
* Can be used in conjunction with [pullRefreshIndicatorTransform] to implement Android-like
* pull-to-refresh behaviour with a custom indicator.
*
* Should be created using [rememberPullRefreshState].
*/
class PullRefreshState internal constructor(
private val animationScope: CoroutineScope,
private val onRefreshState: State<() -> Unit>,
refreshingOffset: Float,
threshold: Float
) {
/**
* A float representing how far the user has pulled as a percentage of the refreshThreshold.
*
* If the component has not been pulled at all, progress is zero. If the pull has reached
* halfway to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has
* gone beyond the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to
* two times the refreshThreshold.
*/
val progress get() = adjustedDistancePulled / threshold
internal val refreshing get() = _refreshing
internal val position get() = _position
internal val threshold get() = _threshold
private val adjustedDistancePulled by derivedStateOf { distancePulled * DragMultiplier }
private var _refreshing by mutableStateOf(false)
private var _position by mutableStateOf(0f)
private var distancePulled by mutableStateOf(0f)
private var _threshold by mutableStateOf(threshold)
private var _refreshingOffset by mutableStateOf(refreshingOffset)
internal fun onPull(pullDelta: Float): Float {
if (_refreshing) return 0f // Already refreshing, do nothing.
val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f)
val dragConsumed = newOffset - distancePulled
distancePulled = newOffset
_position = calculateIndicatorPosition()
return dragConsumed
}
internal fun onRelease(velocity: Float): Float {
if (refreshing) return 0f // Already refreshing, do nothing
if (adjustedDistancePulled > threshold) {
onRefreshState.value()
}
animateIndicatorTo(0f)
val consumed = when {
// We are flinging without having dragged the pull refresh (for example a fling inside
// a list) - don't consume
distancePulled == 0f -> 0f
// If the velocity is negative, the fling is upwards, and we don't want to prevent the
// the list from scrolling
velocity < 0f -> 0f
// We are showing the indicator, and the fling is downwards - consume everything
else -> velocity
}
distancePulled = 0f
return consumed
}
internal fun setRefreshing(refreshing: Boolean) {
if (_refreshing != refreshing) {
_refreshing = refreshing
distancePulled = 0f
animateIndicatorTo(if (refreshing) _refreshingOffset else 0f)
}
}
internal fun setThreshold(threshold: Float) {
_threshold = threshold
}
internal fun setRefreshingOffset(refreshingOffset: Float) {
if (_refreshingOffset != refreshingOffset) {
_refreshingOffset = refreshingOffset
if (refreshing) animateIndicatorTo(refreshingOffset)
}
}
// Make sure to cancel any existing animations when we launch a new one. We use this instead of
// Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
// overhead of running through the animation pipeline instead of directly mutating the state.
private val mutatorMutex = MutatorMutex()
private fun animateIndicatorTo(offset: Float) = animationScope.launch {
mutatorMutex.mutate {
animate(initialValue = _position, targetValue = offset) { value, _ ->
_position = value
}
}
}
private fun calculateIndicatorPosition(): Float = when {
// If drag hasn't gone past the threshold, the position is the adjustedDistancePulled.
adjustedDistancePulled <= threshold -> adjustedDistancePulled
else -> {
// How far beyond the threshold pull has gone, as a percentage of the threshold.
val overshootPercent = abs(progress) - 1.0f
// Limit the overshoot to 200%. Linear between 0 and 200.
val linearTension = overshootPercent.coerceIn(0f, 2f)
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
val tensionPercent = linearTension - linearTension.pow(2) / 4
// The additional offset beyond the threshold.
val extraOffset = threshold * tensionPercent
threshold + extraOffset
}
}
}
/**
* Default parameter values for [rememberPullRefreshState].
*/
object PullRefreshDefaults {
/**
* If the indicator is below this threshold offset when it is released, a refresh
* will be triggered.
*/
val RefreshThreshold = 80.dp
/**
* The offset at which the indicator should be rendered whilst a refresh is occurring.
*/
val RefreshingOffset = 56.dp
}
/**
* The distance pulled is multiplied by this value to give us the adjusted distance pulled, which
* is used in calculating the indicator position (when the adjusted distance pulled is less than
* the refresh threshold, it is the indicator position, otherwise the indicator position is
* derived from the progress).
*/
private const val DragMultiplier = 0.5f
@@ -3,23 +3,26 @@ package com.vitorpamplona.amethyst
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.BooleanType
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.DefaultReactions
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.Nip47URI
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.ServersAvailable
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.model.parseBooleanType
import com.vitorpamplona.amethyst.model.parseConnectivityType
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LnZapEvent
@@ -35,7 +38,8 @@ private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
@Immutable
data class AccountInfo(
val npub: String,
val hasPrivKey: Boolean = false
val hasPrivKey: Boolean,
val loggedInWithExternalSigner: Boolean
)
private object PrefKeys {
@@ -74,7 +78,9 @@ private object PrefKeys {
const val THEME = "theme"
const val PREFERRED_LANGUAGE = "preferred_Language"
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
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"
}
object LocalPreferences {
@@ -82,7 +88,7 @@ object LocalPreferences {
private var _currentAccount: String? = null
private fun currentAccount(): String? {
fun currentAccount(): String? {
if (_currentAccount == null) {
_currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
}
@@ -201,7 +207,11 @@ object LocalPreferences {
fun allSavedAccounts(): List<AccountInfo> {
return savedAccounts().map { npub ->
AccountInfo(npub = npub)
AccountInfo(
npub,
hasPrivKey(npub),
getLoggedInWithExternalSigner(npub)
)
}
}
@@ -245,6 +255,7 @@ object LocalPreferences {
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
}
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.loginWithExternalSigner)
}.apply()
val globalPrefs = encryptedPreferences()
@@ -265,6 +276,17 @@ object LocalPreferences {
} else {
putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview.prefCode!!)
}
if (account.settings.automaticallyHideNavigationBars.prefCode == null) {
remove(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, account.settings.automaticallyHideNavigationBars.prefCode!!)
}
if (account.settings.automaticallyShowProfilePictures.prefCode == null) {
remove(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE, account.settings.automaticallyShowProfilePictures.prefCode!!)
}
putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "")
}.apply()
}
@@ -276,17 +298,19 @@ object LocalPreferences {
}
fun getTheme(): Int {
encryptedPreferences().apply {
return getInt(PrefKeys.THEME, 0)
}
return encryptedPreferences().getInt(PrefKeys.THEME, 0)
}
fun getPreferredLanguage(): String {
var language = ""
encryptedPreferences().apply {
language = getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
}
return language
return encryptedPreferences().getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
}
private fun getLoggedInWithExternalSigner(npub: String): Boolean {
return encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
}
private fun hasPrivKey(npub: String): Boolean {
return (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank()
}
fun loadFromEncryptedStorage(): Account? {
@@ -316,11 +340,11 @@ object LocalPreferences {
val zapAmountChoices = getString(PrefKeys.ZAP_AMOUNTS, "[]")?.let {
Event.mapper.readValue<List<Long>?>(it)
}?.ifEmpty { listOf(500L, 1000L, 5000L) } ?: listOf(500L, 1000L, 5000L)
}?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
val reactionChoices = getString(PrefKeys.REACTION_CHOICES, "[]")?.let {
Event.mapper.readValue<List<String>?>(it)
}?.ifEmpty { listOf("+") } ?: listOf("+")
}?.ifEmpty { DefaultReactions } ?: DefaultReactions
val defaultZapType = getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
LnZapEvent.ZapType.values().firstOrNull() { it.name == serverName }
@@ -335,16 +359,22 @@ object LocalPreferences {
Event.mapper.readValue<Nip47URI?>(it)
}
} catch (e: Throwable) {
Log.w("LocalPreferences", "Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}", e)
e.printStackTrace()
null
}
val latestContactList = try {
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
Event.fromJson(it) as ContactListEvent?
println("Decoding Contact List: " + it)
if (it != null) {
Event.fromJson(it) as ContactListEvent?
} else {
null
}
}
} catch (e: Throwable) {
e.printStackTrace()
Log.w("LocalPreferences", "Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", e)
null
}
@@ -353,6 +383,7 @@ object LocalPreferences {
Event.mapper.readValue<Map<String, String>?>(it)
} ?: mapOf()
} catch (e: Throwable) {
Log.w("LocalPreferences", "Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}", e)
e.printStackTrace()
mapOf()
}
@@ -363,6 +394,7 @@ object LocalPreferences {
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
val loginWithExternalSigner = getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
@@ -377,6 +409,7 @@ object LocalPreferences {
Event.mapper.readValue<Map<String, Long>?>(it)
} ?: mapOf()
} catch (e: Throwable) {
Log.w("LocalPreferences", "Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}", e)
e.printStackTrace()
mapOf()
}
@@ -399,6 +432,17 @@ object LocalPreferences {
} else {
ConnectivityType.ALWAYS
}
settings.automaticallyHideNavigationBars = if (contains(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)) {
parseBooleanType(getBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, false))
} else {
BooleanType.ALWAYS
}
settings.automaticallyShowProfilePictures = if (contains(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE)) {
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE, false))
} else {
ConnectivityType.ALWAYS
}
settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "")
}
@@ -431,51 +475,11 @@ object LocalPreferences {
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,
settings = settings
settings = settings,
loginWithExternalSigner = loginWithExternalSigner
)
return a
}
}
fun migrateSingleUserPrefs() {
if (currentAccount() != null) return
val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return
val npub = Hex.decode(pubkey).toNpub()
val stringPrefs = listOf(
PrefKeys.NOSTR_PRIVKEY,
PrefKeys.NOSTR_PUBKEY,
PrefKeys.RELAYS,
PrefKeys.LANGUAGE_PREFS,
PrefKeys.TRANSLATE_TO,
PrefKeys.ZAP_AMOUNTS,
PrefKeys.LATEST_CONTACT_LIST
)
val stringSetPrefs = listOf(
PrefKeys.FOLLOWING_CHANNELS,
PrefKeys.HIDDEN_USERS,
PrefKeys.DONT_TRANSLATE_FROM
)
encryptedPreferences().apply {
val appPrefs = this
encryptedPreferences(npub).edit().apply {
val userPrefs = this
stringPrefs.forEach { userPrefs.putString(it, appPrefs.getString(it, null)) }
stringSetPrefs.forEach { userPrefs.putStringSet(it, appPrefs.getStringSet(it, null)) }
userPrefs.putBoolean(
PrefKeys.HIDE_DELETE_REQUEST_DIALOG,
appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
)
}.apply()
}
encryptedPreferences().edit().clear().apply()
addAccount(npub)
updateCurrentAccount(npub)
}
}
@@ -92,9 +92,7 @@ class MultiPlayerPlaybackManager(
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
STATE_IDLE -> {
if (player.currentPosition > 5 * 60) { // 5 seconds
cachedPositions.add(uri, player.currentPosition)
}
cachedPositions.add(uri, player.currentPosition)
}
STATE_READY -> {
cachedPositions.get(uri)?.let { lastPosition ->
@@ -104,9 +102,7 @@ class MultiPlayerPlaybackManager(
}
}
else -> {
if (player.currentPosition > 5 * 60) { // 5 seconds
cachedPositions.add(uri, player.currentPosition)
}
cachedPositions.add(uri, player.currentPosition)
}
}
}
@@ -12,6 +12,7 @@ import coil.disk.DiskCache
import coil.util.DebugLogger
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
@@ -35,15 +36,21 @@ import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import java.io.File
object ServiceManager {
var shouldPauseService: Boolean = true // to not open amber in a loop trying to use auth relays and registering for notifications
private var isStarted: Boolean = false // to not open amber in a loop trying to use auth relays and registering for notifications
private var account: Account? = null
fun start(account: Account, context: Context) {
this.account = account
ExternalSignerUtils.account = account
start(context)
}
@Synchronized
fun start(context: Context) {
if (isStarted && account != null) {
return
}
Log.d("ServiceManager", "Starting Relay Services")
val myAccount = account
@@ -88,6 +95,7 @@ object ServiceManager {
NostrSingleEventDataSource.start()
NostrSingleChannelDataSource.start()
NostrSingleUserDataSource.start()
isStarted = true
}
}
@@ -113,6 +121,7 @@ object ServiceManager {
NostrVideoDataSource.stop()
Client.disconnect()
isStarted = false
}
fun cleanObservers() {
@@ -133,6 +142,13 @@ object ServiceManager {
LocalCache.pruneExpiredEvents()
}
}
fun restartIfDifferentAccount(account: Account, context: Context) {
if (this.account != account) {
pause()
start(account, context)
}
}
}
object SingletonDiskCache {
@@ -12,7 +12,7 @@ import java.io.File
@UnstableApi object VideoCache {
var exoPlayerCacheSize: Long = 90 * 1024 * 1024 // 90MB
var exoPlayerCacheSize: Long = 150 * 1024 * 1024 // 90MB
var leastRecentlyUsedCacheEvictor = LeastRecentlyUsedCacheEvictor(exoPlayerCacheSize)
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
import android.util.Log
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledInsert
@@ -11,6 +13,7 @@ import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.HexValidator
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.*
@@ -53,6 +56,10 @@ object LocalCache {
// checkNotInMainThread()
return users[key] ?: run {
require(isValidHex(key = key)) {
"$key is not a valid hex"
}
val newObject = User(key)
users.putIfAbsent(key, newObject) ?: newObject
}
@@ -98,6 +105,10 @@ object LocalCache {
checkNotInMainThread()
return notes.get(idHex) ?: run {
require(isValidHex(idHex)) {
"$idHex is not a valid hex"
}
val newObject = Note(idHex)
notes.putIfAbsent(idHex, newObject) ?: newObject
}
@@ -198,6 +209,14 @@ object LocalCache {
val user = getOrCreateUser(event.pubKey)
if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) {
if (event.dTag() == "bookmark") {
val loggedInUser = LocalPreferences.currentAccount()
val hexKey = loggedInUser?.bechToBytes()
if (hexKey != null) {
val pubKey = Hex.encode(hexKey)
if (pubKey == event.pubKey) {
ExternalSignerUtils.content.remove(event.id)
}
}
user.updateBookmark(event)
}
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
@@ -807,23 +826,23 @@ object LocalCache {
// Already processed this event.
if (note.event != null) return
val zapRequest = event.zapRequest?.id?.let { getOrCreateNote(it) }
val zapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) }
if (zapRequest == null || zapRequest.event !is LnZapRequestEvent) {
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}")
return
}
val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
(
(zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()
(zapRequest.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()
)
note.loadEvent(event, author, repliesTo)
if (zapRequest == null) {
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}")
return
}
// Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
repliesTo.forEach {
@@ -43,6 +43,10 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
return minOf(publishedAt, lastCreatedAt)
}
fun dTag(): String? {
return (event as? AddressableEvent)?.dTag()
}
}
@Stable
@@ -669,16 +673,24 @@ open class Note(val idHex: String) {
}
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
if (event == null) return false
val thisEvent = event ?: return false
val isBoostedNoteHidden = if (event is GenericRepostEvent || event is RepostEvent || event is CommunityPostApprovalEvent) {
val isBoostedNoteHidden = if (thisEvent is GenericRepostEvent || thisEvent is RepostEvent || thisEvent is CommunityPostApprovalEvent) {
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
} else {
false
}
val isSensitive = event?.isSensitive() ?: false
return isBoostedNoteHidden ||
val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) {
accountChoices.hiddenWords.any {
thisEvent.content.contains(it, true)
}
} else {
false
}
val isSensitive = thisEvent.isSensitive()
return isBoostedNoteHidden || isHiddenByWord ||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
accountChoices.spammers.contains(author?.pubkeyHex) ||
(isSensitive && accountChoices.showSensitiveContent == false)
@@ -8,7 +8,9 @@ class Settings(
var preferredLanguage: String? = null,
var automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
var automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS,
var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS
var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS,
var automaticallyHideNavigationBars: BooleanType = BooleanType.ALWAYS,
var automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS
)
enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) {
@@ -38,3 +40,28 @@ fun parseConnectivityType(screenCode: Int): ConnectivityType {
}
}
}
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) {
BooleanType.ALWAYS.prefCode -> BooleanType.ALWAYS
BooleanType.NEVER.prefCode -> BooleanType.NEVER
else -> {
BooleanType.ALWAYS
}
}
}
fun parseBooleanType(screenCode: Int): BooleanType {
return when (screenCode) {
BooleanType.ALWAYS.screenCode -> BooleanType.ALWAYS
BooleanType.NEVER.screenCode -> BooleanType.NEVER
else -> {
BooleanType.ALWAYS
}
}
}
@@ -5,16 +5,15 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.quartz.encoders.Bech32
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChatroomKey
@@ -27,9 +26,6 @@ import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import java.math.BigDecimal
import java.util.regex.Pattern
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
@Stable
class User(val pubkeyHex: String) {
@@ -268,19 +264,11 @@ class User(val pubkeyHex: String) {
info?.updatedMetadataAt = latestMetadata.createdAt
info?.tags = latestMetadata.tags.toImmutableListOfLists()
if (newUserInfo.lud16.isNullOrBlank() && newUserInfo.lud06?.lowercase()?.startsWith("lnurl") == true) {
try {
val url = String(Bech32.decodeBytes(newUserInfo.lud06!!, false).second)
val matcher = lnurlpPattern.matcher(url)
while (matcher.find()) {
val domain = matcher.group(2)
val username = matcher.group(3)
info?.lud16 = "$username@$domain"
if (newUserInfo.lud16.isNullOrBlank()) {
info?.lud06?.let {
if (it.lowercase().startsWith("lnurl")) {
info?.lud16 = Lud06().toLud16(it)
}
} catch (t: Throwable) {
// Doesn't create errors.
}
}
@@ -331,7 +319,7 @@ class User(val pubkeyHex: String) {
return latestContactList?.verifiedFollowKeySet ?: emptySet()
}
fun cachedFollowingTagSet(): Set<HexKey> {
fun cachedFollowingTagSet(): Set<String> {
return latestContactList?.verifiedFollowTagSet ?: emptySet()
}
@@ -11,7 +11,6 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
import com.vitorpamplona.amethyst.ui.components.hashTagsPattern
import com.vitorpamplona.amethyst.ui.components.imageExtensions
import com.vitorpamplona.amethyst.ui.components.startsWithNIP19Scheme
import com.vitorpamplona.amethyst.ui.components.tagIndex
import com.vitorpamplona.amethyst.ui.components.videoExtensions
import com.vitorpamplona.quartz.events.ImmutableListOfLists
@@ -290,3 +289,9 @@ class SchemelessUrlSegment(segment: String, val url: String, val extras: String?
@Immutable
class RegularTextSegment(segment: String) : Segment(segment)
fun startsWithNIP19Scheme(word: String): Boolean {
val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@")
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
}
@@ -0,0 +1,311 @@
package com.vitorpamplona.amethyst.service
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.util.LruCache
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
enum class SignerType {
SIGN_EVENT,
NIP04_ENCRYPT,
NIP04_DECRYPT,
NIP44_ENCRYPT,
NIP44_DECRYPT,
GET_PUBLIC_KEY,
DECRYPT_ZAP_EVENT
}
object ExternalSignerUtils {
val content = LruCache<String, String>(10)
var isActivityRunning: Boolean = false
val cachedDecryptedContent = mutableMapOf<HexKey, String>()
lateinit var account: Account
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private lateinit var decryptResultLauncher: ActivityResultLauncher<Intent>
private lateinit var blockListResultLauncher: ActivityResultLauncher<Intent>
@OptIn(DelicateCoroutinesApi::class)
fun requestRejectedToast() {
GlobalScope.launch(Dispatchers.Main) {
Toast.makeText(
Amethyst.instance,
Amethyst.instance.getString(R.string.sign_request_rejected),
Toast.LENGTH_SHORT
).show()
}
}
@OptIn(DelicateCoroutinesApi::class)
fun default() {
isActivityRunning = false
ServiceManager.shouldPauseService = true
GlobalScope.launch(Dispatchers.IO) {
isActivityRunning = false
ServiceManager.shouldPauseService = true
}
}
@OptIn(DelicateCoroutinesApi::class)
fun start(activity: MainActivity) {
activityResultLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != Activity.RESULT_OK) {
requestRejectedToast()
} else {
val event = it.data?.getStringExtra("signature") ?: ""
val id = it.data?.getStringExtra("id") ?: ""
if (id.isNotBlank()) {
content.put(id, event)
}
}
default()
}
decryptResultLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != Activity.RESULT_OK) {
requestRejectedToast()
} else {
val event = it.data?.getStringExtra("signature") ?: ""
val id = it.data?.getStringExtra("id") ?: ""
if (id.isNotBlank()) {
content.put(id, event)
cachedDecryptedContent[id] = event
}
}
default()
}
blockListResultLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != Activity.RESULT_OK) {
requestRejectedToast()
} else {
val decryptedContent = it.data?.getStringExtra("signature") ?: ""
val id = it.data?.getStringExtra("id") ?: ""
if (id.isNotBlank()) {
cachedDecryptedContent[id] = decryptedContent
account.live.invalidateData()
}
}
default()
}
}
@OptIn(DelicateCoroutinesApi::class)
fun openSigner(
data: String,
type: SignerType,
intentResult: ActivityResultLauncher<Intent>,
pubKey: HexKey,
id: String
) {
try {
ServiceManager.shouldPauseService = false
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data"))
val signerType = when (type) {
SignerType.SIGN_EVENT -> "sign_event"
SignerType.NIP04_ENCRYPT -> "nip04_encrypt"
SignerType.NIP04_DECRYPT -> "nip04_decrypt"
SignerType.NIP44_ENCRYPT -> "nip44_encrypt"
SignerType.NIP44_DECRYPT -> "nip44_decrypt"
SignerType.GET_PUBLIC_KEY -> "get_public_key"
SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event"
}
intent.putExtra("type", signerType)
intent.putExtra("pubKey", pubKey)
intent.putExtra("id", id)
if (type !== SignerType.GET_PUBLIC_KEY) {
intent.putExtra("current_user", account.keyPair.pubKey.toNpub())
}
intent.`package` = "com.greenart7c3.nostrsigner"
intentResult.launch(intent)
} catch (e: Exception) {
Log.e("Signer", "Error opening Signer app", e)
GlobalScope.launch(Dispatchers.Main) {
Toast.makeText(
Amethyst.instance,
Amethyst.instance.getString(R.string.error_opening_external_signer),
Toast.LENGTH_SHORT
).show()
}
}
}
fun openSigner(event: EventInterface, columnName: String = "signature") {
checkNotInMainThread()
val result = getDataFromResolver(SignerType.SIGN_EVENT, arrayOf(event.toJson(), event.pubKey()), columnName)
if (result !== null) {
content.put(event.id(), result)
return
}
ServiceManager.shouldPauseService = false
isActivityRunning = true
openSigner(
event.toJson(),
SignerType.SIGN_EVENT,
activityResultLauncher,
"",
event.id()
)
while (isActivityRunning) {
Thread.sleep(100)
}
}
fun decryptBlockList(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
isActivityRunning = true
openSigner(
encryptedContent,
signerType,
blockListResultLauncher,
pubKey,
id
)
}
fun getDataFromResolver(signerType: SignerType, data: Array<out String>, columnName: String = "signature"): String? {
val localData = if (signerType !== SignerType.GET_PUBLIC_KEY) {
data.toList().plus(account.keyPair.pubKey.toNpub()).toTypedArray()
} else {
data
}
Amethyst.instance.contentResolver.query(
Uri.parse("content://com.greenart7c3.nostrsigner.$signerType"),
localData,
null,
null,
null
).use {
if (it !== null) {
if (it.moveToFirst()) {
val index = it.getColumnIndex(columnName)
if (index < 0) {
Log.d("getDataFromResolver", "column '$columnName' not found")
return null
}
return it.getString(index)
}
}
}
return null
}
fun decrypt(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
isActivityRunning = true
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
while (isActivityRunning) {
// do nothing
}
}
fun decryptDM(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
}
fun decryptBookmark(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
}
fun encrypt(decryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_ENCRYPT) {
val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey))
if (result !== null) {
content.put(id, result)
return
}
isActivityRunning = true
openSigner(
decryptedContent,
signerType,
activityResultLauncher,
pubKey,
id
)
while (isActivityRunning) {
Thread.sleep(100)
}
}
fun decryptZapEvent(event: LnZapRequestEvent) {
val result = getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey))
if (result !== null) {
content.put(event.id, result)
cachedDecryptedContent[event.id] = result
return
}
openSigner(
event.toJson(),
SignerType.DECRYPT_ZAP_EVENT,
decryptResultLauncher,
event.pubKey,
event.id
)
}
}
@@ -16,16 +16,16 @@ class FileHeader(
val size: Int,
val dim: String?,
val blurHash: String?,
val description: String? = null,
val alt: String? = null,
val sensitiveContent: Boolean = false
) {
companion object {
suspend fun prepare(fileUrl: String, mimeType: String?, description: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
suspend fun prepare(fileUrl: String, mimeType: String?, alt: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
try {
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
if (imageData != null) {
prepare(imageData, fileUrl, mimeType, description, sensitiveContent, onReady, onError)
prepare(imageData, fileUrl, mimeType, alt, sensitiveContent, onReady, onError)
} else {
onError()
}
@@ -39,7 +39,7 @@ class FileHeader(
data: ByteArray,
fileUrl: String,
mimeType: String?,
description: String?,
alt: String?,
sensitiveContent: Boolean,
onReady: (FileHeader) -> Unit,
onError: () -> Unit
@@ -79,7 +79,7 @@ class FileHeader(
Pair(null, null)
}
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, description, sensitiveContent))
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, alt, sensitiveContent))
} catch (e: Exception) {
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
onError()
@@ -8,17 +8,26 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
object NostrAccountDataSource : NostrDataSource("AccountData") {
@@ -136,6 +145,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
}
override fun consume(event: Event, relay: Relay) {
checkNotInMainThread()
if (LocalCache.justVerify(event)) {
if (event is GiftWrapEvent) {
val privateKey = account.keyPair.privKey
@@ -143,6 +154,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
event.cachedGift(privateKey)?.let {
this.consume(it, relay)
}
} else if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
event.content,
event.pubKey,
event.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
}
event.cachedGift(account.keyPair.pubKey, cached)?.let {
this.consume(it, relay)
}
}
}
@@ -152,6 +177,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
event.cachedGossip(privateKey)?.let {
LocalCache.justConsume(it, relay)
}
} else if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
event.content,
event.pubKey,
event.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
}
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
LocalCache.justConsume(it, relay)
}
}
// Don't store sealed gossips to avoid rebroadcasting by mistake.
@@ -181,7 +220,6 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
if (this::account.isInitialized) {
val event = account.createAuthEvent(relay, challenge)
if (event != null) {
Client.send(
event,
@@ -44,7 +44,7 @@ class NostrLnZapPaymentResponseDataSource(
override fun auth(relay: Relay, challenge: String) {
super.auth(relay, challenge)
val event = RelayAuthEvent.create(relay.url, challenge, authSigningKey)
val event = RelayAuthEvent.create(relay.url, challenge, "", authSigningKey)
Client.send(
event,
relay.url
@@ -0,0 +1,17 @@
package com.vitorpamplona.amethyst.service
import android.content.Context
object PackageUtils {
private fun isPackageInstalled(context: Context, target: String): Boolean {
return context.packageManager.getInstalledApplications(0).find { info -> info.packageName == target } != null
}
fun isOrbotInstalled(context: Context): Boolean {
return isPackageInstalled(context, "org.torproject.android")
}
fun isAmberInstalled(context: Context): Boolean {
return isPackageInstalled(context, "com.greenart7c3.nostrsigner")
}
}
@@ -0,0 +1,207 @@
package com.vitorpamplona.amethyst.service
import android.content.Context
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.ZapSplitSetup
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.round
class ZapPaymentHandler(val account: Account) {
@Immutable
data class Payable(
val info: ZapSplitSetup,
val user: User?,
val amountMilliSats: Long,
val invoice: String
)
suspend fun zap(
note: Note,
amountMilliSats: Long,
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
zapType: LnZapEvent.ZapType
) = withContext(Dispatchers.IO) {
val zapSplitSetup = note.event?.zapSplitSetup()
val zapsToSend = if (!zapSplitSetup.isNullOrEmpty()) {
zapSplitSetup
} else {
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
if (lud16.isNullOrBlank()) {
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
return@withContext
}
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
}
val totalWeight = zapsToSend.sumOf { it.weight }
val invoicesToPayOnIntent = mutableListOf<Payable>()
zapsToSend.forEachIndexed { index, value ->
val outerProgressMin = index / zapsToSend.size.toFloat()
val outerProgressMax = (index + 1) / zapsToSend.size.toFloat()
val zapValue =
round((amountMilliSats * value.weight / totalWeight) / 1000f).toLong() * 1000
if (value.isLnAddress) {
innerZap(
lud16 = value.lnAddressOrPubKeyHex,
note = note,
amount = zapValue,
pollOption = pollOption,
message = message,
context = context,
onError = onError,
onProgress = {
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
},
zapType = zapType,
onPayInvoiceThroughIntent = {
invoicesToPayOnIntent.add(
Payable(
info = value,
user = null,
amountMilliSats = zapValue,
invoice = it
)
)
}
)
} else {
val user = LocalCache.getUserIfExists(value.lnAddressOrPubKeyHex)
val lud16 = user?.info?.lnAddress()
if (lud16 != null) {
innerZap(
lud16 = lud16,
note = note,
amount = zapValue,
pollOption = pollOption,
message = message,
context = context,
onError = onError,
onProgress = {
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
},
zapType = zapType,
overrideUser = user,
onPayInvoiceThroughIntent = {
invoicesToPayOnIntent.add(
Payable(
info = value,
user = user,
amountMilliSats = zapValue,
invoice = it
)
)
}
)
} else {
onError(
context.getString(
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
)
)
}
}
}
if (invoicesToPayOnIntent.isNotEmpty()) {
onPayViaIntent(invoicesToPayOnIntent.toImmutableList())
onProgress(1f)
} else {
launch(Dispatchers.IO) {
// Awaits for the event to come back to LocalCache.
delay(5000)
onProgress(1f)
}
}
}
private suspend fun innerZap(
lud16: String,
note: Note,
amount: Long,
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayInvoiceThroughIntent: (String) -> Unit,
zapType: LnZapEvent.ZapType,
overrideUser: User? = null
) {
var zapRequestJson = ""
if (zapType != LnZapEvent.ZapType.NONZAP) {
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType, overrideUser)
if (zapRequest != null) {
zapRequestJson = zapRequest.toJson()
}
}
onProgress(0.10f)
LightningAddressResolver().lnAddressInvoice(
lud16,
amount,
message,
zapRequestJson,
onSuccess = {
onProgress(0.7f)
if (account.hasWalletConnectSetup()) {
account.sendZapPaymentRequestFor(
bolt11 = it,
note,
onResponse = { response ->
if (response is PayInvoiceErrorResponse) {
onProgress(0.0f)
onError(
response.error?.message
?: response.error?.code?.toString()
?: "Error parsing error message"
)
} else {
onProgress(1f)
}
}
)
onProgress(0.8f)
} else {
try {
onPayInvoiceThroughIntent(it)
} catch (e: Exception) {
onError(context.getString(R.string.lightning_wallets_not_found2))
}
onProgress(0f)
}
},
onError = onError,
onProgress = onProgress
)
}
}
@@ -4,16 +4,14 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.Bech32
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.toLnUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import java.math.BigDecimal
import java.math.RoundingMode
import java.net.URLEncoder
class LightningAddressResolver() {
@@ -27,11 +25,7 @@ class LightningAddressResolver() {
}
if (lnaddress.lowercase().startsWith("lnurl")) {
return try {
String(Bech32.decodeBytes(lnaddress, false).second)
} catch (e: Exception) {
null
}
return Lud06().toLnUrlp(lnaddress)
}
return null
@@ -66,7 +60,7 @@ class LightningAddressResolver() {
}
}
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
val encodedMessage = URLEncoder.encode(message, "utf-8")
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
@@ -82,22 +76,13 @@ class LightningAddressResolver() {
.url(url)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
response.use {
if (it.isSuccessful) {
onSuccess(response.body.string())
} else {
onError("Could not fetch invoice from $lnCallback")
}
}
client.newCall(request).execute().use {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("Could not fetch invoice from $lnCallback")
}
override fun onFailure(call: Call, e: java.io.IOException) {
onError("Could not fetch an invoice from $lnCallback. Message ${e.message}")
e.printStackTrace()
}
})
}
}
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
@@ -115,7 +100,7 @@ class LightningAddressResolver() {
milliSats: Long,
message: String,
nostrRequest: String? = null,
onSuccess: (String) -> Unit,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit
) {
@@ -159,13 +144,14 @@ class LightningAddressResolver() {
lnInvoice?.get("pr")?.asText()?.ifBlank { null }?.let { pr ->
// Forces LN Invoice amount to be the requested amount.
val expectedAmountInSats = BigDecimal(milliSats).divide(BigDecimal(1000), RoundingMode.HALF_UP).toLong()
val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr)
if (invoiceAmount.multiply(BigDecimal(1000)).toLong() == BigDecimal(milliSats).toLong()) {
if (invoiceAmount.toLong() == expectedAmountInSats) {
onProgress(0.7f)
onSuccess(pr)
} else {
onProgress(0.0f)
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from server")
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from $lnaddress. It should have been $expectedAmountInSats")
}
} ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason ->
onProgress(0.0f)
@@ -7,6 +7,8 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.SignerType
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
import com.vitorpamplona.amethyst.ui.note.showAmount
@@ -32,27 +34,57 @@ class EventNotificationConsumer(private val applicationContext: Context) {
// Test with all logged in accounts
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null && acc.keyPair.privKey != null) {
if (acc != null && (acc.keyPair.privKey != null || acc.loginWithExternalSigner)) {
consumeIfMatchesAccount(event, acc)
}
}
}
private suspend fun consumeIfMatchesAccount(pushWrappedEvent: GiftWrapEvent, account: Account) {
val key = account.keyPair.privKey ?: return
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
val key = account.keyPair.privKey
if (account.loginWithExternalSigner) {
ExternalSignerUtils.account = account
var cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
pushWrappedEvent.content,
pushWrappedEvent.pubKey,
pushWrappedEvent.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id] ?: ""
}
pushWrappedEvent.unwrap(cached)?.let { notificationEvent ->
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
LocalCache.justConsume(notificationEvent, null)
LocalCache.justConsume(notificationEvent, null)
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
if (innerEvent is PrivateDmEvent) {
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
notify(innerEvent, account)
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
if (innerEvent is PrivateDmEvent) {
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
notify(innerEvent, account)
}
}
}
} else if (key != null) {
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
LocalCache.justConsume(notificationEvent, null)
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
if (innerEvent is PrivateDmEvent) {
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
notify(innerEvent, account)
}
}
}
}
@@ -63,17 +95,54 @@ class EventNotificationConsumer(private val applicationContext: Context) {
return when (event) {
is GiftWrapEvent -> {
val key = account.keyPair.privKey ?: return null
event.cachedGift(key)?.let {
unwrapAndConsume(it, account)
val key = account.keyPair.privKey
if (key != null) {
event.cachedGift(key)?.let {
unwrapAndConsume(it, account)
}
} else if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
event.content,
event.pubKey,
event.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
}
event.cachedGift(account.keyPair.pubKey, cached)?.let {
unwrapAndConsume(it, account)
}
} else {
null
}
}
is SealedGossipEvent -> {
val key = account.keyPair.privKey ?: return null
event.cachedGossip(key)?.let {
// this is not verifiable
LocalCache.justConsume(it, null)
it
val key = account.keyPair.privKey
if (key != null) {
event.cachedGossip(key)?.let {
// this is not verifiable
LocalCache.justConsume(it, null)
it
}
} else if (account.loginWithExternalSigner) {
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
event.content,
event.pubKey,
event.id,
SignerType.NIP44_DECRYPT
)
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
}
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
LocalCache.justConsume(it, null)
it
}
} else {
null
}
}
else -> {
@@ -154,7 +223,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
if (acc != null && acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
if (acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
val amount = showAmount(event.amount)
val senderInfo = (noteZapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
@@ -4,6 +4,7 @@ import android.util.Log
import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.quartz.events.RelayAuthEvent
import kotlinx.coroutines.Dispatchers
@@ -23,7 +24,10 @@ class RegisterAccounts(
): List<RelayAuthEvent> {
return accounts.mapNotNull {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null) {
if (acc != null && (acc.isWriteable() || acc.loginWithExternalSigner)) {
if (acc.loginWithExternalSigner) {
ExternalSignerUtils.account = acc
}
val relayToUse = acc.activeRelays()?.firstOrNull { it.read }
if (relayToUse != null) {
acc.createAuthEvent(relayToUse, notificationToken)
@@ -36,7 +40,7 @@ class RegisterAccounts(
}
}
private fun postRegistrationEvent(events: List<RelayAuthEvent>) {
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
try {
val jsonObject = """{
"events": [ ${events.joinToString(", ") { it.toJson() }} ]
@@ -66,5 +70,6 @@ class RegisterAccounts(
postRegistrationEvent(
signEventsToProveControlOfAccounts(accounts, notificationToken)
)
PushNotificationUtils.hasInit = true
}
}
@@ -6,6 +6,7 @@ import kotlinx.coroutines.withContext
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.lang.IllegalArgumentException
private const val ELEMENT_TAG_META = "meta"
private const val ATTRIBUTE_VALUE_PROPERTY = "property"
@@ -64,7 +65,11 @@ suspend fun getDocument(url: String, timeOut: Int = 30000): Document =
withContext(Dispatchers.IO) {
val request: Request = Request.Builder().url(url).get().build()
val html = HttpClient.getHttpClient().newCall(request).execute().use {
it.body.string()
if (it.isSuccessful) {
it.body.string()
} else {
throw IllegalArgumentException("Website returned: " + it.code)
}
}
Jsoup.parse(html)
@@ -16,13 +16,14 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting
@@ -51,9 +52,10 @@ import java.nio.charset.StandardCharsets
class MainActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
ExternalSignerUtils.start(this)
super.onCreate(savedInstanceState)
LocalPreferences.migrateSingleUserPrefs()
val language = LocalPreferences.getPreferredLanguage()
if (language.isNotBlank()) {
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(language)
@@ -68,7 +70,7 @@ class MainActivity : AppCompatActivity() {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
color = MaterialTheme.colorScheme.background
) {
val accountStateViewModel: AccountStateViewModel = viewModel {
AccountStateViewModel(this@MainActivity)
@@ -99,11 +101,13 @@ class MainActivity : AppCompatActivity() {
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.start(this@MainActivity)
if (ServiceManager.shouldPauseService) {
ServiceManager.start(this@MainActivity)
}
}
GlobalScope.launch(Dispatchers.IO) {
PushNotificationUtils().init(LocalPreferences.allSavedAccounts())
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
}
}
@@ -113,7 +117,9 @@ class MainActivity : AppCompatActivity() {
debugState(this)
// }
ServiceManager.pause()
if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
}
super.onPause()
}
@@ -145,8 +151,10 @@ class MainActivity : AppCompatActivity() {
Log.d("NETWORKCALLBACK", "onAvailable: Disconnecting and connecting again")
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.pause()
ServiceManager.start(this@MainActivity)
if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
ServiceManager.start(this@MainActivity)
}
}
}
@@ -176,7 +184,9 @@ class MainActivity : AppCompatActivity() {
Log.d("NETWORKCALLBACK", "onLost: Disconnecting and pausing relay's connection")
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.pause()
if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
}
}
}
}
@@ -16,15 +16,15 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -54,10 +54,12 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.note.ChannelName
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.SearchIcon
@@ -133,7 +135,7 @@ fun JoinUserOrChannelView(searchBarViewModel: SearchBarViewModel, onClose: () ->
Text(
text = "",
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
fontWeight = FontWeight.Bold
)
}
@@ -194,7 +196,7 @@ private fun RenderSearch(
}
}
DisposableEffect(Unit) {
DisposableEffect(lifeCycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("Join Start")
@@ -270,7 +272,7 @@ private fun SearchEditTextForJoin(
placeholder = {
Text(
text = stringResource(R.string.channel_list_user_or_group_id_demo),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
trailingIcon = {
@@ -303,6 +305,13 @@ private fun RenderSearchResults(
val users by searchBarViewModel.searchResultsUsers.collectAsState()
val channels by searchBarViewModel.searchResultsChannels.collectAsState()
val scope = rememberCoroutineScope()
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
Row(
modifier = Modifier
@@ -337,7 +346,7 @@ private fun RenderSearchResults(
channels,
key = { _, item -> "c" + item.idHex }
) { _, item ->
RenderChannel(item) {
RenderChannel(item, automaticallyShowProfilePicture) {
nav("Channel/${item.idHex}")
searchBarViewModel.clear()
}
@@ -350,6 +359,7 @@ private fun RenderSearchResults(
@Composable
private fun RenderChannel(
item: com.vitorpamplona.amethyst.model.Channel,
loadProfilePicture: Boolean,
onClick: () -> Unit
) {
val hasNewMessages = remember {
@@ -368,7 +378,8 @@ private fun RenderChannel(
channelLastTime = null,
channelLastContent = item.summary(),
hasNewMessages,
onClick = onClick
onClick = onClick,
loadProfilePicture = loadProfilePicture
)
}
@@ -426,7 +437,7 @@ private fun DisplayUserAboutInfo(baseUser: User) {
Text(
text = about,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -10,11 +10,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -75,7 +75,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
placeholder = {
Text(
text = stringResource(R.string.my_awesome_group),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -94,7 +94,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
placeholder = {
Text(
text = "http://mygroup.com/logo.jpg",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -111,7 +111,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
placeholder = {
Text(
text = stringResource(R.string.about_us),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -26,7 +26,7 @@ open class NewMediaModel : ViewModel() {
var mediaType by mutableStateOf<String?>(null)
var selectedServer by mutableStateOf<ServersAvailable?>(null)
var description by mutableStateOf("")
var alt by mutableStateOf("")
var sensitiveContent by mutableStateOf(false)
// Images and Videos
@@ -79,7 +79,7 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.2f
uploadingDescription.value = "Loading"
contentResolver.openInputStream(fileUri)?.use {
createNIP95Record(it.readBytes(), contentType, description, sensitiveContent, relayList = relayList)
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
}
?: run {
viewModelScope.launch {
@@ -92,24 +92,32 @@ open class NewMediaModel : ViewModel() {
} else {
uploadingPercentage.value = 0.2f
uploadingDescription.value = "Uploading"
ImageUploader.uploadImage(
uri = fileUri,
contentType = contentType,
size = size,
server = serverToUse,
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
createNIP94Record(imageUrl, mimeType, description, sensitiveContent, relayList = relayList)
},
onError = {
isUploadingImage = false
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
viewModelScope.launch(Dispatchers.IO) {
ImageUploader.uploadImage(
uri = fileUri,
contentType = contentType,
size = size,
server = serverToUse,
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
createNIP94Record(
imageUrl,
mimeType,
alt,
sensitiveContent,
relayList = relayList
)
},
onError = {
isUploadingImage = false
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
}
}
}
)
)
}
}
},
onError = {
@@ -131,7 +139,7 @@ open class NewMediaModel : ViewModel() {
uploadingDescription.value = null
uploadingPercentage.value = 0.0f
description = ""
alt = ""
selectedServer = account?.defaultFileServer
}
@@ -139,7 +147,7 @@ open class NewMediaModel : ViewModel() {
return !isUploadingImage && galleryUri != null && selectedServer != null
}
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
fun createNIP94Record(imageUrl: String, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
uploadingPercentage.value = 0.40f
viewModelScope.launch(Dispatchers.IO) {
uploadingDescription.value = "Server Processing"
@@ -158,7 +166,7 @@ open class NewMediaModel : ViewModel() {
imageData,
imageUrl,
mimeType,
description,
alt,
sensitiveContent,
onReady = {
uploadingPercentage.value = 0.90f
@@ -192,7 +200,7 @@ open class NewMediaModel : ViewModel() {
}
}
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
uploadingPercentage.value = 0.30f
uploadingDescription.value = "Hashing"
@@ -201,7 +209,7 @@ open class NewMediaModel : ViewModel() {
bytes,
"",
mimeType,
description,
alt,
sensitiveContent,
onReady = {
uploadingDescription.value = "Signing"
@@ -21,12 +21,12 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.model.ServersAvailable
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.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -134,7 +135,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
painter = painterResource(R.drawable.relays),
contentDescription = null,
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
}
}
@@ -186,8 +187,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
)
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() }
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
val resolver = LocalContext.current.contentResolver
Row(
@@ -252,7 +252,6 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
label = stringResource(id = R.string.file_server),
placeholder = fileServers.firstOrNull { it.first == accountViewModel.account.defaultFileServer }?.second ?: fileServers[0].second,
options = fileServerOptions,
explainers = fileServerExplainers,
onSelect = {
postViewModel.selectedServer = fileServers[it].first
},
@@ -288,12 +287,12 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
value = postViewModel.description,
onValueChange = { postViewModel.description = it },
value = postViewModel.alt,
onValueChange = { postViewModel.alt = it },
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -1,6 +1,6 @@
package com.vitorpamplona.amethyst.ui.actions
import com.vitorpamplona.amethyst.model.LocalCache
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.crypto.KeyPair
@@ -13,7 +13,8 @@ class NewMessageTagger(
var message: String,
var mentions: List<User>? = null,
var replyTos: List<Note>? = null,
var channelHex: String? = null
var channelHex: String? = null,
var dao: Dao
) {
val directMentions = mutableSetOf<HexKey>()
@@ -40,20 +41,20 @@ class NewMessageTagger(
return (if (channelHex != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
}
fun run() {
suspend fun run() {
// adds all references to mentions and reply tos
message.split('\n').forEach { paragraph: String ->
paragraph.split(' ').forEach { word: String ->
val results = parseDirtyWordForKey(word)
if (results?.key?.type == Nip19.Type.USER) {
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
addUserToMentions(dao.getOrCreateUser(results.key.hex))
} else if (results?.key?.type == Nip19.Type.NOTE) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
addNoteToReplyTos(dao.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.EVENT) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
addNoteToReplyTos(dao.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
val note = dao.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
addNoteToReplyTos(note)
}
@@ -66,19 +67,19 @@ class NewMessageTagger(
paragraph.split(' ').map { word: String ->
val results = parseDirtyWordForKey(word)
if (results?.key?.type == Nip19.Type.USER) {
val user = LocalCache.getOrCreateUser(results.key.hex)
val user = dao.getOrCreateUser(results.key.hex)
"nostr:${user.pubkeyNpub()}${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.NOTE) {
val note = LocalCache.getOrCreateNote(results.key.hex)
val note = dao.getOrCreateNote(results.key.hex)
"nostr:${note.toNEvent()}${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.EVENT) {
val note = LocalCache.getOrCreateNote(results.key.hex)
val note = dao.getOrCreateNote(results.key.hex)
"nostr:${note.toNEvent()}${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
val note = dao.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
"nostr:${note.idNote()}${results.restOfWord}"
} else {
@@ -91,6 +92,7 @@ class NewMessageTagger(
}.joinToString("\n")
}
@Immutable
data class DirtyKeyInfo(val key: Nip19.Return, val restOfWord: String)
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
@@ -101,37 +103,50 @@ class NewMessageTagger(
key = key.removePrefix("@")
if (key.length < 63) {
return null
}
try {
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
if (key.startsWith("nsec1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
// Converts to npub
val pubkey = Nip19.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("npub1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
val pubkey = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("note1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
val noteId = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(noteId, restOfWord)
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val pubkeyRelay = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
} else if (key.startsWith("nevent1", true)) {
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val noteRelayId = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
} else if (key.startsWith("naddr1", true)) {
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val address = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(address, address.additionalChars) // no way to know when they address ends and dirt begins
}
@@ -142,3 +157,9 @@ class NewMessageTagger(
return null
}
}
interface Dao {
suspend fun getOrCreateUser(hex: String): User
suspend fun getOrCreateNote(hex: String): Note
suspend fun checkGetOrCreateAddressableNote(hex: String): Note?
}
@@ -5,10 +5,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -38,13 +38,13 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
} catch (e: Exception) { pollViewModel.isValidClosedAt.value = false }
}
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.error,
val colorInValid = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.error,
unfocusedBorderColor = Color.Red
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.placeholderText
val colorValid = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText
)
Row(
@@ -60,13 +60,13 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_closing_time),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_closing_time_days),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -5,10 +5,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -38,13 +38,13 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
} catch (e: Exception) { pollViewModel.isValidConsensusThreshold.value = false }
}
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.error,
val colorInValid = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.error,
unfocusedBorderColor = Color.Red
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.placeholderText
val colorValid = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText
)
Row(
@@ -60,13 +60,13 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_consensus_threshold),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_consensus_threshold_percent),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -36,13 +36,13 @@ fun NewPollOption(pollViewModel: NewPostViewModel, optionIndex: Int) {
label = {
Text(
text = stringResource(R.string.poll_option_index).format(optionIndex + 1),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_option_description),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -33,13 +33,13 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
isInputValid = false
}
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.error,
val colorInValid = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.error,
unfocusedBorderColor = Color.Red
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.placeholderText
val colorValid = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText
)
OutlinedTextField(
@@ -50,7 +50,7 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_primary_description),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -68,11 +68,11 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
placeholder = {
Text(
text = stringResource(R.string.poll_primary_description),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
colors = if (isInputValid) colorValid else colorInValid,
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
@@ -1,9 +1,9 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material3.MaterialTheme
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
@@ -30,13 +30,13 @@ fun NewPollRecipientsField(pollViewModel: NewPostViewModel, account: Account) {
label = {
Text(
text = stringResource(R.string.poll_zap_recipients),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_zap_recipients),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
@@ -7,10 +7,10 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@@ -26,13 +26,13 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.error,
val colorInValid = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.error,
unfocusedBorderColor = Color.Red
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.placeholderText
val colorValid = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText
)
Row(
@@ -48,13 +48,13 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_zap_value_min),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.sats),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -70,13 +70,13 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_zap_value_max),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.sats),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -88,7 +88,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
) {
Text(
text = stringResource(R.string.poll_zap_value_min_max_explainer),
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
}
@@ -9,6 +9,7 @@ import android.util.Size
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -18,7 +19,6 @@ import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.Bolt
@@ -31,6 +31,7 @@ import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -45,7 +46,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@@ -81,21 +81,27 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
import com.vitorpamplona.amethyst.service.startsWithNIP19Scheme
import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.CancelIcon
import com.vitorpamplona.amethyst.ui.note.CloseIcon
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.PollIcon
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -111,7 +117,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewPostView(
onClose: () -> Unit,
@@ -136,7 +144,7 @@ fun NewPostView(
var relayList = account.activeWriteRelays()
LaunchedEffect(Unit) {
postViewModel.load(account, baseReplyTo, quote)
postViewModel.load(accountViewModel, baseReplyTo, quote)
launch(Dispatchers.IO) {
postViewModel.imageUploadingError.collect { error ->
@@ -180,9 +188,7 @@ fun NewPostView(
TopAppBar(
title = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(end = 10.dp),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -199,14 +205,15 @@ fun NewPostView(
painter = painterResource(R.drawable.relays),
contentDescription = null,
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
}
}
PostButton(
onPost = {
scope.launch(Dispatchers.IO) {
postViewModel.sendPost(relayList = relayList)
postViewModel.sendPost(relayList = relayList)
scope.launch {
delay(100)
onClose()
}
},
@@ -215,14 +222,20 @@ fun NewPostView(
}
},
navigationIcon = {
Spacer(modifier = StdHorzSpacer)
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})
Row() {
Spacer(modifier = StdHorzSpacer)
CloseButton(onPress = {
postViewModel.cancel()
scope.launch {
delay(100)
onClose()
}
})
}
},
backgroundColor = MaterialTheme.colors.surface,
elevation = 0.dp
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { pad ->
@@ -243,7 +256,6 @@ fun NewPostView(
) {
Column(
modifier = Modifier
.padding(top = 10.dp)
.imePadding()
.weight(1f)
) {
@@ -264,7 +276,7 @@ fun NewPostView(
makeItShort = true,
unPackReply = false,
isQuotedNote = true,
modifier = MaterialTheme.colors.replyModifier,
modifier = MaterialTheme.colorScheme.replyModifier,
accountViewModel = accountViewModel,
nav = nav
)
@@ -321,9 +333,9 @@ fun NewPostView(
if (postViewModel.wantsForwardZapTo) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
modifier = Modifier.padding(top = Size5dp, bottom = Size5dp, start = Size10dp)
) {
FowardZapTo(postViewModel)
FowardZapTo(postViewModel, accountViewModel)
}
}
@@ -333,8 +345,8 @@ fun NewPostView(
ImageVideoDescription(
url,
account.defaultFileServer,
onAdd = { description, server, sensitiveContent ->
postViewModel.upload(url, description, sensitiveContent, server, context, relayList)
onAdd = { alt, server, sensitiveContent ->
postViewModel.upload(url, alt, sensitiveContent, server, context, relayList)
account.changeDefaultFileServer(server)
},
onCancel = {
@@ -400,7 +412,7 @@ fun NewPostView(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
)
)
@@ -410,7 +422,7 @@ fun NewPostView(
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
}
} else if (startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colors.background
val bgColor = MaterialTheme.colorScheme.background
val backgroundColor = remember {
mutableStateOf(bgColor)
}
@@ -457,7 +469,7 @@ fun NewPostView(
) {
UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
) {
postViewModel.selectImage(it)
@@ -521,10 +533,10 @@ private fun PollField(postViewModel: NewPostViewModel) {
},
border = BorderStroke(
1.dp,
MaterialTheme.colors.placeholderText
MaterialTheme.colorScheme.placeholderText
),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.placeholderText
containerColor = MaterialTheme.colorScheme.placeholderText
)
) {
Image(
@@ -536,7 +548,7 @@ private fun PollField(postViewModel: NewPostViewModel) {
}
}
@OptIn(ExperimentalComposeUiApi::class)
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun MessageField(
postViewModel: NewPostViewModel
@@ -563,7 +575,7 @@ private fun MessageField(
.fillMaxWidth()
.border(
width = 1.dp,
color = MaterialTheme.colors.surface,
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(8.dp)
)
.focusRequester(focusRequester)
@@ -575,15 +587,14 @@ private fun MessageField(
placeholder = {
Text(
text = stringResource(R.string.what_s_on_your_mind),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
colors = TextFieldDefaults
.outlinedTextFieldColors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
@@ -634,7 +645,7 @@ fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) {
Text(
text = stringResource(R.string.add_sensitive_content_explainer),
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
}
@@ -664,18 +675,16 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
placeholder = {
Text(
text = stringResource(R.string.messages_new_message_to_caption),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
visualTransformation = UrlUserTagTransformation(
MaterialTheme.colors.primary
MaterialTheme.colorScheme.primary
),
colors = TextFieldDefaults
.outlinedTextFieldColors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
colors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
)
)
}
@@ -701,18 +710,16 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
placeholder = {
Text(
text = stringResource(R.string.messages_new_message_subject_caption),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
visualTransformation = UrlUserTagTransformation(
MaterialTheme.colors.primary
MaterialTheme.colorScheme.primary
),
colors = TextFieldDefaults
.outlinedTextFieldColors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
colors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
)
)
}
@@ -721,7 +728,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
}
@Composable
fun FowardZapTo(postViewModel: NewPostViewModel) {
fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewModel) {
Column(
modifier = Modifier.fillMaxWidth()
) {
@@ -755,7 +762,7 @@ fun FowardZapTo(postViewModel: NewPostViewModel) {
}
Text(
text = stringResource(R.string.zap_forward_title),
text = stringResource(R.string.zap_split_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
@@ -765,28 +772,58 @@ fun FowardZapTo(postViewModel: NewPostViewModel) {
Divider()
Text(
text = stringResource(R.string.zap_forward_explainer),
color = MaterialTheme.colors.placeholderText,
text = stringResource(R.string.zap_split_explainer),
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
postViewModel.forwardZapTo.items.forEachIndexed { index, splitItem ->
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size10dp)) {
BaseUserPicture(splitItem.key, Size55dp, accountViewModel = accountViewModel)
Spacer(modifier = DoubleHorzSpacer)
Column(modifier = Modifier.weight(1f)) {
UsernameDisplay(splitItem.key, showPlayButton = false)
Text(
text = String.format("%.0f%%", splitItem.percentage * 100),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
}
Spacer(modifier = DoubleHorzSpacer)
Slider(
value = splitItem.percentage,
onValueChange = { sliderValue ->
val rounded = (round(sliderValue * 20)) / 20.0f
postViewModel.updateZapPercentage(index, rounded)
},
modifier = Modifier
.weight(1.5f)
)
}
}
OutlinedTextField(
value = postViewModel.forwardZapToEditting,
onValueChange = {
postViewModel.updateZapForwardTo(it)
},
label = { Text(text = stringResource(R.string.zap_forward_lnAddress)) },
label = { Text(text = stringResource(R.string.zap_split_serarch_and_add_user)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.zap_forward_lnAddress),
color = MaterialTheme.colors.placeholderText
text = stringResource(R.string.zap_split_serarch_and_add_user_placeholder),
color = MaterialTheme.colorScheme.placeholderText
)
},
singleLine = true,
visualTransformation = UrlUserTagTransformation(
MaterialTheme.colors.primary
MaterialTheme.colorScheme.primary
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
@@ -834,7 +871,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
imageVector = Icons.Default.LocationOn,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}
@@ -854,7 +891,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
Text(
text = stringResource(R.string.geohash_explainer),
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
}
@@ -915,7 +952,7 @@ fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
Text(
stringResource(R.string.reply_notify),
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.align(CenterVertically)
)
@@ -929,7 +966,7 @@ fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
Button(
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.mediumImportanceLink
containerColor = MaterialTheme.colorScheme.mediumImportanceLink
),
onClick = {
onClick(myUser)
@@ -988,7 +1025,7 @@ private fun AddZapraiserButton(
modifier = Modifier
.size(20.dp)
.align(Alignment.TopStart),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
Icon(
imageVector = Icons.Default.Bolt,
@@ -996,7 +1033,7 @@ private fun AddZapraiserButton(
modifier = Modifier
.size(13.dp)
.align(Alignment.BottomEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1032,14 +1069,14 @@ fun AddGeoHash(postViewModel: NewPostViewModel, onClick: () -> Unit) {
imageVector = Icons.Default.LocationOff,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
imageVector = Icons.Default.LocationOn,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}
}
@@ -1060,7 +1097,7 @@ private fun AddLnInvoiceButton(
imageVector = Icons.Default.CurrencyBitcoin,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1095,7 +1132,7 @@ private fun ForwardZapTo(
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterStart),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
Icon(
imageVector = Icons.Default.ArrowForwardIos,
@@ -1103,7 +1140,7 @@ private fun ForwardZapTo(
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1149,7 +1186,7 @@ private fun MarkAsSensitive(
modifier = Modifier
.size(18.dp)
.align(Alignment.BottomStart),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
Icon(
imageVector = Icons.Rounded.Warning,
@@ -1157,7 +1194,7 @@ private fun MarkAsSensitive(
modifier = Modifier
.size(10.dp)
.align(Alignment.TopEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1183,15 +1220,9 @@ private fun MarkAsSensitive(
@Composable
fun CloseButton(onPress: () -> Unit) {
Button(
onClick = {
onPress()
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = Color.Gray
)
OutlinedButton(
onClick = onPress,
contentPadding = PaddingValues(horizontal = Size5dp)
) {
CloseIcon()
}
@@ -1201,17 +1232,11 @@ fun CloseButton(onPress: () -> Unit) {
fun PostButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) {
Button(
modifier = modifier,
enabled = isActive,
onClick = {
if (isActive) {
onPost()
}
onPost()
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
),
contentPadding = PaddingValues(0.dp)
shape = ButtonBorder
) {
Text(text = stringResource(R.string.post), color = Color.White)
}
@@ -1220,19 +1245,11 @@ fun PostButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier =
@Composable
fun SaveButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) {
Button(
enabled = isActive,
modifier = modifier,
onClick = {
if (isActive) {
onPost()
}
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
)
onClick = onPost
) {
Text(text = stringResource(R.string.save), color = Color.White)
Text(text = stringResource(R.string.save))
}
}
@@ -1246,10 +1263,9 @@ fun CreateButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
}
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
)
colors = ButtonDefaults.buttonColors(
containerColor = if (isActive) MaterialTheme.colorScheme.primary else Color.Gray
)
) {
Text(text = stringResource(R.string.create), color = Color.White)
}
@@ -1284,8 +1300,7 @@ fun ImageVideoDescription(
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
)
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() }
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
var selectedServer by remember { mutableStateOf(defaultServer) }
var message by remember { mutableStateOf("") }
@@ -1298,7 +1313,7 @@ fun ImageVideoDescription(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
)
) {
@@ -1399,7 +1414,6 @@ fun ImageVideoDescription(
label = stringResource(id = R.string.file_server),
placeholder = fileServers.filter { it.first == defaultServer }.firstOrNull()?.second ?: fileServers[0].second,
options = fileServerOptions,
explainers = fileServerExplainers,
onSelect = {
selectedServer = fileServers[it].first
},
@@ -1440,7 +1454,7 @@ fun ImageVideoDescription(
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -1459,7 +1473,7 @@ fun ImageVideoDescription(
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(text = stringResource(R.string.add_content), color = Color.White, fontSize = 20.sp)
@@ -1493,21 +1507,17 @@ fun SettingSwitchItem(
modifier = Modifier.weight(1.0f),
verticalArrangement = Arrangement.spacedBy(3.dp)
) {
val contentAlpha = if (enabled) ContentAlpha.high else ContentAlpha.disabled
Text(
text = stringResource(id = title),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(contentAlpha)
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(id = description),
style = MaterialTheme.typography.caption,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(contentAlpha)
overflow = TextOverflow.Ellipsis
)
}
@@ -21,7 +21,9 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
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.components.isValidURL
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
@@ -29,6 +31,7 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.ZapSplitSetup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -43,6 +46,7 @@ enum class UserSuggestionAnchor {
@Stable
open class NewPostViewModel() : ViewModel() {
var accountViewModel: AccountViewModel? = null
var account: Account? = null
var requiresNIP24: Boolean = false
@@ -88,9 +92,13 @@ open class NewPostViewModel() : ViewModel() {
var canAddInvoice by mutableStateOf(false)
var wantsInvoice by mutableStateOf(false)
data class ForwardZapSetup(val user: User) {
var percentage by mutableStateOf(100)
}
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
var forwardZapTo by mutableStateOf<User?>(null)
var forwardZapTo by mutableStateOf<Split<User>>(Split())
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
// NSFW, Sensitive
@@ -109,7 +117,7 @@ open class NewPostViewModel() : ViewModel() {
// NIP24 Wrapped DMs / Group messages
var nip24 by mutableStateOf(false)
open fun load(account: Account, replyingTo: Note?, quote: Note?) {
open fun load(accountViewModel: AccountViewModel, replyingTo: Note?, quote: Note?) {
originalNote = replyingTo
replyingTo?.let { replyNote ->
if (replyNote.event is BaseTextNoteEvent) {
@@ -141,8 +149,8 @@ open class NewPostViewModel() : ViewModel() {
urlPreview = findUrlInMessage()
}
canAddInvoice = account.userProfile().info?.lnAddress() != null
canAddZapRaiser = account.userProfile().info?.lnAddress() != null
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
contentToAddUrl = null
@@ -151,25 +159,40 @@ open class NewPostViewModel() : ViewModel() {
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
forwardZapTo = null
forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("")
this.account = account
this.accountViewModel = accountViewModel
this.account = accountViewModel.account
}
fun sendPost(relayList: List<Relay>? = null) {
val tagger = NewMessageTagger(message.text, mentions, replyTos, originalNote?.channelHex())
viewModelScope.launch(Dispatchers.IO) {
innerSendPost(relayList)
}
}
suspend fun innerSendPost(relayList: List<Relay>? = null) {
if (accountViewModel == null) {
cancel()
return
}
val tagger = NewMessageTagger(message.text, mentions, replyTos, originalNote?.channelHex(), accountViewModel!!)
tagger.run()
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null)
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null, accountViewModel!!)
toUsersTagger.run()
val dmUsers = toUsersTagger.mentions
val zapReceiver = if (wantsForwardZapTo) {
if (forwardZapTo != null) {
forwardZapTo?.info?.lud16 ?: forwardZapTo?.info?.lud06
} else {
forwardZapToEditting.text
forwardZapTo?.items?.map {
ZapSplitSetup(
lnAddressOrPubKeyHex = it.key.pubkeyHex,
relay = it.key.relaysBeingUsed.keys.firstOrNull(),
weight = it.percentage.toDouble(),
isLnAddress = false
)
}
} else {
null
@@ -276,7 +299,7 @@ open class NewPostViewModel() : ViewModel() {
cancel()
}
fun upload(galleryUri: Uri, description: String, sensitiveContent: Boolean, server: ServersAvailable, context: Context, relayList: List<Relay>? = null) {
fun upload(galleryUri: Uri, alt: String, sensitiveContent: Boolean, server: ServersAvailable, context: Context, relayList: List<Relay>? = null) {
isUploadingImage = true
contentToAddUrl = null
@@ -291,7 +314,7 @@ open class NewPostViewModel() : ViewModel() {
onReady = { fileUri, contentType, size ->
if (server == ServersAvailable.NIP95) {
contentResolver.openInputStream(fileUri)?.use {
createNIP95Record(it.readBytes(), contentType, description, sensitiveContent, relayList = relayList)
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
}
} else {
ImageUploader.uploadImage(
@@ -302,7 +325,7 @@ open class NewPostViewModel() : ViewModel() {
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
if (isNIP94Server(server)) {
createNIP94Record(imageUrl, mimeType, description, sensitiveContent)
createNIP94Record(imageUrl, mimeType, alt, sensitiveContent)
} else {
isUploadingImage = false
message = TextFieldValue(message.text + "\n\n" + imageUrl)
@@ -355,7 +378,7 @@ open class NewPostViewModel() : ViewModel() {
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
forwardZapTo = null
forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("")
userSuggestions = emptyList()
@@ -427,10 +450,10 @@ open class NewPostViewModel() : ViewModel() {
open fun updateZapForwardTo(it: TextFieldValue) {
forwardZapToEditting = it
if (it.selection.collapsed) {
val lastWord = it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWord = it.text
userSuggestionAnchor = it.selection
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
if (lastWord.startsWith("@") && lastWord.length > 2) {
if (lastWord.length > 2) {
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
viewModelScope.launch(Dispatchers.IO) {
userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
@@ -460,6 +483,9 @@ open class NewPostViewModel() : ViewModel() {
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
)
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo?.addItem(item)
forwardZapToEditting = TextFieldValue("")
/*
val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length
val wordToInsert = "@${item.pubkeyNpub()}"
@@ -468,7 +494,7 @@ open class NewPostViewModel() : ViewModel() {
forwardZapToEditting = TextFieldValue(
forwardZapToEditting.text.replaceRange(lastWordStart, it.end, wordToInsert),
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
)
)*/
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
val lastWord = toUsers.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length
@@ -511,13 +537,13 @@ open class NewPostViewModel() : ViewModel() {
}
}
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
fun createNIP94Record(imageUrl: String, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
viewModelScope.launch(Dispatchers.IO) {
// Images don't seem to be ready immediately after upload
FileHeader.prepare(
imageUrl,
mimeType,
description,
alt,
sensitiveContent,
onReady = {
val note = account?.sendHeader(it, relayList = relayList)
@@ -542,13 +568,13 @@ open class NewPostViewModel() : ViewModel() {
}
}
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
viewModelScope.launch(Dispatchers.IO) {
FileHeader.prepare(
bytes,
"",
mimeType,
description,
alt,
sensitiveContent,
onReady = {
val nip95 = account?.createNip95(bytes, headerInfo = it)
@@ -656,6 +682,10 @@ open class NewPostViewModel() : ViewModel() {
isValidvalueMaximum.value = true
}
}
fun updateZapPercentage(index: Int, sliderValue: Float) {
forwardZapTo?.updatePercentage(index, sliderValue)
}
}
enum class GeohashPrecision(val digits: Int) {
@@ -14,16 +14,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.DeleteSweep
@@ -34,6 +24,18 @@ import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SyncProblem
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -56,9 +58,11 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays
import com.vitorpamplona.amethyst.service.relays.FeedType
@@ -81,6 +85,7 @@ import com.vitorpamplona.amethyst.ui.theme.warningColor
import kotlinx.coroutines.launch
import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) {
val postViewModel: NewRelayListViewModel = viewModel()
@@ -135,8 +140,9 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
onClose()
})
},
backgroundColor = MaterialTheme.colors.surface,
elevation = 0.dp
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { pad ->
@@ -210,7 +216,7 @@ fun ServerConfigHeader() {
maxLines = 1,
fontSize = Font14SP,
modifier = Modifier.weight(1.2f),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Spacer(modifier = Modifier.size(5.dp))
@@ -220,7 +226,7 @@ fun ServerConfigHeader() {
maxLines = 1,
fontSize = Font14SP,
modifier = Modifier.weight(1.2f),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Spacer(modifier = Modifier.size(5.dp))
@@ -230,7 +236,7 @@ fun ServerConfigHeader() {
maxLines = 1,
fontSize = Font14SP,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Spacer(modifier = Modifier.size(5.dp))
@@ -240,7 +246,7 @@ fun ServerConfigHeader() {
maxLines = 1,
fontSize = Font14SP,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Spacer(modifier = Modifier.size(2.dp))
@@ -258,6 +264,7 @@ fun ServerConfigHeader() {
@Composable
fun ServerConfigPreview() {
ServerConfigClickableLine(
loadProfilePicture = true,
item = RelaySetupInfo(
url = "nostr.mom",
read = true,
@@ -310,7 +317,16 @@ fun ServerConfig(
)
}
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
ServerConfigClickableLine(
loadProfilePicture = automaticallyShowProfilePicture,
item = item,
onToggleDownload = onToggleDownload,
onToggleUpload = onToggleUpload,
@@ -351,6 +367,7 @@ fun ServerConfig(
@Composable
fun ServerConfigClickableLine(
loadProfilePicture: Boolean,
item: RelaySetupInfo,
onToggleDownload: (RelaySetupInfo) -> Unit,
onToggleUpload: (RelaySetupInfo) -> Unit,
@@ -368,7 +385,7 @@ fun ServerConfigClickableLine(
modifier = Modifier.padding(vertical = 5.dp)
) {
Column(Modifier.clickable(onClick = onClick)) {
RenderRelayIcon(iconUrl = item.briefInfo.favIcon, Size55dp)
RenderRelayIcon(iconUrl = item.briefInfo.favIcon, loadProfilePicture, Size55dp)
}
Spacer(modifier = HalfHorzPadding)
@@ -441,9 +458,9 @@ private fun RenderStatusRow(
}
),
tint = if (item.read) {
MaterialTheme.colors.allGoodColor
MaterialTheme.colorScheme.allGoodColor
} else {
MaterialTheme.colors.onSurface.copy(
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.32f
)
}
@@ -454,7 +471,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -477,9 +494,9 @@ private fun RenderStatusRow(
}
),
tint = if (item.write) {
MaterialTheme.colors.allGoodColor
MaterialTheme.colorScheme.allGoodColor
} else {
MaterialTheme.colors.onSurface.copy(
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.32f
)
}
@@ -490,7 +507,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -512,7 +529,7 @@ private fun RenderStatusRow(
}
}
),
tint = if (item.errorCount > 0) MaterialTheme.colors.warningColor else MaterialTheme.colors.allGoodColor
tint = if (item.errorCount > 0) MaterialTheme.colorScheme.warningColor else MaterialTheme.colorScheme.allGoodColor
)
Text(
@@ -520,7 +537,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -542,7 +559,7 @@ private fun RenderStatusRow(
}
}
),
tint = if (item.spamCount > 0) MaterialTheme.colors.warningColor else MaterialTheme.colors.allGoodColor
tint = if (item.spamCount > 0) MaterialTheme.colorScheme.warningColor else MaterialTheme.colorScheme.allGoodColor
)
Text(
@@ -550,7 +567,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
@@ -571,7 +588,7 @@ private fun RenderActiveToggles(
text = stringResource(id = R.string.active_for),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 2.dp, end = 5.dp),
fontSize = 14.sp
)
@@ -601,9 +618,9 @@ private fun RenderActiveToggles(
}
),
tint = if (item.feedTypes.contains(FeedType.FOLLOWS)) {
MaterialTheme.colors.allGoodColor
MaterialTheme.colorScheme.allGoodColor
} else {
MaterialTheme.colors.onSurface.copy(
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.32f
)
}
@@ -634,9 +651,9 @@ private fun RenderActiveToggles(
}
),
tint = if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) {
MaterialTheme.colors.allGoodColor
MaterialTheme.colorScheme.allGoodColor
} else {
MaterialTheme.colors.onSurface.copy(
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.32f
)
}
@@ -667,9 +684,9 @@ private fun RenderActiveToggles(
}
),
tint = if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) {
MaterialTheme.colors.allGoodColor
MaterialTheme.colorScheme.allGoodColor
} else {
MaterialTheme.colors.onSurface.copy(
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.32f
)
}
@@ -700,9 +717,9 @@ private fun RenderActiveToggles(
}
),
tint = if (item.feedTypes.contains(FeedType.GLOBAL)) {
MaterialTheme.colors.allGoodColor
MaterialTheme.colorScheme.allGoodColor
} else {
MaterialTheme.colors.onSurface.copy(
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.32f
)
}
@@ -734,9 +751,9 @@ private fun RenderActiveToggles(
}
),
tint = if (item.feedTypes.contains(FeedType.SEARCH)) {
MaterialTheme.colors.allGoodColor
MaterialTheme.colorScheme.allGoodColor
} else {
MaterialTheme.colors.onSurface.copy(
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.32f
)
}
@@ -767,7 +784,7 @@ private fun FirstLine(
modifier = Modifier
.padding(start = 5.dp, top = 1.dp)
.size(14.dp),
tint = MaterialTheme.colors.allGoodColor
tint = MaterialTheme.colorScheme.allGoodColor
)
}
}
@@ -803,7 +820,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
placeholder = {
Text(
text = "server.com",
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1
)
},
@@ -817,7 +834,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
modifier = Modifier
.size(Size35dp)
.padding(horizontal = 5.dp),
tint = if (read) MaterialTheme.colors.allGoodColor else MaterialTheme.colors.placeholderText
tint = if (read) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.placeholderText
)
}
@@ -828,7 +845,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
modifier = Modifier
.size(Size35dp)
.padding(horizontal = 5.dp),
tint = if (write) MaterialTheme.colors.allGoodColor else MaterialTheme.colors.placeholderText
tint = if (write) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.placeholderText
)
}
@@ -846,7 +863,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (url.isNotBlank()) MaterialTheme.colors.primary else MaterialTheme.colors.placeholderText
containerColor = if (url.isNotBlank()) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.placeholderText
)
) {
Text(text = stringResource(id = R.string.add), color = Color.White)
@@ -57,21 +57,30 @@ class NewRelayListViewModel : ViewModel() {
_relays.update {
var relayFile = account.userProfile().latestContactList?.relays()
// Ugly, but forces nostr.band as the only search-supporting relay today.
// TODO: Remove when search becomes more available.
if (relayFile?.none { it.key.removeSuffix("/") in Constants.forcedRelaysForSearchSet } == true) {
relayFile = relayFile + Constants.forcedRelayForSearch.map {
Pair(
it.url,
ContactListEvent.ReadWrite(it.read, it.write)
)
}
}
if (relayFile != null) {
// Ugly, but forces nostr.band as the only search-supporting relay today.
// TODO: Remove when search becomes more available.
val needsSearchRelay = relayFile.none {
it.key.removeSuffix("/") in Constants.forcedRelaysForSearchSet
} && relayFile.none {
account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes?.contains(FeedType.SEARCH) ?: false
}
if (needsSearchRelay) {
relayFile = relayFile + Constants.forcedRelayForSearch.map {
Pair(
it.url,
ContactListEvent.ReadWrite(it.read, it.write)
)
}
}
relayFile.map {
val liveRelay = RelayPool.getRelay(it.key)
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes ?: FeedType.values().toSet().toImmutableSet()
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes
?: Constants.defaultRelays.filter { defaultRelay -> defaultRelay.url == it.key }.firstOrNull()?.feedTypes
?: FeedType.values().toSet().toImmutableSet()
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
@@ -11,10 +11,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@@ -82,45 +82,26 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
}
Column(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())
modifier = Modifier
.padding(10.dp)
.verticalScroll(rememberScrollState())
) {
Row(
modifier = Modifier.fillMaxWidth(1f),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.display_name)) },
modifier = Modifier.weight(1f),
value = postViewModel.displayName.value,
onValueChange = { postViewModel.displayName.value = it },
placeholder = {
Text(
text = stringResource(R.string.my_display_name),
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true
)
Text("@", Modifier.padding(5.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.username)) },
modifier = Modifier.weight(1f),
value = postViewModel.userName.value,
onValueChange = { postViewModel.userName.value = it },
placeholder = {
Text(
text = stringResource(R.string.my_username),
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true
)
}
OutlinedTextField(
label = { Text(text = stringResource(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),
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
@@ -134,7 +115,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(id = R.string.about_me),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -153,13 +134,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "https://mywebsite.com/me.jpg",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
leadingIcon = {
UploadFromGallery(
isUploading = postViewModel.isUploadingImageForPicture,
tint = MaterialTheme.colors.placeholderText,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp)
) {
postViewModel.uploadForPicture(it, context)
@@ -178,13 +159,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "https://mywebsite.com/mybanner.jpg",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
leadingIcon = {
UploadFromGallery(
isUploading = postViewModel.isUploadingImageForBanner,
tint = MaterialTheme.colors.placeholderText,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp)
) {
postViewModel.uploadForBanner(it, context)
@@ -203,7 +184,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "https://mywebsite.com",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
singleLine = true
@@ -219,7 +200,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "_@mywebsite.com",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
singleLine = true
@@ -234,7 +215,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "me@mylightiningnode.com",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
singleLine = true
@@ -250,7 +231,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.lnurl),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -265,7 +246,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.twitter_proof_url_template),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -280,7 +261,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.mastodon_proof_url_template),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -295,7 +276,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.github_proof_url_template),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -23,7 +23,7 @@ import java.io.StringWriter
class NewUserMetadataViewModel : ViewModel() {
private lateinit var account: Account
val userName = mutableStateOf("")
// val userName = mutableStateOf("")
val displayName = mutableStateOf("")
val about = mutableStateOf("")
@@ -47,7 +47,7 @@ class NewUserMetadataViewModel : ViewModel() {
this.account = account
account.userProfile().let {
userName.value = it.bestUsername() ?: ""
// userName.value = it.bestUsername() ?: ""
displayName.value = it.bestDisplayName() ?: ""
about.value = it.info?.about ?: ""
picture.value = it.info?.picture ?: ""
@@ -82,8 +82,8 @@ class NewUserMetadataViewModel : ViewModel() {
} else {
ObjectMapper().createObjectNode()
}
currentJson.put("name", userName.value.trim())
currentJson.put("username", userName.value.trim())
// currentJson.put("username", userName.value.trim())
currentJson.put("name", displayName.value.trim())
currentJson.put("display_name", displayName.value.trim())
currentJson.put("displayName", displayName.value.trim())
currentJson.put("picture", picture.value.trim())
@@ -124,12 +124,11 @@ class NewUserMetadataViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
account.sendNewUserMetadata(writer.buffer.toString(), newClaims)
}
clear()
}
fun clear() {
userName.value = ""
// userName.value = ""
displayName.value = ""
about.value = ""
picture.value = ""
@@ -183,23 +182,25 @@ class NewUserMetadataViewModel : ViewModel() {
contentResolver.getType(galleryUri),
context.applicationContext,
onReady = { fileUri, contentType, size ->
ImageUploader.uploadImage(
uri = fileUri,
contentType = contentType,
size = size,
server = account.defaultFileServer,
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
onUploading(false)
onUploaded(imageUrl)
},
onError = {
onUploading(false)
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
viewModelScope.launch(Dispatchers.IO) {
ImageUploader.uploadImage(
uri = fileUri,
contentType = contentType,
size = size,
server = account.defaultFileServer,
contentResolver = contentResolver,
onSuccess = { imageUrl, mimeType ->
onUploading(false)
onUploaded(imageUrl)
},
onError = {
onUploading(false)
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
}
}
}
)
)
}
},
onError = {
onUploading(false)
@@ -13,9 +13,10 @@ 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.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -25,8 +26,10 @@ 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.ConnectivityType
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.note.LoadUser
@@ -47,6 +50,14 @@ fun RelayInformationDialog(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
@@ -77,6 +88,7 @@ fun RelayInformationDialog(
Column() {
RenderRelayIcon(
relayBriefInfo.favIcon,
automaticallyShowProfilePicture,
Size55dp
)
}
@@ -111,7 +123,15 @@ fun RelayInformationDialog(
Section(stringResource(R.string.contact))
Box(modifier = Modifier.padding(start = 10.dp)) {
ClickableEmail(relayInfo.contact ?: "")
relayInfo.contact?.let {
if (it.startsWith("https:")) {
ClickableUrl(urlText = it, url = it)
} else if (it.startsWith("mailto:") || it.contains('@')) {
ClickableEmail(it)
} else {
SectionContent(it)
}
}
}
Section(stringResource(R.string.supports))
@@ -248,7 +268,7 @@ private fun DisplayOwnerInformation(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
LoadUser(baseUserHex = userHex) {
LoadUser(baseUserHex = userHex, accountViewModel) {
Crossfade(it) {
if (it != null) {
UserCompose(baseUser = it, accountViewModel = accountViewModel, showDiviser = false, nav = nav)
@@ -12,9 +12,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Surface
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -3,12 +3,10 @@ package com.vitorpamplona.amethyst.ui.actions
import android.Manifest
import android.os.Build
import android.widget.Toast
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -66,21 +64,16 @@ fun SaveToGallery(url: String) {
}
}
Button(
OutlinedButton(
onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || writeStoragePermissionState.status.isGranted) {
saveImage()
} else {
writeStoragePermissionState.launchPermissionRequest()
}
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = Color.Gray
)
}
) {
Text(text = stringResource(id = R.string.save), color = Color.White)
Text(text = stringResource(id = R.string.save))
}
}
@@ -126,7 +119,7 @@ fun SaveToGallery(localFile: File, mimeType: String?) {
}
}
Button(
OutlinedButton(
onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || writeStoragePermissionState.status.isGranted) {
saveImage()
@@ -134,12 +127,8 @@ fun SaveToGallery(localFile: File, mimeType: String?) {
writeStoragePermissionState.launchPermissionRequest()
}
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = Color.Gray
)
shape = ButtonBorder
) {
Text(text = stringResource(id = R.string.save), color = Color.White)
Text(text = stringResource(id = R.string.save))
}
}
@@ -11,9 +11,9 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddPhotoAlternate
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -144,7 +144,7 @@ fun LoadingAnimation(
),
progress = 1f,
strokeWidth = 1.dp,
color = MaterialTheme.colors.background // Set background color
color = MaterialTheme.colorScheme.background // Set background color
)
}
@@ -2,13 +2,12 @@ package com.vitorpamplona.amethyst.ui.buttons
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -20,6 +19,7 @@ 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.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
@Composable
fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
@@ -46,12 +46,11 @@ fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit)
Column() {
if (isOpen) {
OutlinedButton(
FloatingActionButton(
onClick = { wantsToSendNewMessage = true; isOpen = false },
modifier = Modifier.size(55.dp),
modifier = Size55Modifier,
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(bottom = 3.dp)
containerColor = MaterialTheme.colorScheme.primary
) {
Text(
text = stringResource(R.string.messages_new_message),
@@ -63,12 +62,11 @@ fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit)
Spacer(modifier = Modifier.height(20.dp))
OutlinedButton(
FloatingActionButton(
onClick = { wantsToCreateChannel = true; isOpen = false },
modifier = Modifier.size(55.dp),
modifier = Size55Modifier,
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(bottom = 3.dp)
containerColor = MaterialTheme.colorScheme.primary
) {
Text(
text = stringResource(R.string.messages_create_public_chat),
@@ -81,12 +79,11 @@ fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit)
Spacer(modifier = Modifier.height(20.dp))
}
OutlinedButton(
FloatingActionButton(
onClick = { isOpen = !isOpen },
modifier = Modifier.size(55.dp),
modifier = Size55Modifier,
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(0.dp)
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(
imageVector = Icons.Outlined.Add,
@@ -3,12 +3,12 @@ package com.vitorpamplona.amethyst.buttons
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -36,7 +36,7 @@ fun NewChannelButton(accountViewModel: AccountViewModel) {
onClick = { wantsToPost = true },
modifier = Modifier.size(55.dp),
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
colors = ButtonDefaults.outlinedButtonColors(containerColor = MaterialTheme.colorScheme.primary),
contentPadding = PaddingValues(0.dp)
) {
Icon(
@@ -1,12 +1,10 @@
package com.vitorpamplona.amethyst.ui.buttons
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -21,10 +19,11 @@ 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.theme.Size55Modifier
@Composable
fun NewCommunityNoteButton(communityIdHex: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
LoadNote(baseNoteHex = communityIdHex) {
LoadNote(baseNoteHex = communityIdHex, accountViewModel) {
it?.let {
NewCommunityNoteButton(it, accountViewModel, nav)
}
@@ -41,12 +40,11 @@ fun NewCommunityNoteButton(note: Note, accountViewModel: AccountViewModel, nav:
NewPostView({ wantsToPost = false }, note, accountViewModel = accountViewModel, nav = nav)
}
OutlinedButton(
FloatingActionButton(
onClick = { wantsToPost = true },
modifier = Modifier.size(55.dp),
modifier = Size55Modifier,
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(0.dp)
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
@@ -6,16 +6,14 @@ import android.os.Build
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -41,6 +39,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.theme.Size55Modifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -112,12 +111,11 @@ fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit, na
if (postViewModel.isUploadingImage) {
ShowProgress(postViewModel)
} else {
OutlinedButton(
FloatingActionButton(
onClick = { wantsToPost = true },
modifier = Modifier.size(55.dp),
modifier = Size55Modifier,
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(0.dp)
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
@@ -137,16 +135,15 @@ private fun ShowProgress(postViewModel: NewMediaModel) {
targetValue = postViewModel.uploadingPercentage.value,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
).value,
modifier = Modifier
.size(55.dp)
modifier = Size55Modifier
.clip(CircleShape)
.background(MaterialTheme.colors.background),
.background(MaterialTheme.colorScheme.background),
strokeWidth = 5.dp
)
postViewModel.uploadingDescription.value?.let {
Text(
it,
color = MaterialTheme.colors.onSurface,
color = MaterialTheme.colorScheme.onSurface,
fontSize = 10.sp,
textAlign = TextAlign.Center
)
@@ -1,12 +1,10 @@
package com.vitorpamplona.amethyst.ui.buttons
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -19,6 +17,7 @@ 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.theme.Size55Modifier
@Composable
fun NewNoteButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
@@ -30,12 +29,11 @@ fun NewNoteButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
NewPostView({ wantsToPost = false }, accountViewModel = accountViewModel, nav = nav)
}
OutlinedButton(
FloatingActionButton(
onClick = { wantsToPost = true },
modifier = Modifier.size(55.dp),
modifier = Size55Modifier,
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(0.dp)
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
@@ -6,7 +6,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -55,7 +55,7 @@ fun AudioWaveformReadOnly(
amplitudes: List<Int>,
onProgressChange: (Float) -> Unit
) {
val backgroundColor = MaterialTheme.colors.background
val backgroundColor = MaterialTheme.colorScheme.background
val _progress = remember(progress) { progress.coerceIn(MinProgress, MaxProgress) }
val _spikeWidth = remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) }
val _spikePadding = remember(spikePadding) { spikePadding.coerceIn(MinSpikePaddingDp, MaxSpikePaddingDp) }
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -76,7 +76,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
.fillMaxWidth()
.padding(start = 30.dp, end = 30.dp)
.clip(shape = QuoteBorder)
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder)
) {
Column(
modifier = Modifier
@@ -157,7 +157,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(
@@ -186,7 +186,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text("", color = Color.White, fontSize = 18.sp)
@@ -4,8 +4,8 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@@ -13,12 +13,13 @@ import androidx.compose.ui.text.AnnotatedString
@Composable
fun ClickableEmail(email: String) {
val stripped = email.replaceFirst("mailto:", "")
val context = LocalContext.current
ClickableText(
text = remember { AnnotatedString(email) },
onClick = { runCatching { context.sendMail(email) } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
text = remember { AnnotatedString(stripped) },
onClick = { runCatching { context.sendMail(stripped) } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
}
@@ -1,8 +1,8 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.AnnotatedString
import com.vitorpamplona.amethyst.model.Note
@@ -16,6 +16,6 @@ fun ClickableNoteTag(
ClickableText(
text = AnnotatedString("@${baseNote.idNote().toShortenHex()}"),
onClick = { nav("Note/${baseNote.idHex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
}
@@ -4,8 +4,8 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@@ -18,7 +18,7 @@ fun ClickablePhone(phone: String) {
ClickableText(
text = remember { AnnotatedString(phone) },
onClick = { runCatching { context.dial(phone) } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
}
@@ -7,11 +7,10 @@ import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
@@ -40,12 +39,12 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NIP30Parser
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ImmutableListOfLists
@@ -61,20 +60,21 @@ import kotlinx.coroutines.launch
@Composable
fun ClickableRoute(
nip19: Nip19.Return,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
when (nip19.type) {
Nip19.Type.USER -> {
DisplayUser(nip19, nav)
DisplayUser(nip19, accountViewModel, nav)
}
Nip19.Type.ADDRESS -> {
DisplayAddress(nip19, nav)
DisplayAddress(nip19, accountViewModel, nav)
}
Nip19.Type.NOTE -> {
DisplayNote(nip19, nav)
DisplayNote(nip19, accountViewModel, nav)
}
Nip19.Type.EVENT -> {
DisplayEvent(nip19, nav)
DisplayEvent(nip19, accountViewModel, nav)
}
else -> {
Text(
@@ -86,34 +86,15 @@ fun ClickableRoute(
}
}
@Composable
private fun LoadNote(
hex: String,
content: @Composable (Note) -> Unit
) {
var noteBase by remember(hex) { mutableStateOf(LocalCache.getNoteIfExists(hex)) }
if (noteBase == null) {
LaunchedEffect(key1 = hex) {
launch(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateNote(hex)
}
}
}
noteBase?.let {
content(it)
}
}
@Composable
private fun DisplayEvent(
nip19: Nip19.Return,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
LoadNote(nip19.hex) {
LoadNote(nip19.hex, accountViewModel) {
if (it != null) {
DisplayNoteLink(it, nip19, nav)
DisplayNoteLink(it, nip19, accountViewModel, nav)
} else {
CreateClickableText(
clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" },
@@ -128,11 +109,12 @@ private fun DisplayEvent(
@Composable
private fun DisplayNote(
nip19: Nip19.Return,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
LoadNote(nip19.hex) {
LoadNote(nip19.hex, accountViewModel = accountViewModel) {
if (it != null) {
DisplayNoteLink(it, nip19, nav)
DisplayNoteLink(it, nip19, accountViewModel, nav)
} else {
CreateClickableText(
clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" },
@@ -148,6 +130,7 @@ private fun DisplayNote(
private fun DisplayNoteLink(
it: Note,
nip19: Nip19.Return,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteState by it.live().metadata.observeAsState()
@@ -177,7 +160,7 @@ private fun DisplayNoteLink(
nav = nav
)
} else if (channelHex != null) {
LoadChannel(baseChannelHex = channelHex) { baseChannel ->
LoadChannel(baseChannelHex = channelHex, accountViewModel) { baseChannel ->
val channelState by baseChannel.live.observeAsState()
val channelDisplayName by remember(channelState) {
derivedStateOf {
@@ -205,14 +188,15 @@ private fun DisplayNoteLink(
@Composable
private fun DisplayAddress(
nip19: Nip19.Return,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var noteBase by remember(nip19) { mutableStateOf(LocalCache.getNoteIfExists(nip19.hex)) }
var noteBase by remember(nip19) { mutableStateOf(accountViewModel.getNoteIfExists(nip19.hex)) }
if (noteBase == null) {
LaunchedEffect(key1 = nip19.hex) {
launch(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
accountViewModel.checkGetOrCreateAddressableNote(nip19.hex) {
noteBase = it
}
}
}
@@ -244,14 +228,19 @@ private fun DisplayAddress(
@Composable
private fun DisplayUser(
nip19: Nip19.Return,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var userBase by remember(nip19) { mutableStateOf(LocalCache.getUserIfExists(nip19.hex)) }
var userBase by remember(nip19) {
mutableStateOf(
accountViewModel.getUserIfExists(nip19.hex)
)
}
if (userBase == null) {
LaunchedEffect(key1 = nip19.hex) {
launch(Dispatchers.IO) {
userBase = LocalCache.checkGetOrCreateUser(nip19.hex)
accountViewModel.checkGetOrCreateUser(nip19.hex) {
userBase = it
}
}
}
@@ -317,8 +306,8 @@ fun CreateClickableText(
nav: (String) -> Unit
) {
val currentStyle = LocalTextStyle.current
val primaryColor = MaterialTheme.colors.primary
val onBackgroundColor = MaterialTheme.colors.onBackground
val primaryColor = MaterialTheme.colorScheme.primary
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
val clickablePartStyle = remember(primaryColor, overrideColor) {
currentStyle.copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight).toSpanStyle()
@@ -378,7 +367,7 @@ fun CreateTextWithEmoji(
val textColor = color.takeOrElse {
LocalTextStyle.current.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
LocalContentColor.current
}
}
@@ -434,7 +423,7 @@ fun CreateTextWithEmoji(
val textColor = color.takeOrElse {
LocalTextStyle.current.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
LocalContentColor.current
}
}
@@ -547,14 +536,14 @@ fun CreateClickableTextWithEmoji(
ClickableInLineIconRenderer(
emojiLists!!.part1,
maxLines,
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colorScheme.primary, fontWeight = fontWeight).toSpanStyle()
) {
nav(route)
}
InLineIconRenderer(
emojiLists!!.part2,
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle(),
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colorScheme.onBackground, fontWeight = fontWeight).toSpanStyle(),
maxLines = maxLines
)
}
@@ -687,7 +676,7 @@ fun InLineIconRenderer(
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 1.dp)
.padding(horizontal = 0.dp)
)
}
)
@@ -1,8 +1,8 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalUriHandler
@@ -23,6 +23,6 @@ fun ClickableUrl(urlText: String, url: String) {
ClickableText(
text = text,
onClick = { runCatching { uri.openUri(doubleCheckedUrl) } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
}
@@ -1,8 +1,8 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -28,6 +28,6 @@ fun ClickableUserTag(
ClickableText(
text = userName,
onClick = { nav(route) },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
}
@@ -5,9 +5,9 @@ import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
@@ -66,6 +66,6 @@ fun ClickableWithdrawal(withdrawalString: String) {
}
}
},
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
}
@@ -7,10 +7,10 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
import com.vitorpamplona.quartz.events.ImmutableListOfLists
const val SHORT_TEXT_LENGTH = 350
const val SHORTEN_AFTER_LINES = 10
@Composable
fun ExpandableRichTextViewer(
@@ -45,11 +46,25 @@ fun ExpandableRichTextViewer(
var showFullText by remember { mutableStateOf(false) }
val whereToCut = remember(content) {
// Cuts the text in the first space after 350
// Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters
val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
val firstNewLineAfterCut = content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
minOf(firstSpaceAfterCut, firstNewLineAfterCut)
// or after SHORTEN_AFTER_LINES lines
val numberOfLines = content.count { it == '\n' }
var charactersInLines = minOf(firstSpaceAfterCut, firstNewLineAfterCut)
if (numberOfLines > SHORTEN_AFTER_LINES) {
val shortContent = content.lines().take(SHORTEN_AFTER_LINES)
charactersInLines = 0
for (line in shortContent) {
// +1 because new line character is omitted from .lines
charactersInLines += (line.length + 1)
}
}
minOf(firstSpaceAfterCut, firstNewLineAfterCut, charactersInLines)
}
val text by remember(content) {
@@ -82,7 +97,7 @@ fun ExpandableRichTextViewer(
.fillMaxWidth()
.background(getGradient(backgroundColor))
) {
ShowMoreButton() {
ShowMoreButton {
showFullText = !showFullText
}
}
@@ -97,7 +112,7 @@ fun ShowMoreButton(onClick: () -> Unit) {
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondaryButtonBackground
containerColor = MaterialTheme.colorScheme.secondaryButtonBackground
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -74,7 +74,7 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
.fillMaxWidth()
.padding(start = 30.dp, end = 30.dp)
.clip(shape = QuoteBorder)
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder)
) {
Column(
modifier = Modifier
@@ -136,7 +136,7 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(text = stringResource(R.string.pay), color = Color.White, fontSize = 20.sp)
@@ -8,13 +8,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -57,7 +57,7 @@ fun InvoiceRequestCard(
.fillMaxWidth()
.padding(start = 30.dp, end = 30.dp)
.clip(shape = QuoteBorder)
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder)
) {
Column(
modifier = Modifier
@@ -116,7 +116,7 @@ fun InvoiceRequest(
placeholder = {
Text(
text = stringResource(R.string.thank_you_so_much),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -141,7 +141,7 @@ fun InvoiceRequest(
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -175,7 +175,7 @@ fun InvoiceRequest(
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(text = buttonText ?: stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp)
@@ -0,0 +1,154 @@
package com.vitorpamplona.amethyst.ui.components
import android.util.Log
import android.util.Patterns
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.startsWithNIP19Scheme
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.events.ImmutableListOfLists
class MarkdownParser {
private fun getDisplayNameAndNIP19FromTag(tag: String, tags: ImmutableListOfLists<String>): Pair<String, String>? {
val matcher = tagIndex.matcher(tag)
val (index, suffix) = try {
matcher.find()
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
} catch (e: Exception) {
Log.w("Tag Parser", "Couldn't link tag $tag", e)
Pair(null, null)
}
if (index != null && index >= 0 && index < tags.lists.size) {
val tag = tags.lists[index]
if (tag.size > 1) {
if (tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tag[1])?.let {
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
}
} else if (tag[0] == "e" || tag[0] == "a") {
LocalCache.checkGetOrCreateNote(tag[1])?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
}
}
return null
}
private fun getDisplayNameFromNip19(nip19: Nip19.Return): Pair<String, String>? {
if (nip19.type == Nip19.Type.USER) {
LocalCache.users[nip19.hex]?.let {
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
}
} else if (nip19.type == Nip19.Type.NOTE) {
LocalCache.notes[nip19.hex]?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
} else if (nip19.type == Nip19.Type.ADDRESS) {
LocalCache.addressables[nip19.hex]?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
} else if (nip19.type == Nip19.Type.EVENT) {
LocalCache.notes[nip19.hex]?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
return null
}
fun returnNIP19References(content: String, tags: ImmutableListOfLists<String>?): List<Nip19.Return> {
checkNotInMainThread()
val listOfReferences = mutableListOf<Nip19.Return>()
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19.uriToRoute(word)
parsedNip19?.let {
listOfReferences.add(it)
}
}
}
}
tags?.lists?.forEach {
if (it[0] == "p" && it.size > 1) {
listOfReferences.add(Nip19.Return(Nip19.Type.USER, it[1], null, null, null, ""))
} else if (it[0] == "e" && it.size > 1) {
listOfReferences.add(Nip19.Return(Nip19.Type.NOTE, it[1], null, null, null, ""))
} else if (it[0] == "a" && it.size > 1) {
listOfReferences.add(Nip19.Return(Nip19.Type.ADDRESS, it[1], null, null, null, ""))
}
}
return listOfReferences
}
fun returnMarkdownWithSpecialContent(content: String, tags: ImmutableListOfLists<String>?): String {
var returnContent = ""
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
returnContent += "![]($word) "
} else {
returnContent += "[$word]($word) "
}
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
returnContent += "[$word](mailto:$word) "
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
returnContent += "[$word](tel:$word) "
} else if (startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19.uriToRoute(word)
returnContent += if (parsedNip19 !== null) {
val pair = getDisplayNameFromNip19(parsedNip19)
if (pair != null) {
val (displayName, nip19) = pair
"[$displayName](nostr:$nip19) "
} else {
"$word "
}
} else {
"$word "
}
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
val pair = getDisplayNameAndNIP19FromTag(word, tags)
if (pair != null) {
returnContent += "[${pair.first}](nostr:${pair.second}) "
} else {
returnContent += "$word "
}
} else if (hashTagsPattern.matcher(word).matches()) {
val hashtagMatcher = hashTagsPattern.matcher(word)
val (myTag, mySuffix) = try {
hashtagMatcher.find()
Pair(hashtagMatcher.group(1), hashtagMatcher.group(2))
} catch (e: Exception) {
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
Pair(null, null)
}
if (myTag != null) {
returnContent += "[#$myTag](nostr:Hashtag?id=$myTag)$mySuffix "
} else {
returnContent += "$word "
}
} else {
returnContent += "$word "
}
} else {
returnContent += "$word "
}
}
returnContent += "\n"
}
return returnContent
}
}
@@ -1,20 +1,17 @@
package com.vitorpamplona.amethyst.ui.components
import android.util.Log
import android.util.Patterns
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.Icon
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
@@ -32,9 +29,8 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.em
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.material3.Material3RichText
import com.vitorpamplona.amethyst.model.HashtagIcon
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
@@ -59,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadUser
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
@@ -113,7 +110,7 @@ fun RichTextViewer(
) {
Column(modifier = modifier) {
if (remember(content) { isMarkdown(content) }) {
RenderContentAsMarkdown(content, tags, nav)
RenderContentAsMarkdown(content, tags, accountViewModel, nav)
} else {
RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, nav)
}
@@ -139,7 +136,7 @@ private fun RenderRegular(
val textStyle = currentTextStyle.copy(
lineHeight = 1.4.em,
color = currentTextStyle.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
LocalContentColor.current
}
)
@@ -243,7 +240,7 @@ private fun RenderWordWithoutPreview(
is PhoneSegment -> ClickablePhone(word.segmentText)
is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav)
is HashTagSegment -> HashTag(word, nav)
is HashIndexUserSegment -> TagLink(word, nav)
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav)
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
is RegularTextSegment -> NormalWord(word.segmentText, style)
@@ -270,7 +267,7 @@ private fun RenderWordWithPreview(
is PhoneSegment -> ClickablePhone(word.segmentText)
is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
is HashTagSegment -> HashTag(word, nav)
is HashIndexUserSegment -> TagLink(word, nav)
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav)
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
is RegularTextSegment -> NormalWord(word.segmentText, style)
@@ -320,7 +317,7 @@ fun RenderCustomEmoji(word: String, state: RichTextViewerState) {
}
@Composable
private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<String>?, nav: (String) -> Unit) {
private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<String>?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val uri = LocalUriHandler.current
val onClick = remember {
{ link: String ->
@@ -335,10 +332,10 @@ private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<
}
ProvideTextStyle(MarkdownTextStyle) {
MaterialRichText(
style = MaterialTheme.colors.markdownStyle
Material3RichText(
style = MaterialTheme.colorScheme.markdownStyle
) {
RefreshableContent(content, tags) {
RefreshableContent(content, tags, accountViewModel) {
Markdown(
content = it,
markdownParseOptions = MarkdownParseOptions.Default,
@@ -350,13 +347,14 @@ private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<
}
@Composable
private fun RefreshableContent(content: String, tags: ImmutableListOfLists<String>?, onCompose: @Composable (String) -> Unit) {
private fun RefreshableContent(content: String, tags: ImmutableListOfLists<String>?, accountViewModel: AccountViewModel, onCompose: @Composable (String) -> Unit) {
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(content) }
ObserverAllNIP19References(content, tags) {
val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent
ObserverAllNIP19References(content, tags, accountViewModel) {
accountViewModel.returnMarkdownWithSpecialContent(content, tags) { newMarkdownWithSpecialContent ->
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent
}
}
}
@@ -366,47 +364,47 @@ private fun RefreshableContent(content: String, tags: ImmutableListOfLists<Strin
}
@Composable
fun ObserverAllNIP19References(content: String, tags: ImmutableListOfLists<String>?, onRefresh: () -> Unit) {
fun ObserverAllNIP19References(content: String, tags: ImmutableListOfLists<String>?, accountViewModel: AccountViewModel, onRefresh: () -> Unit) {
var nip19References by remember(content) { mutableStateOf<List<Nip19.Return>>(emptyList()) }
LaunchedEffect(key1 = content) {
launch(Dispatchers.IO) {
nip19References = returnNIP19References(content, tags)
accountViewModel.returnNIP19References(content, tags) {
nip19References = it
onRefresh()
}
}
nip19References.forEach {
ObserveNIP19(it, onRefresh)
ObserveNIP19(it, accountViewModel, onRefresh)
}
}
@Composable
fun ObserveNIP19(
it: Nip19.Return,
accountViewModel: AccountViewModel,
onRefresh: () -> Unit
) {
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
ObserveNIP19Event(it, onRefresh)
ObserveNIP19Event(it, accountViewModel, onRefresh)
} else if (it.type == Nip19.Type.USER) {
ObserveNIP19User(it, onRefresh)
ObserveNIP19User(it, accountViewModel, onRefresh)
}
}
@Composable
private fun ObserveNIP19Event(
it: Nip19.Return,
accountViewModel: AccountViewModel,
onRefresh: () -> Unit
) {
var baseNote by remember(it) { mutableStateOf<Note?>(LocalCache.getNoteIfExists(it.hex)) }
var baseNote by remember(it) { mutableStateOf<Note?>(accountViewModel.getNoteIfExists(it.hex)) }
if (baseNote == null) {
LaunchedEffect(key1 = it.hex) {
launch(Dispatchers.IO) {
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
launch(Dispatchers.Main) { baseNote = note }
}
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
accountViewModel.checkGetOrCreateNote(it.hex) { note ->
launch(Dispatchers.Main) { baseNote = note }
}
}
}
@@ -423,9 +421,7 @@ fun ObserveNote(note: Note, onRefresh: () -> Unit) {
LaunchedEffect(key1 = loadedNoteId) {
if (loadedNoteId != null) {
launch(Dispatchers.IO) {
onRefresh()
}
onRefresh()
}
}
}
@@ -433,17 +429,16 @@ fun ObserveNote(note: Note, onRefresh: () -> Unit) {
@Composable
private fun ObserveNIP19User(
it: Nip19.Return,
accountViewModel: AccountViewModel,
onRefresh: () -> Unit
) {
var baseUser by remember(it) { mutableStateOf<User?>(LocalCache.getUserIfExists(it.hex)) }
var baseUser by remember(it) { mutableStateOf<User?>(accountViewModel.getUserIfExists(it.hex)) }
if (baseUser == null) {
LaunchedEffect(key1 = it.hex) {
launch(Dispatchers.IO) {
if (it.type == Nip19.Type.USER) {
LocalCache.checkGetOrCreateUser(it.hex)?.let { user ->
launch(Dispatchers.Main) { baseUser = user }
}
if (it.type == Nip19.Type.USER) {
accountViewModel.checkGetOrCreateUser(it.hex)?.let { user ->
launch(Dispatchers.Main) { baseUser = user }
}
}
}
@@ -460,184 +455,19 @@ private fun ObserveUser(user: User, onRefresh: () -> Unit) {
LaunchedEffect(key1 = loadedUserMetaId) {
if (loadedUserMetaId != null) {
launch(Dispatchers.IO) {
onRefresh()
}
onRefresh()
}
}
}
private fun getDisplayNameAndNIP19FromTag(tag: String, tags: ImmutableListOfLists<String>): Pair<String, String>? {
val matcher = tagIndex.matcher(tag)
val (index, suffix) = try {
matcher.find()
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
} catch (e: Exception) {
Log.w("Tag Parser", "Couldn't link tag $tag", e)
Pair(null, null)
}
if (index != null && index >= 0 && index < tags.lists.size) {
val tag = tags.lists[index]
if (tag.size > 1) {
if (tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tag[1])?.let {
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
}
} else if (tag[0] == "e" || tag[0] == "a") {
LocalCache.checkGetOrCreateNote(tag[1])?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
}
}
return null
}
private fun getDisplayNameFromNip19(nip19: Nip19.Return): Pair<String, String>? {
if (nip19.type == Nip19.Type.USER) {
LocalCache.users[nip19.hex]?.let {
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
}
} else if (nip19.type == Nip19.Type.NOTE) {
LocalCache.notes[nip19.hex]?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
} else if (nip19.type == Nip19.Type.ADDRESS) {
LocalCache.addressables[nip19.hex]?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
} else if (nip19.type == Nip19.Type.EVENT) {
LocalCache.notes[nip19.hex]?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
return null
}
private fun returnNIP19References(content: String, tags: ImmutableListOfLists<String>?): List<Nip19.Return> {
val listOfReferences = mutableListOf<Nip19.Return>()
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19.uriToRoute(word)
parsedNip19?.let {
listOfReferences.add(it)
}
}
}
}
tags?.lists?.forEach {
if (it[0] == "p" && it.size > 1) {
listOfReferences.add(Nip19.Return(Nip19.Type.USER, it[1], null, null, null, ""))
} else if (it[0] == "e" && it.size > 1) {
listOfReferences.add(Nip19.Return(Nip19.Type.NOTE, it[1], null, null, null, ""))
} else if (it[0] == "a" && it.size > 1) {
listOfReferences.add(Nip19.Return(Nip19.Type.ADDRESS, it[1], null, null, null, ""))
}
}
return listOfReferences
}
private fun returnMarkdownWithSpecialContent(content: String, tags: ImmutableListOfLists<String>?): String {
var returnContent = ""
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
returnContent += "![]($word) "
} else {
returnContent += "[$word]($word) "
}
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
returnContent += "[$word](mailto:$word) "
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
returnContent += "[$word](tel:$word) "
} else if (startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19.uriToRoute(word)
returnContent += if (parsedNip19 !== null) {
val pair = getDisplayNameFromNip19(parsedNip19)
if (pair != null) {
val (displayName, nip19) = pair
"[$displayName](nostr:$nip19) "
} else {
"$word "
}
} else {
"$word "
}
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
val pair = getDisplayNameAndNIP19FromTag(word, tags)
if (pair != null) {
returnContent += "[${pair.first}](nostr:${pair.second}) "
} else {
returnContent += "$word "
}
} else if (hashTagsPattern.matcher(word).matches()) {
val hashtagMatcher = hashTagsPattern.matcher(word)
val (myTag, mySuffix) = try {
hashtagMatcher.find()
Pair(hashtagMatcher.group(1), hashtagMatcher.group(2))
} catch (e: Exception) {
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
Pair(null, null)
}
if (myTag != null) {
returnContent += "[#$myTag](nostr:Hashtag?id=$myTag)$mySuffix "
} else {
returnContent += "$word "
}
} else {
returnContent += "$word "
}
} else {
returnContent += "$word "
}
}
returnContent += "\n"
}
return returnContent
}
fun startsWithNIP19Scheme(word: String): Boolean {
val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@")
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
}
@Immutable
data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19.Return)
@Composable
fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Color>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var loadedLink by remember { mutableStateOf<LoadedBechLink?>(null) }
if (loadedLink == null) {
LaunchedEffect(key1 = word) {
launch(Dispatchers.IO) {
Nip19.uriToRoute(word)?.let {
var returningNote: Note? = null
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
returningNote = note
}
}
val newLink = LoadedBechLink(returningNote, it)
launch(Dispatchers.Main) {
loadedLink = newLink
}
}
accountViewModel.parseNIP19(word) {
loadedLink = it
}
}
}
@@ -654,10 +484,10 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Co
}
} else if (loadedLink?.nip19 != null) {
Row() {
ClickableRoute(loadedLink?.nip19!!, nav)
ClickableRoute(loadedLink?.nip19!!, accountViewModel, nav)
}
} else {
val text = remember {
val text = remember(word) {
if (word.length > 16) {
word.replaceRange(8, word.length - 8, ":")
} else {
@@ -680,7 +510,7 @@ private fun DisplayFullNote(
NoteCompose(
baseNote = it,
accountViewModel = accountViewModel,
modifier = MaterialTheme.colors.replyModifier,
modifier = MaterialTheme.colorScheme.replyModifier,
parentBackgroundColor = backgroundColor,
isQuotedNote = true,
nav = nav
@@ -709,12 +539,12 @@ private fun RenderHashtag(
segment: HashTagSegment,
nav: (String) -> Unit
) {
val primary = MaterialTheme.colors.primary
val primary = MaterialTheme.colorScheme.primary
val hashtagIcon = remember(segment.hashtag) { checkForHashtagWithIcon(segment.hashtag, primary) }
ClickableText(
text = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary).toSpanStyle()
) {
append("#${segment.hashtag}")
}
@@ -726,7 +556,7 @@ private fun RenderHashtag(
val myId = "inlineContent"
val emptytext = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary).toSpanStyle()
) {
append("")
appendInlineContent(myId, "[icon]")
@@ -763,8 +593,8 @@ private fun InlineIcon(hashtagIcon: HashtagIcon) =
}
@Composable
fun TagLink(word: HashIndexUserSegment, nav: (String) -> Unit) {
LoadUser(baseUserHex = word.hex) {
fun TagLink(word: HashIndexUserSegment, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
LoadUser(baseUserHex = word.hex, accountViewModel) {
if (it == null) {
Text(text = word.segmentText)
} else {
@@ -776,15 +606,15 @@ fun TagLink(word: HashIndexUserSegment, nav: (String) -> Unit) {
}
@Composable
fun LoadNote(baseNoteHex: String, content: @Composable (Note?) -> Unit) {
fun LoadNote(baseNoteHex: String, accountViewModel: AccountViewModel, content: @Composable (Note?) -> Unit) {
var note by remember(baseNoteHex) {
mutableStateOf<Note?>(LocalCache.getNoteIfExists(baseNoteHex))
mutableStateOf<Note?>(accountViewModel.getNoteIfExists(baseNoteHex))
}
if (note == null) {
LaunchedEffect(key1 = baseNoteHex) {
launch(Dispatchers.IO) {
note = LocalCache.checkGetOrCreateNote(baseNoteHex)
accountViewModel.checkGetOrCreateNote(baseNoteHex) {
note = it
}
}
}
@@ -794,7 +624,7 @@ fun LoadNote(baseNoteHex: String, content: @Composable (Note?) -> Unit) {
@Composable
fun TagLink(word: HashIndexEventSegment, canPreview: Boolean, backgroundColor: MutableState<Color>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
LoadNote(baseNoteHex = word.hex) {
LoadNote(baseNoteHex = word.hex, accountViewModel) {
if (it == null) {
Text(text = remember { word.segmentText.toShortenHex() })
} else {
@@ -825,7 +655,7 @@ private fun DisplayNoteFromTag(
NoteCompose(
baseNote = baseNote,
accountViewModel = accountViewModel,
modifier = MaterialTheme.colors.innerPostModifier,
modifier = MaterialTheme.colorScheme.innerPostModifier,
parentBackgroundColor = backgroundColor,
isQuotedNote = true,
nav = nav
@@ -58,14 +58,15 @@ fun RobohashFallbackAsyncImage(
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
loadProfilePicture: Boolean
) {
val context = LocalContext.current
val painter = rememberAsyncImagePainter(
model = Robohash.imageRequest(context, robot)
)
if (model != null) {
if (model != null && loadProfilePicture) {
AsyncImage(
model = model,
contentDescription = contentDescription,
@@ -101,7 +102,8 @@ fun RobohashAsyncImageProxy(
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
loadProfilePicture: Boolean
) {
RobohashFallbackAsyncImage(
robot = robot,
@@ -112,6 +114,7 @@ fun RobohashAsyncImageProxy(
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
filterQuality = filterQuality,
loadProfilePicture = loadProfilePicture
)
}
@@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -11,14 +11,14 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -104,7 +104,7 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
modifier = Modifier
.size(70.dp)
.align(Alignment.BottomStart),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
Icon(
imageVector = Icons.Rounded.Warning,
@@ -112,7 +112,7 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
modifier = Modifier
.size(30.dp)
.align(Alignment.TopEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
}
}
@@ -141,7 +141,7 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
@@ -14,8 +14,8 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@@ -66,8 +66,8 @@ fun DotsIndicator(
modifier: Modifier = Modifier,
totalDots: Int,
selectedIndex: Int,
selectedColor: Color = MaterialTheme.colors.primary /* Color.Yellow */,
unSelectedColor: Color = MaterialTheme.colors.placeholderText /* Color.Gray */,
selectedColor: Color = MaterialTheme.colorScheme.primary /* Color.Yellow */,
unSelectedColor: Color = MaterialTheme.colorScheme.placeholderText /* Color.Gray */,
dotSize: Dp
) {
LazyRow(
@@ -0,0 +1,99 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlin.math.abs
class SplitItem<T>(val key: T) {
var percentage by mutableStateOf(0f)
}
class Split<T>() {
var items: List<SplitItem<T>> by mutableStateOf(emptyList())
fun addItem(key: T): Int {
val wasEqualSplit = isEqualSplit()
val newItem = SplitItem(key)
items = items.plus(newItem)
if (wasEqualSplit) {
forceEqualSplit()
} else {
updatePercentage(items.lastIndex, equalSplit())
}
return items.lastIndex
}
fun equalSplit() = 1f / items.size
fun isEqualSplit(): Boolean {
val expectedPercentage = equalSplit()
return items.all { (it.percentage - expectedPercentage) < 0.01 }
}
fun forceEqualSplit() {
val correctPercentage = equalSplit()
items.forEach {
it.percentage = correctPercentage
}
}
fun updatePercentage(index: Int, percentage: Float) {
if (items.isEmpty()) return
val splitItem = items.getOrNull(index) ?: return
if (items.size == 1) {
splitItem.percentage = 1f
} else {
splitItem.percentage = percentage
println("Update ${items[index].key} to $percentage")
val othersMustShare = 1.0f - splitItem.percentage
val othersHave = items.sumOf {
if (it == splitItem) 0.0 else it.percentage.toDouble()
}.toFloat()
if (abs(othersHave - othersMustShare) < 0.01) return // nothing to do
println("Others Must Share $othersMustShare but have $othersHave")
bottomUpAdjustment(othersMustShare, othersHave, index)
}
}
private fun bottomUpAdjustment(othersMustShare: Float, othersHave: Float, exceptForIndex: Int) {
var needToRemove = othersHave - othersMustShare
if (needToRemove > 0) {
for (i in items.indices.reversed()) {
if (i == exceptForIndex) continue // do not update the current item
if (needToRemove < items[i].percentage) {
val oldValue = items[i].percentage
items[i].percentage -= needToRemove
needToRemove = 0f
println("- Updating ${items[i].key} from $oldValue to ${items[i].percentage - needToRemove}. $needToRemove left")
} else {
val oldValue = items[i].percentage
needToRemove -= items[i].percentage
items[i].percentage = 0f
println("- Updating ${items[i].key} from $oldValue to ${0}. $needToRemove left")
}
if (needToRemove < 0.01) {
break
}
}
} else if (needToRemove < 0) {
if (items.lastIndex == exceptForIndex) {
items[items.lastIndex - 1].percentage += -needToRemove
} else {
items.last().percentage += -needToRemove
}
}
}
}
@@ -14,12 +14,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -37,8 +38,7 @@ import kotlinx.collections.immutable.ImmutableList
fun TextSpinner(
label: String?,
placeholder: String,
options: ImmutableList<String>,
explainers: ImmutableList<String>? = null,
options: ImmutableList<TitleExplainer>,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier
) {
@@ -74,8 +74,8 @@ fun TextSpinner(
if (optionsShowing) {
options.isNotEmpty().also {
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) {
currentText = options[it]
SpinnerSelectionDialog(options = options, onDismiss = { optionsShowing = false }) {
currentText = options[it].title
optionsShowing = false
onSelect(it)
}
@@ -85,10 +85,40 @@ fun TextSpinner(
@Composable
fun SpinnerSelectionDialog(
options: ImmutableList<String>,
explainers: ImmutableList<String>?,
options: ImmutableList<TitleExplainer>,
onDismiss: () -> Unit,
onSelect: (Int) -> Unit
) {
SpinnerSelectionDialog(
options = options,
onSelect = onSelect,
onDismiss = onDismiss
) { item ->
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = item.title, color = MaterialTheme.colorScheme.onSurface)
}
item.explainer?.let {
Spacer(modifier = Modifier.height(5.dp))
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = it, color = Color.Gray, fontSize = Font14SP)
}
}
}
}
@Composable
fun <T> SpinnerSelectionDialog(
options: ImmutableList<T>,
onSelect: (Int) -> Unit,
onDismiss: () -> Unit,
onRenderItem: @Composable (T) -> Unit
) {
Dialog(onDismissRequest = onDismiss) {
Surface(
@@ -106,23 +136,7 @@ fun SpinnerSelectionDialog(
}
) {
Column() {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = item, color = MaterialTheme.colors.onSurface)
}
explainers?.getOrNull(index)?.let {
Spacer(modifier = Modifier.height(5.dp))
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = it, color = Color.Gray, fontSize = Font14SP)
}
}
onRenderItem(item)
}
}
if (index < options.lastIndex) {
@@ -133,3 +147,6 @@ fun SpinnerSelectionDialog(
}
}
}
@Immutable
data class TitleExplainer(val title: String, val explainer: String? = null)
@@ -0,0 +1,11 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Immutable
@Immutable
data class TranslationConfig(
val result: String?,
val sourceLang: String?,
val targetLang: String?,
val showOriginal: Boolean
)
@@ -3,10 +3,10 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -15,13 +15,15 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
@Composable
@@ -44,7 +46,7 @@ fun UrlPreviewCard(
val uri = LocalUriHandler.current
Row(
modifier = MaterialTheme.colors.innerPostModifier
modifier = MaterialTheme.colorScheme.innerPostModifier
.clickable {
runCatching { uri.openUri(url) }
}
@@ -57,12 +59,12 @@ fun UrlPreviewCard(
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = StdVertSpacer)
Text(
text = previewInfo.verifiedUrl?.host ?: previewInfo.url,
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
style = MaterialTheme.typography.bodySmall,
modifier = MaxWidthWithHorzPadding,
color = Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -70,24 +72,22 @@ fun UrlPreviewCard(
Text(
text = previewInfo.title,
style = MaterialTheme.typography.body2,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp),
style = MaterialTheme.typography.bodyMedium,
modifier = MaxWidthWithHorzPadding,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = previewInfo.description,
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
style = MaterialTheme.typography.bodySmall,
modifier = MaxWidthWithHorzPadding,
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = DoubleVertSpacer)
}
}
}
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.net.Uri
@@ -21,14 +22,15 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -210,27 +212,29 @@ fun VideoViewInner(
} else {
VideoPlayerActiveMutex(videoUri) { activeOnScreen ->
val mediaItem = remember(videoUri) {
MediaItem.Builder()
.setMediaId(videoUri)
.setUri(videoUri)
.setMediaMetadata(
MediaMetadata.Builder()
.setArtist(authorName?.ifBlank { null })
.setTitle(title?.ifBlank { null } ?: videoUri)
.setArtworkUri(
try {
if (artworkUri != null) {
Uri.parse(artworkUri)
} else {
mutableStateOf(
MediaItem.Builder()
.setMediaId(videoUri)
.setUri(videoUri)
.setMediaMetadata(
MediaMetadata.Builder()
.setArtist(authorName?.ifBlank { null })
.setTitle(title?.ifBlank { null } ?: videoUri)
.setArtworkUri(
try {
if (artworkUri != null) {
Uri.parse(artworkUri)
} else {
null
}
} catch (e: Exception) {
null
}
} catch (e: Exception) {
null
}
)
.build()
)
.build()
)
.build()
)
.build()
)
}
GetVideoController(
@@ -257,7 +261,7 @@ fun VideoViewInner(
@Composable
@OptIn(UnstableApi::class)
fun GetVideoController(
mediaItem: MediaItem,
mediaItem: MutableState<MediaItem>,
videoUri: String,
defaultToStart: Boolean = false,
nostrUriCallback: String? = null,
@@ -281,18 +285,22 @@ fun GetVideoController(
UUID.randomUUID().toString()
}
val scope = rememberCoroutineScope()
// Prepares a VideoPlayer from the foreground service.
LaunchedEffect(key1 = videoUri) {
DisposableEffect(key1 = videoUri) {
// If it is not null, the user might have come back from a playing video, like clicking on
// the notification of the video player.
if (controller.value == null) {
launch(Dispatchers.IO) {
scope.launch(Dispatchers.IO) {
PlaybackClientController.prepareController(
uid,
videoUri,
nostrUriCallback,
context
) {
// REQUIRED TO BE RUN IN THE MAIN THREAD
// checks again because of race conditions.
if (controller.value == null) { // still prone to race conditions.
controller.value = it
@@ -308,7 +316,7 @@ fun GetVideoController(
}
}
controller.value?.setMediaItem(mediaItem)
controller.value?.setMediaItem(mediaItem.value)
controller.value?.prepare()
} else if (controller.value != it) {
// discards the new controller because there is an existing one
@@ -326,7 +334,7 @@ fun GetVideoController(
it.volume = if (defaultToStart) 0f else 1f
}
it.setMediaItem(mediaItem)
it.setMediaItem(mediaItem.value)
it.prepare()
}
}
@@ -345,29 +353,39 @@ fun GetVideoController(
it.volume = if (defaultToStart) 0f else 1f
}
it.setMediaItem(mediaItem)
it.setMediaItem(mediaItem.value)
it.prepare()
}
}
}
onDispose {
if (!keepPlaying.value) {
// Stops and releases the media.
controller.value?.stop()
controller.value?.release()
controller.value = null
}
}
}
// User pauses and resumes the app. What to do with videos?
val scope = rememberCoroutineScope()
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = videoUri) {
DisposableEffect(key1 = lifeCycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
// if the controller is null, restarts the controller with a new one
// if the controller is not null, just continue playing what the controller was playing
if (controller.value == null) {
scope.launch(Dispatchers.IO) {
scope.launch(Dispatchers.IO) {
if (controller.value == null) {
PlaybackClientController.prepareController(
UUID.randomUUID().toString(),
uid,
videoUri,
nostrUriCallback,
context
) {
// REQUIRED TO BE RUN IN THE MAIN THREAD
// checks again to make sure no other thread has created a controller.
if (controller.value == null) {
controller.value = it
@@ -383,7 +401,7 @@ fun GetVideoController(
}
}
controller.value?.setMediaItem(mediaItem)
controller.value?.setMediaItem(mediaItem.value)
controller.value?.prepare()
} else if (controller.value != it) {
// discards the new controller because there is an existing one
@@ -407,13 +425,6 @@ fun GetVideoController(
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
if (!keepPlaying.value) {
// Stops and releases the media.
controller.value?.stop()
controller.value?.release()
controller.value = null
}
}
}
@@ -456,7 +467,7 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Bo
}
}
Box(
val myModifier = remember(videoUri) {
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 70.dp)
@@ -485,7 +496,9 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Bo
}
}
}
) {
}
Box(modifier = myModifier) {
inner(active)
}
}
@@ -507,8 +520,6 @@ private fun RenderVideoPlayer(
activeOnScreen: MutableState<Boolean>,
onDialog: ((Boolean) -> Unit)?
) {
val context = LocalContext.current
ControlWhenPlayerIsActive(controller, keepPlaying, automaticallyStartPlayback, activeOnScreen)
val controllerVisible = remember(controller) {
@@ -516,17 +527,23 @@ private fun RenderVideoPlayer(
}
BoxWithConstraints() {
AndroidView(
modifier = if (roundedCorner) {
MaterialTheme.colors.imageModifier
val borders = MaterialTheme.colorScheme.imageModifier
val myModifier = remember {
if (roundedCorner) {
borders
.defaultMinSize(minHeight = 100.dp)
.align(Alignment.Center)
} else {
Modifier.fillMaxWidth()
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 100.dp)
.align(Alignment.Center)
},
factory = {
}
}
val factory = remember(controller) {
{ context: Context ->
PlayerView(context).apply {
player = controller
layoutParams = FrameLayout.LayoutParams(
@@ -551,10 +568,15 @@ private fun RenderVideoPlayer(
)
}
}
}
AndroidView(
modifier = myModifier,
factory = factory
)
waveform?.let {
Waveform(it, controller, Modifier.align(Alignment.Center))
Waveform(it, controller, remember { Modifier.align(Alignment.Center) })
}
val startingMuteState = remember(controller) {
@@ -575,7 +597,7 @@ private fun RenderVideoPlayer(
controller.volume = if (mute) 0f else 1f
}
KeepPlayingButton(keepPlaying, controllerVisible, Modifier.align(Alignment.TopEnd)) { newKeepPlaying: Boolean ->
KeepPlayingButton(keepPlaying, controllerVisible, remember { Modifier.align(Alignment.TopEnd) }) { newKeepPlaying: Boolean ->
// If something else is playing and the user marks this video to keep playing, stops the other one.
if (newKeepPlaying) {
if (keepPlayingMutex != null && keepPlayingMutex != controller) {
@@ -612,7 +634,7 @@ fun Waveform(
DrawWaveform(waveform, waveformProgress, modifier)
val restartFlow = remember {
mutableStateOf(0)
mutableIntStateOf(0)
}
// Keeps the screen on while playing and viewing videos.
@@ -693,7 +715,7 @@ fun ControlWhenPlayerIsActive(
val view = LocalView.current
// Keeps the screen on while playing and viewing videos.
DisposableEffect(key1 = controller) {
DisposableEffect(key1 = controller, key2 = view) {
val listener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
// doesn't consider the mutex because the screen can turn off if the video
@@ -775,7 +797,7 @@ private fun MuteButton(
.clip(CircleShape)
.fillMaxSize(0.6f)
.align(Alignment.Center)
.background(MaterialTheme.colors.background)
.background(MaterialTheme.colorScheme.background)
)
IconButton(
@@ -816,7 +838,7 @@ private fun KeepPlayingButton(
.clip(CircleShape)
.fillMaxSize(0.6f)
.align(Alignment.Center)
.background(MaterialTheme.colors.background)
.background(MaterialTheme.colorScheme.background)
)
IconButton(
@@ -827,9 +849,9 @@ private fun KeepPlayingButton(
modifier = Size50Modifier
) {
if (keepPlaying.value) {
LyricsIcon(Size22Modifier, MaterialTheme.colors.onBackground)
LyricsIcon(Size22Modifier, MaterialTheme.colorScheme.onBackground)
} else {
LyricsOffIcon(Size22Modifier, MaterialTheme.colors.onBackground)
LyricsOffIcon(Size22Modifier, MaterialTheme.colorScheme.onBackground)
}
}
}
@@ -5,11 +5,11 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@@ -60,7 +60,7 @@ fun ZapRaiserRequest(
Text(
text = stringResource(R.string.zapraiser_explainer),
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
@@ -84,7 +84,7 @@ fun ZapRaiserRequest(
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -29,18 +30,17 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
@@ -53,7 +53,6 @@ import androidx.compose.runtime.rememberCoroutineScope
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.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
@@ -86,15 +85,14 @@ import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.Size24dp
import com.vitorpamplona.amethyst.ui.theme.Size30dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import kotlinx.collections.immutable.ImmutableList
@@ -212,7 +210,7 @@ fun ZoomableContentView(
}
var mainImageModifier = if (roundedCorner) {
MaterialTheme.colors.imageModifier
MaterialTheme.colorScheme.imageModifier
} else {
Modifier.fillMaxWidth()
}
@@ -477,14 +475,16 @@ private fun AddedImageFeatures(
if (content.blurhash != null) {
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
} else {
FlowRow() {
FlowRow(Modifier.fillMaxWidth()) {
DisplayUrlWithLoadingSymbol(content)
}
}
}
is AsyncImagePainter.State.Error -> {
ClickableUrl(urlText = "${content.url} ", url = content.url)
FlowRow(Modifier.fillMaxWidth()) {
ClickableUrl(urlText = "${content.url} ", url = content.url)
}
}
is AsyncImagePainter.State.Success -> {
@@ -551,7 +551,7 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) {
val myId = "inlineContent"
val emptytext = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary).toSpanStyle()
) {
append("")
appendInlineContent(myId, "[icon]")
@@ -612,7 +612,7 @@ fun ZoomableImageDialog(
) {
val view = LocalView.current
DisposableEffect(key1 = Unit) {
DisposableEffect(key1 = view) {
if (Build.VERSION.SDK_INT >= 30) {
view.windowInsetsController?.hide(
android.view.WindowInsets.Type.systemBars()
@@ -635,7 +635,7 @@ fun ZoomableImageDialog(
decorFitsSystemWindows = false
)
) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
val pagerState: PagerState = rememberPagerState() { allImages.size }
@@ -669,6 +669,7 @@ fun ZoomableImageDialog(
if (myContent is ZoomableUrlContent) {
Row() {
CopyToClipboard(content = myContent)
Spacer(modifier = StdHorzSpacer)
SaveToGallery(url = myContent.url)
}
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
@@ -689,17 +690,11 @@ private fun CopyToClipboard(
) {
val popupExpanded = remember { mutableStateOf(false) }
Button(
OutlinedButton(
modifier = Modifier.padding(horizontal = Size5dp),
onClick = { popupExpanded.value = true },
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.placeholderText
)
onClick = { popupExpanded.value = true }
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Share,
modifier = Size20Modifier,
contentDescription = stringResource(R.string.copy_url_to_clipboard)
@@ -723,20 +718,35 @@ private fun ShareImageAction(
val clipboardManager = LocalClipboardManager.current
if (content is ZoomableUrlContent) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.url)); onDismiss() }) {
Text(stringResource(R.string.copy_url_to_clipboard))
}
if (content.uri != null) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.uri)); onDismiss() }) {
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
DropdownMenuItem(
text = {
Text(stringResource(R.string.copy_url_to_clipboard))
},
onClick = {
clipboardManager.setText(AnnotatedString(content.url)); onDismiss()
}
)
if (content.uri != null) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
},
onClick = {
clipboardManager.setText(AnnotatedString(content.uri)); onDismiss()
}
)
}
}
if (content is ZoomablePreloadedContent) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.uri)); onDismiss() }) {
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
}
DropdownMenuItem(
text = {
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
},
onClick = {
clipboardManager.setText(AnnotatedString(content.uri)); onDismiss()
}
)
}
}
}
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.quartz.encoders.toHexKey
object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
lateinit var account: Account
@@ -12,18 +14,44 @@ object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
}
override fun feed(): List<Note> {
val privKey = account.keyPair.privKey ?: return emptyList()
val bookmarks = account.userProfile().latestBookmarkList
val notes = bookmarks?.privateTaggedEvents(privKey)
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
if (account.loginWithExternalSigner) {
val id = bookmarks?.id
if (id != null) {
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id]
if (decryptedContent == null) {
ExternalSignerUtils.decryptBookmark(
bookmarks.content,
account.keyPair.pubKey.toHexKey(),
id
)
} else {
bookmarks.decryptedContent = decryptedContent
}
}
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] ?: ""
val addresses = bookmarks?.privateTaggedAddresses(privKey)
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
val notes = bookmarks?.privateTaggedEvents(decryptedContent)
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
val addresses = bookmarks?.privateTaggedAddresses(decryptedContent)
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
return notes.plus(addresses).toSet()
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
return notes.plus(addresses).toSet()
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
} else {
val privKey = account.keyPair.privKey ?: return emptyList()
val notes = bookmarks?.privateTaggedEvents(privKey)
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
val addresses = bookmarks?.privateTaggedAddresses(privKey)
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
return notes.plus(addresses).toSet()
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
}
}
}
@@ -14,13 +14,50 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
}
override fun feed(): List<User> {
return account.getBlockList()
val blockList = account.getBlockList()
val decryptedContent = blockList?.decryptedContent ?: ""
if (account.loginWithExternalSigner) {
if (decryptedContent.isEmpty()) return emptyList()
return blockList
?.publicAndPrivateUsers(decryptedContent)
?.map { LocalCache.getOrCreateUser(it) }
?: emptyList()
}
return blockList
?.publicAndPrivateUsers(account.keyPair.privKey)
?.map { LocalCache.getOrCreateUser(it) }
?: emptyList()
}
}
class HiddenWordsFeedFilter(val account: Account) : FeedFilter<String>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
}
override fun showHiddenKey(): Boolean {
return true
}
override fun feed(): List<String> {
val blockList = account.getBlockList()
val decryptedContent = blockList?.decryptedContent ?: ""
if (account.loginWithExternalSigner) {
if (decryptedContent.isEmpty()) return emptyList()
return blockList
?.publicAndPrivateWords(decryptedContent)?.toList()
?: emptyList()
}
return blockList
?.publicAndPrivateWords(account.keyPair.privKey)?.toList()
?: emptyList()
}
}
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
@@ -12,17 +12,19 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AlertDialog
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -33,7 +35,6 @@ import androidx.compose.runtime.remember
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 androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -44,8 +45,10 @@ import androidx.lifecycle.map
import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
@@ -60,6 +63,7 @@ import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountSwitchBottomSheet(
accountViewModel: AccountViewModel,
@@ -111,8 +115,9 @@ fun AccountSwitchBottomSheet(
ArrowBackIcon()
}
},
backgroundColor = Color.Transparent,
elevation = 0.dp
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
}
@@ -136,6 +141,14 @@ fun DisplayAccount(
)
}
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
if (baseUser == null) {
LaunchedEffect(key1 = acc.npub) {
launch(Dispatchers.IO) {
@@ -173,7 +186,7 @@ fun DisplayAccount(
.width(55.dp)
.padding(0.dp)
) {
AccountPicture(it)
AccountPicture(it, automaticallyShowProfilePicture)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
@@ -202,22 +215,21 @@ private fun ActiveMarker(acc: AccountInfo, accountViewModel: AccountViewModel) {
Icon(
imageVector = Icons.Default.RadioButtonChecked,
contentDescription = stringResource(R.string.account_switch_active_account),
tint = MaterialTheme.colors.secondary
tint = MaterialTheme.colorScheme.secondary
)
}
}
@Composable
private fun AccountPicture(user: User) {
val profilePicture by user.live().metadata.map {
it.user.profilePicture()
}.observeAsState()
private fun AccountPicture(user: User, loadProfilePicture: Boolean) {
val profilePicture by user.live().profilePictureChanges.observeAsState()
RobohashAsyncImageProxy(
robot = remember(user) { user.pubkeyHex },
model = profilePicture,
contentDescription = stringResource(R.string.profile_image),
modifier = AccountPictureModifier
modifier = AccountPictureModifier,
loadProfilePicture = loadProfilePicture
)
}
@@ -293,7 +305,7 @@ private fun LogoutButton(
Icon(
imageVector = Icons.Default.Logout,
contentDescription = stringResource(R.string.log_out),
tint = MaterialTheme.colors.onSurface
tint = MaterialTheme.colorScheme.onSurface
)
}
}
@@ -11,12 +11,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -26,7 +26,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -42,6 +41,7 @@ import androidx.navigation.NavBackStackEntry
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -104,12 +104,10 @@ private fun RenderBottomMenu(
) {
Column(modifier = BottomTopHeight) {
Divider(
thickness = 0.25.dp
thickness = DividerThickness
)
BottomNavigation(
modifier = Modifier,
elevation = 0.dp,
backgroundColor = MaterialTheme.colors.background
NavigationBar(
tonalElevation = 0.dp
) {
bottomNavigationItems.forEach { item ->
HasNewItemsIcon(item, accountViewModel, navEntryState, nav)
@@ -133,8 +131,6 @@ private fun RowScope.HasNewItemsIcon(
}
}
val scope = rememberCoroutineScope()
val size = remember {
if ("Home" == route.base) 25.dp else 23.dp
}
@@ -150,9 +146,7 @@ private fun RowScope.HasNewItemsIcon(
hasNewItems = hasNewItems,
navEntryState = navEntryState
) { selected ->
scope.launch {
nav(route, selected)
}
nav(route, selected)
}
}
@@ -210,7 +204,7 @@ private fun RowScope.NavigationIcon(
hasNewItems: Boolean,
onClick: (Boolean) -> Unit
) {
BottomNavigationItem(
NavigationBarItem(
icon = {
NotifiableIcon(
icon,
@@ -232,7 +226,7 @@ private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean,
painter = painterResource(id = icon),
contentDescription = null,
modifier = remember { Modifier.size(iconSize) },
tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified
)
if (hasNewItems) {
@@ -250,7 +244,7 @@ private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean,
.width(10.dp)
.height(10.dp)
.clip(shape = CircleShape)
}.background(MaterialTheme.colors.primary),
}.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.TopEnd
) {
Text(
@@ -284,7 +284,7 @@ fun AppNavigation(
actionableNextPage = null
}
DisposableEffect(navController) {
DisposableEffect(activity) {
val consumer = Consumer<Intent> { intent ->
val uri = intent?.data?.toString()
val newPage = uriToRoute(uri)
@@ -17,22 +17,19 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AppBarDefaults
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.ScaffoldState
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
@@ -50,18 +47,19 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry
import coil.Coil
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
@@ -80,6 +78,7 @@ import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
@@ -88,6 +87,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.LoadCityName
import com.vitorpamplona.amethyst.ui.note.LoadUser
import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
@@ -106,7 +106,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongRoomHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.RoomNameOnlyDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShortChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowVideoStreaming
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
@@ -117,6 +116,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -131,7 +131,7 @@ import kotlinx.coroutines.launch
fun AppTopBar(
followLists: FollowListViewModel,
navEntryState: State<NavBackStackEntry?>,
scaffoldState: ScaffoldState,
drawerState: DrawerState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
navPopBack: () -> Unit
@@ -148,7 +148,7 @@ fun AppTopBar(
}
}
RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav, navPopBack)
RenderTopRouteBar(currentRoute, id, followLists, drawerState, accountViewModel, nav, navPopBack)
}
@Composable
@@ -156,16 +156,16 @@ private fun RenderTopRouteBar(
currentRoute: String?,
id: String?,
followLists: FollowListViewModel,
scaffoldState: ScaffoldState,
drawerState: DrawerState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
navPopBack: () -> Unit
) {
when (currentRoute) {
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
Route.Discover.base -> DiscoveryTopBar(followLists, scaffoldState, accountViewModel, nav)
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav)
Route.Home.base -> HomeTopBar(followLists, drawerState, accountViewModel, nav)
Route.Video.base -> StoriesTopBar(followLists, drawerState, accountViewModel, nav)
Route.Discover.base -> DiscoveryTopBar(followLists, drawerState, accountViewModel, nav)
Route.Notification.base -> NotificationTopBar(followLists, drawerState, accountViewModel, nav)
Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
else -> {
if (id != null) {
@@ -176,10 +176,10 @@ private fun RenderTopRouteBar(
Route.Community.base -> CommunityTopBar(id, accountViewModel, nav, navPopBack)
Route.Hashtag.base -> HashTagTopBar(id, accountViewModel, navPopBack)
Route.Geohash.base -> GeoHashTopBar(id, accountViewModel, navPopBack)
else -> MainTopBar(scaffoldState, accountViewModel, nav)
else -> MainTopBar(drawerState, accountViewModel, nav)
}
} else {
MainTopBar(scaffoldState, accountViewModel, nav)
MainTopBar(drawerState, accountViewModel, nav)
}
}
}
@@ -227,7 +227,7 @@ private fun CommunityTopBar(
nav: (String) -> Unit,
navPopBack: () -> Unit
) {
LoadAddressableNote(aTagHex = id) { baseNote ->
LoadAddressableNote(aTagHex = id, accountViewModel) { baseNote ->
if (baseNote != null) {
FlexibleTopBarWithBackButton(
title = {
@@ -288,7 +288,7 @@ private fun RenderRoomTopBar(
if (room.users.size == 1) {
FlexibleTopBarWithBackButton(
title = {
LoadUser(baseUserHex = room.users.first()) { baseUser ->
LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser ->
if (baseUser != null) {
ClickableUserPicture(
baseUser = baseUser,
@@ -303,7 +303,7 @@ private fun RenderRoomTopBar(
}
},
extendableRow = {
LoadUser(baseUserHex = room.users.first()) {
LoadUser(baseUserHex = room.users.first(), accountViewModel) {
if (it != null) {
UserCompose(
baseUser = it,
@@ -348,13 +348,8 @@ private fun ChannelTopBar(
nav: (String) -> Unit,
navPopBack: () -> Unit
) {
LoadChannel(baseChannelHex = id) { baseChannel ->
LoadChannel(baseChannelHex = id, accountViewModel) { baseChannel ->
FlexibleTopBarWithBackButton(
prefixRow = {
if (baseChannel is LiveActivitiesChannel) {
ShowVideoStreaming(baseChannel, accountViewModel)
}
},
title = {
ShortChannelHeader(
baseChannel = baseChannel,
@@ -377,84 +372,93 @@ fun NoTopBar() {
}
@Composable
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
fun StoriesTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList(
followLists,
list,
true
followListsModel = followLists,
listName = list,
withGlobal = true,
withRoutes = false
) { listName ->
accountViewModel.account.changeDefaultStoriesFollowList(listName)
accountViewModel.account.changeDefaultStoriesFollowList(listName.code)
}
}
}
@Composable
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
fun HomeTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS)
FollowList(
followLists,
list,
true
followListsModel = followLists,
listName = list,
withGlobal = true,
withRoutes = true
) { listName ->
accountViewModel.account.changeDefaultHomeFollowList(listName)
if (listName.type == CodeNameType.ROUTE) {
nav(listName.code)
} else {
accountViewModel.account.changeDefaultHomeFollowList(listName.code)
}
}
}
}
@Composable
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
fun NotificationTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList(
followLists,
list,
true
followListsModel = followLists,
listName = list,
withGlobal = true,
withRoutes = false
) { listName ->
accountViewModel.account.changeDefaultNotificationFollowList(listName)
accountViewModel.account.changeDefaultNotificationFollowList(listName.code)
}
}
}
@Composable
fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
fun DiscoveryTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList(
followLists,
list,
true
followListsModel = followLists,
listName = list,
withGlobal = true,
withRoutes = false
) { listName ->
accountViewModel.account.changeDefaultDiscoveryFollowList(listName)
accountViewModel.account.changeDefaultDiscoveryFollowList(listName.code)
}
}
}
@Composable
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(scaffoldState, accountViewModel, nav) {
fun MainTopBar(drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
GenericMainTopBar(drawerState, accountViewModel, nav) {
AmethystClickableIcon()
}
}
@OptIn(coil.annotation.ExperimentalCoilApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GenericMainTopBar(
scaffoldState: ScaffoldState,
drawerState: DrawerState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
content: @Composable (AccountViewModel) -> Unit
) {
Column(modifier = BottomTopHeight) {
MyTopAppBar(
elevation = 0.dp,
backgroundColor = MaterialTheme.colors.surface,
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
),
title = {
Column(
modifier = Modifier.fillMaxWidth(),
@@ -477,7 +481,7 @@ fun GenericMainTopBar(
val coroutineScope = rememberCoroutineScope()
LoggedInUserPictureDrawer(accountViewModel) {
coroutineScope.launch {
scaffoldState.drawerState.open()
drawerState.open()
}
}
},
@@ -509,6 +513,14 @@ private fun LoggedInUserPictureDrawer(
val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex }
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
IconButton(
onClick = onClick
) {
@@ -517,43 +529,88 @@ private fun LoggedInUserPictureDrawer(
model = profilePicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = HeaderPictureModifier,
contentScale = ContentScale.Crop
contentScale = ContentScale.Crop,
loadProfilePicture = automaticallyShowProfilePicture
)
}
}
@Composable
fun FollowList(followListsModel: FollowListViewModel, listName: String, withGlobal: Boolean, onChange: (String) -> Unit) {
val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows))
val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global))
fun FollowList(
followListsModel: FollowListViewModel,
listName: String,
withGlobal: Boolean,
withRoutes: Boolean,
onChange: (CodeName) -> Unit
) {
val context = LocalContext.current
val kind3Follow = CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows, context), CodeNameType.HARDCODED)
val globalFollow = CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global, context), CodeNameType.HARDCODED)
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
val followLists by followListsModel.followLists.collectAsState()
val followLists by followListsModel.peopleLists.collectAsState()
val routeList by followListsModel.routes.collectAsState()
val allLists = remember(followLists) {
(defaultOptions + followLists)
if (withRoutes) {
(defaultOptions + followLists + routeList)
} else {
(defaultOptions + followLists)
}
}
val followNames by remember(followLists) {
derivedStateOf {
allLists.map { it.second }.toImmutableList()
allLists.map { it.name }.toImmutableList()
}
}
SimpleTextSpinner(
placeholder = allLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
placeholder = allLists.firstOrNull { it.code == listName }?.name?.name() ?: "Select an Option",
options = followNames,
onSelect = {
onChange(allLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
onChange(allLists.getOrNull(it) ?: kind3Follow)
}
)
}
enum class CodeNameType {
HARDCODED, PEOPLE_LIST, ROUTE
}
abstract class Name {
abstract fun name(): String
}
class GeoHashName(val geoHashTag: String) : Name() {
override fun name() = "/g/$geoHashTag"
}
class HashtagName(val hashTag: String) : Name() {
override fun name() = "#$hashTag"
}
class ResourceName(val resourceId: Int, val context: Context) : Name() {
override fun name() = context.getString(resourceId)
}
class PeopleListName(val note: AddressableNote) : Name() {
override fun name() = note.dTag() ?: ""
}
class CommunityName(val note: AddressableNote) : Name() {
override fun name() = "/n/${(note.dTag() ?: "")}"
}
@Immutable
data class CodeName(val code: String, val name: Name, val type: CodeNameType)
@Stable
class FollowListViewModel(val account: Account) : ViewModel() {
private var _followLists = MutableStateFlow<ImmutableList<Pair<String, String>>>(emptyList<Pair<String, String>>().toPersistentList())
val followLists = _followLists.asStateFlow()
private var _peopleLists = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
val peopleLists = _peopleLists.asStateFlow()
private var _routes = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
val routes = _routes.asStateFlow()
fun refresh() {
viewModelScope.launch(Dispatchers.Default) {
@@ -571,14 +628,34 @@ class FollowListViewModel(val account: Account) : ViewModel() {
event.pubKey == account.userProfile().pubkeyHex &&
(event.tags.size > 1 || event.content.length > 50)
) {
Pair(event.dTag(), event.dTag())
CodeName(event.address().toTag(), PeopleListName(it.value), CodeNameType.PEOPLE_LIST)
} else {
null
}
}.sortedBy { it.second }.toImmutableList()
}.sortedBy { it.name.name() }.toImmutableList()
if (!equalImmutableLists(_followLists.value, newFollowLists)) {
_followLists.emit(newFollowLists)
if (!equalImmutableLists(_peopleLists.value, newFollowLists)) {
_peopleLists.emit(newFollowLists)
}
val communities = account.userProfile().cachedFollowingCommunitiesSet().mapNotNull {
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
CodeName("Community/${communityNote.idHex}", CommunityName(communityNote), CodeNameType.ROUTE)
}
}
val hashtags = account.userProfile().cachedFollowingTagSet().map {
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
}
val geotags = account.userProfile().cachedFollowingGeohashSet().map {
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
}
val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() }.toImmutableList()
if (!equalImmutableLists(_routes.value, routeList)) {
_routes.emit(routeList)
}
}
@@ -590,7 +667,10 @@ class FollowListViewModel(val account: Account) : ViewModel() {
collectorJob = viewModelScope.launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect { newNotes ->
checkNotInMainThread()
if (newNotes.any { it.event is PeopleListEvent }) {
if (newNotes.any {
it.event?.pubKey() == account.userProfile().pubkeyHex && (it.event is PeopleListEvent || it.event is ContactListEvent)
}
) {
refresh()
}
}
@@ -612,8 +692,7 @@ class FollowListViewModel(val account: Account) : ViewModel() {
@Composable
fun SimpleTextSpinner(
placeholder: String,
options: ImmutableList<String>,
explainers: ImmutableList<String>? = null,
options: ImmutableList<Name>,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier
) {
@@ -632,7 +711,7 @@ fun SimpleTextSpinner(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
Box(
@@ -649,20 +728,78 @@ fun SimpleTextSpinner(
if (optionsShowing) {
options.isNotEmpty().also {
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) {
currentText = options[it]
optionsShowing = false
onSelect(it)
SpinnerSelectionDialog(
options = options,
onDismiss = { optionsShowing = false },
onSelect = {
currentText = options[it].name()
optionsShowing = false
onSelect(it)
}
) {
RenderOption(it)
}
}
}
}
@Composable
fun RenderOption(it: Name) {
when (it) {
is GeoHashName -> {
LoadCityName(it.geoHashTag) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
}
}
}
is HashtagName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = it.name(), color = MaterialTheme.colorScheme.onSurface)
}
}
is ResourceName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = it.resourceId), color = MaterialTheme.colorScheme.onSurface)
}
}
is PeopleListName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = it.name(), color = MaterialTheme.colorScheme.onSurface)
}
}
is CommunityName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
val name by it.note.live().metadata.map {
"/n/" + (it.note as? AddressableNote)?.dTag()
}.observeAsState()
Text(text = name ?: "", color = MaterialTheme.colorScheme.onSurface)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopBarWithBackButton(caption: String, popBack: () -> Unit) {
Column(modifier = BottomTopHeight) {
MyTopAppBar(
elevation = 0.dp,
TopAppBar(
title = { Text(caption) },
navigationIcon = {
IconButton(
@@ -680,15 +817,12 @@ fun TopBarWithBackButton(caption: String, popBack: () -> Unit) {
@Composable
fun FlexibleTopBarWithBackButton(
prefixRow: (@Composable () -> Unit)? = null,
title: @Composable RowScope.() -> Unit,
extendableRow: (@Composable () -> Unit)? = null,
popBack: () -> Unit
) {
Column() {
MyExtensibleTopAppBar(
elevation = 0.dp,
prefixRow = prefixRow,
title = title,
extendableRow = extendableRow,
navigationIcon = {
@@ -766,135 +900,53 @@ fun debugState(context: Context) {
}
}
@Composable
fun MyTopAppBar(
title: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
) {
Surface(
contentColor = contentColor,
elevation = elevation,
modifier = modifier
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Row(
Modifier
.fillMaxWidth()
.padding(AppBarDefaults.ContentPadding),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
if (navigationIcon == null) {
Spacer(TitleInsetWithoutIcon)
} else {
Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
content = navigationIcon
)
}
}
Row(
Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
ProvideTextStyle(MaterialTheme.typography.h6) {
title()
}
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
content = actions
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyExtensibleTopAppBar(
prefixRow: (@Composable () -> Unit)? = null,
title: @Composable RowScope.() -> Unit,
extendableRow: (@Composable () -> Unit)? = null,
modifier: Modifier = Modifier,
navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
actions: @Composable RowScope.() -> Unit = {}
) {
val expanded = remember { mutableStateOf(false) }
Surface(
color = backgroundColor,
contentColor = contentColor,
elevation = elevation,
modifier = modifier.clickable {
Column(
Modifier.clickable {
expanded.value = !expanded.value
}
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Column(Modifier.fillMaxWidth()) {
Row(
Modifier
.fillMaxWidth()
.padding(AppBarDefaults.ContentPadding),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Row(modifier = BottomTopHeight) {
TopAppBar(
title = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
title()
}
},
modifier = modifier,
navigationIcon = {
if (navigationIcon == null) {
Spacer(TitleInsetWithoutIcon)
} else {
Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
content = navigationIcon
)
navigationIcon()
}
}
},
actions = actions
)
}
Row(
Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
ProvideTextStyle(MaterialTheme.typography.h6) {
title()
}
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
content = actions
)
}
}
if (expanded.value && extendableRow != null) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Column {
extendableRow()
}
}
}
if (prefixRow != null) {
prefixRow()
if (expanded.value && extendableRow != null) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Column {
extendableRow()
}
}
}
@@ -22,21 +22,20 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AlertDialog
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.ScaffoldState
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
@@ -70,8 +69,10 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
@@ -92,17 +93,25 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DrawerContent(
nav: (String) -> Unit,
scaffoldState: ScaffoldState,
sheetState: ModalBottomSheetState,
drawerState: DrawerState,
openSheet: () -> Unit,
accountViewModel: AccountViewModel
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.background
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
ModalDrawerSheet(
drawerContainerColor = MaterialTheme.colorScheme.background,
drawerTonalElevation = 0.dp
) {
Column() {
ProfileContent(
@@ -111,7 +120,7 @@ fun DrawerContent(
.fillMaxWidth()
.padding(horizontal = 25.dp)
.padding(top = 70.dp),
scaffoldState,
drawerState,
accountViewModel,
nav
)
@@ -121,15 +130,15 @@ fun DrawerContent(
)
ListContent(
nav,
scaffoldState,
sheetState,
drawerState,
openSheet,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
accountViewModel
)
BottomContent(accountViewModel.account.userProfile(), scaffoldState, nav)
BottomContent(accountViewModel.account.userProfile(), drawerState, automaticallyShowProfilePicture, nav)
}
}
}
@@ -138,7 +147,7 @@ fun DrawerContent(
fun ProfileContent(
baseAccountUser: User,
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState,
drawerState: DrawerState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
@@ -150,11 +159,19 @@ fun ProfileContent(
val profileBanner = remember(accountUserState) { accountUserState?.user?.info?.banner?.ifBlank { null } }
val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture() }
val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() }
val bestDisplayName = remember(accountUserState) { accountUserState?.user?.bestDisplayName() }
// val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() }
val bestDisplayName = remember(accountUserState) { accountUserState?.user?.toBestDisplayName() }
val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" }
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
Box {
if (profileBanner != null) {
AsyncImage(
@@ -170,8 +187,7 @@ fun ProfileContent(
painter = painterResource(R.drawable.profile_banner),
contentDescription = stringResource(R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
.height(120.dp)
)
}
@@ -185,14 +201,15 @@ fun ProfileContent(
.width(100.dp)
.height(100.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colors.background, CircleShape)
.background(MaterialTheme.colors.background)
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape)
.background(MaterialTheme.colorScheme.background)
.clickable(onClick = {
nav(route)
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
})
}),
loadProfilePicture = automaticallyShowProfilePicture
)
if (bestDisplayName != null) {
@@ -204,7 +221,7 @@ fun ProfileContent(
.clickable(onClick = {
nav(route)
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
}),
fontWeight = FontWeight.Bold,
@@ -213,24 +230,6 @@ fun ProfileContent(
overflow = TextOverflow.Ellipsis
)
}
if (bestUserName != null) {
CreateTextWithEmoji(
text = remember { " @$bestUserName" },
tags = tags,
color = Color.LightGray,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.clickable(
onClick = {
nav(route)
coroutineScope.launch {
scaffoldState.drawerState.close()
}
}
)
)
}
Row(Modifier.padding(top = Size10dp)) {
EditStatusBox(baseAccountUser, accountViewModel)
@@ -242,7 +241,7 @@ fun ProfileContent(
.clickable(onClick = {
nav(route)
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
})
) {
@@ -257,7 +256,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
LoadStatuses(user = baseAccountUser) { statuses ->
LoadStatuses(user = baseAccountUser, accountViewModel) { statuses ->
if (statuses.isEmpty()) {
val currentStatus = remember {
mutableStateOf("")
@@ -276,7 +275,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
placeholder = {
Text(
text = stringResource(R.string.status_update),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -292,7 +291,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
singleLine = true,
trailingIcon = {
if (hasChanged) {
UserStatusSendButton() {
SendButton() {
accountViewModel.createStatus(currentStatus.value)
focusManager.clearFocus(true)
}
@@ -322,7 +321,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
placeholder = {
Text(
text = stringResource(R.string.status_update),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -338,7 +337,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
singleLine = true,
trailingIcon = {
if (hasChanged) {
UserStatusSendButton() {
SendButton() {
accountViewModel.updateStatus(it, thisStatus.value)
focusManager.clearFocus(true)
}
@@ -356,7 +355,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
}
@Composable
fun UserStatusSendButton(onClick: () -> Unit) {
fun SendButton(onClick: () -> Unit) {
IconButton(
modifier = Size26Modifier,
onClick = onClick
@@ -365,7 +364,7 @@ fun UserStatusSendButton(onClick: () -> Unit) {
imageVector = Icons.Default.Send,
null,
modifier = Size20Modifier,
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
}
@@ -380,7 +379,7 @@ fun UserStatusDeleteButton(onClick: () -> Unit) {
imageVector = Icons.Default.Delete,
null,
modifier = Size20Modifier,
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
}
@@ -441,12 +440,11 @@ fun WatchFollower(baseAccountUser: User, onReady: (String) -> Unit) {
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ListContent(
nav: (String) -> Unit,
scaffoldState: ScaffoldState,
sheetState: ModalBottomSheetState,
drawerState: DrawerState,
openSheet: () -> Unit,
modifier: Modifier,
accountViewModel: AccountViewModel
) {
@@ -474,18 +472,18 @@ fun ListContent(
NavigationRow(
title = stringResource(R.string.profile),
icon = Route.Profile.icon,
tint = MaterialTheme.colors.primary,
tint = MaterialTheme.colorScheme.primary,
nav = nav,
scaffoldState = scaffoldState,
drawerState = drawerState,
route = route
)
NavigationRow(
title = stringResource(R.string.bookmarks),
icon = Route.Bookmarks.icon,
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
nav = nav,
scaffoldState = scaffoldState,
drawerState = drawerState,
route = Route.Bookmarks.route
)
@@ -493,7 +491,7 @@ fun ListContent(
accountViewModel = accountViewModel,
onClick = {
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
wantsToEditRelays = true
}
@@ -502,32 +500,34 @@ fun ListContent(
NavigationRow(
title = stringResource(R.string.security_filters),
icon = Route.BlockedUsers.icon,
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
nav = nav,
scaffoldState = scaffoldState,
drawerState = drawerState,
route = Route.BlockedUsers.route
)
IconRow(
title = stringResource(R.string.backup_keys),
icon = R.drawable.ic_key,
tint = MaterialTheme.colors.onBackground,
onClick = {
coroutineScope.launch {
scaffoldState.drawerState.close()
accountViewModel.account.keyPair.privKey?.let {
IconRow(
title = stringResource(R.string.backup_keys),
icon = R.drawable.ic_key,
tint = MaterialTheme.colorScheme.onBackground,
onClick = {
coroutineScope.launch {
drawerState.close()
}
backupDialogOpen = true
}
backupDialogOpen = true
}
)
)
}
val textTorProxy = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor_short)
IconRow(
title = textTorProxy,
icon = R.drawable.ic_tor,
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
onLongClick = {
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
conectOrbotDialogOpen = true
},
@@ -536,7 +536,7 @@ fun ListContent(
disconnectTorDialog = true
} else {
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
conectOrbotDialogOpen = true
}
@@ -546,9 +546,9 @@ fun ListContent(
NavigationRow(
title = stringResource(R.string.settings),
icon = Route.Settings.icon,
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
nav = nav,
scaffoldState = scaffoldState,
drawerState = drawerState,
route = Route.Settings.route
)
@@ -557,8 +557,8 @@ fun ListContent(
IconRow(
title = stringResource(R.string.drawer_accounts),
icon = R.drawable.manage_accounts,
tint = MaterialTheme.colors.onBackground,
onClick = { coroutineScope.launch { sheetState.show() } }
tint = MaterialTheme.colorScheme.onBackground,
onClick = openSheet
)
}
@@ -649,7 +649,7 @@ private fun RenderRelayStatus(
}
}
val placeHolder = MaterialTheme.colors.placeholderText
val placeHolder = MaterialTheme.colorScheme.placeholderText
val color by remember(relayPool) {
derivedStateOf {
@@ -664,7 +664,7 @@ private fun RenderRelayStatus(
Text(
text = text,
color = color,
style = MaterialTheme.typography.subtitle1
style = MaterialTheme.typography.titleMedium
)
}
@@ -674,14 +674,14 @@ fun NavigationRow(
icon: Int,
tint: Color,
nav: (String) -> Unit,
scaffoldState: ScaffoldState,
drawerState: DrawerState,
route: String
) {
val coroutineScope = rememberCoroutineScope()
IconRow(title, icon, tint, onClick = {
nav(route)
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
})
}
@@ -735,7 +735,7 @@ fun IconRowRelays(accountViewModel: AccountViewModel, onClick: () -> Unit) {
painter = painterResource(R.drawable.relays),
null,
modifier = Modifier.size(22.dp),
tint = MaterialTheme.colors.onSurface
tint = MaterialTheme.colorScheme.onSurface
)
Text(
@@ -752,7 +752,7 @@ fun IconRowRelays(accountViewModel: AccountViewModel, onClick: () -> Unit) {
}
@Composable
fun BottomContent(user: User, scaffoldState: ScaffoldState, nav: (String) -> Unit) {
fun BottomContent(user: User, drawerState: DrawerState, loadProfilePicture: Boolean, nav: (String) -> Unit) {
val coroutineScope = rememberCoroutineScope()
// store the dialog open or close state
@@ -791,21 +791,21 @@ fun BottomContent(user: User, scaffoldState: ScaffoldState, nav: (String) -> Uni
painter = painterResource(R.drawable.ic_theme),
null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}*/
Box(modifier = Modifier.weight(1F))
IconButton(onClick = {
dialogOpen = true
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
}) {
Icon(
painter = painterResource(R.drawable.ic_qrcode),
null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}
}
@@ -814,10 +814,11 @@ fun BottomContent(user: User, scaffoldState: ScaffoldState, nav: (String) -> Uni
if (dialogOpen) {
ShowQRDialog(
user,
loadProfilePicture = loadProfilePicture,
onScan = {
dialogOpen = false
coroutineScope.launch {
scaffoldState.drawerState.close()
drawerState.close()
}
nav(it)
},
@@ -150,6 +150,14 @@ sealed class Route(
route = "Settings",
icon = R.drawable.ic_settings
)
companion object {
val InvertedLayouts = setOf(
Channel.route,
Room.route,
RoomByAuthor.route
)
}
}
// **
@@ -10,13 +10,14 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -36,6 +37,8 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AddBountyAmountViewModel : ViewModel() {
private var account: Account? = null
@@ -80,6 +83,7 @@ class AddBountyAmountViewModel : ViewModel() {
fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onClose: () -> Unit) {
val postViewModel: AddBountyAmountViewModel = viewModel()
postViewModel.load(accountViewModel.account, bounty)
val scope = rememberCoroutineScope()
Dialog(
onDismissRequest = { onClose() },
@@ -89,7 +93,11 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
)
) {
Surface() {
Column(modifier = Modifier.padding(10.dp).width(IntrinsicSize.Min)) {
Column(
modifier = Modifier
.padding(10.dp)
.width(IntrinsicSize.Min)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@@ -102,8 +110,10 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
PostButton(
onPost = {
postViewModel.sendPost()
onClose()
scope.launch(Dispatchers.IO) {
postViewModel.sendPost()
onClose()
}
},
isActive = postViewModel.hasChanged()
)
@@ -128,7 +138,7 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
placeholder = {
Text(
text = "10000, 50000, 5000000",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
singleLine = true
@@ -9,14 +9,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MilitaryTech
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -59,9 +59,9 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
if (note == null) {
BlankNote(Modifier, isInnerNote)
} else {
val defaultBackgroundColor = MaterialTheme.colors.background
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
LaunchedEffect(key1 = likeSetCard) {
scope.launch(Dispatchers.IO) {
@@ -117,7 +117,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
modifier = Modifier
.size(25.dp)
.align(Alignment.TopEnd),
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}
}
@@ -134,7 +134,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
Text(
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1
)
@@ -146,7 +146,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
NoteDropDownMenu(note, popupExpanded, accountViewModel)
@@ -7,11 +7,11 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -108,7 +108,7 @@ fun HiddenNote(
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
contentColor = MaterialTheme.colorScheme.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
@@ -15,9 +15,9 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
@@ -275,8 +275,8 @@ private fun CheckNewAndRenderChannelCard(
showPopup: () -> Unit,
nav: (String) -> Unit
) {
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colors.background
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
@@ -614,9 +614,9 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
}
description?.let {
@@ -624,7 +624,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
Row() {
Text(
text = it,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp
@@ -646,7 +646,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
fun RenderChannelThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteEvent = baseNote.event as? ChannelCreateEvent ?: return
LoadChannel(baseChannelHex = baseNote.idHex) {
LoadChannel(baseChannelHex = baseNote.idHex, accountViewModel) {
RenderChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav)
}
}
@@ -728,9 +728,9 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
}
description?.let {
@@ -738,7 +738,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
Row() {
Text(
text = it,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp
@@ -768,7 +768,7 @@ fun Gallery(users: List<User>, accountViewModel: AccountViewModel) {
Text(
text = remember(users) { " + " + (showCount(users.size - 6)).toString() },
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface
color = MaterialTheme.colorScheme.onSurface
)
}
}
@@ -14,10 +14,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.Divider
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
@@ -47,9 +47,10 @@ import androidx.lifecycle.map
import com.patrykandpatrick.vico.core.extension.forEachIndexedExtended
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -99,7 +100,7 @@ fun ChatroomComposeChannelOrUser(
}
if (channelHex != null) {
ChatroomChannel(channelHex, baseNote, accountViewModel, nav)
ChatroomChannel(channelHex!!, baseNote, accountViewModel, nav)
} else {
ChatroomPrivateMessages(baseNote, accountViewModel, nav)
}
@@ -134,12 +135,12 @@ private fun ChatroomPrivateMessages(
@Composable
private fun ChatroomChannel(
channelHex: HexKey?,
channelHex: HexKey,
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
LoadChannel(baseChannelHex = channelHex!!) { channel ->
LoadChannel(baseChannelHex = channelHex, accountViewModel) { channel ->
ChannelRoomCompose(baseNote, channel, accountViewModel, nav)
}
}
@@ -186,6 +187,14 @@ private fun ChannelRoomCompose(
val hasNewMessages = remember { mutableStateOf<Boolean>(false) }
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
if (hasNewMessages.value != newHasNewMessages) {
hasNewMessages.value = newHasNewMessages
@@ -201,6 +210,7 @@ private fun ChannelRoomCompose(
channelLastTime = remember(note) { note.createdAt() },
channelLastContent = remember(note) { "$authorName: $description" },
hasNewMessages = hasNewMessages,
loadProfilePicture = automaticallyShowProfilePicture,
onClick = { nav(route) }
)
}
@@ -208,7 +218,7 @@ private fun ChannelRoomCompose(
@Composable
private fun ChannelTitleWithLabelInfo(channelName: String, modifier: Modifier) {
val label = stringResource(id = R.string.public_chat)
val placeHolderColor = MaterialTheme.colors.placeholderText
val placeHolderColor = MaterialTheme.colorScheme.placeholderText
val channelNameAndBoostInfo = remember(channelName) {
buildAnnotatedString {
withStyle(
@@ -256,11 +266,10 @@ private fun UserRoomCompose(
note.createdAt()
}
}
val content by remember(note) {
derivedStateOf {
mutableStateOf(
accountViewModel.decrypt(note)
}
)
}
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
@@ -278,7 +287,7 @@ private fun UserRoomCompose(
)
},
channelTitle = {
RoomNameDisplay(room, it, accountViewModel.userProfile())
RoomNameDisplay(room, it, accountViewModel)
},
channelLastTime = createAt,
channelLastContent = content,
@@ -288,20 +297,20 @@ private fun UserRoomCompose(
}
@Composable
fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, loggedInUser: User) {
val roomSubject by loggedInUser.live().messages.map {
fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, accountViewModel: AccountViewModel) {
val roomSubject by accountViewModel.userProfile().live().messages.map {
it.user.privateChatrooms[room]?.subject
}.distinctUntilChanged().observeAsState(loggedInUser.privateChatrooms[room]?.subject)
}.distinctUntilChanged().observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject)
Crossfade(targetState = roomSubject, modifier) {
if (it != null && it.isNotBlank()) {
if (room.users.size > 1) {
DisplayRoomSubject(it)
} else {
DisplayUserAndSubject(room.users.first(), it)
DisplayUserAndSubject(room.users.first(), it, accountViewModel)
}
} else {
DisplayUserSetAsSubject(room)
DisplayUserSetAsSubject(room, accountViewModel)
}
}
}
@@ -309,7 +318,8 @@ fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, loggedInUser: User) {
@Composable
private fun DisplayUserAndSubject(
user: HexKey,
subject: String
subject: String,
accountViewModel: AccountViewModel
) {
Row() {
Text(
@@ -323,7 +333,7 @@ private fun DisplayUserAndSubject(
fontWeight = FontWeight.Bold,
maxLines = 1
)
LoadUser(baseUserHex = user) {
LoadUser(baseUserHex = user, accountViewModel = accountViewModel) {
it?.let {
UsernameDisplay(it, Modifier.weight(1f))
}
@@ -334,6 +344,7 @@ private fun DisplayUserAndSubject(
@Composable
fun DisplayUserSetAsSubject(
room: ChatroomKey,
accountViewModel: AccountViewModel,
fontWeight: FontWeight = FontWeight.Bold
) {
val userList = remember(room) {
@@ -343,7 +354,7 @@ fun DisplayUserSetAsSubject(
if (userList.size == 1) {
// Regular Design
Row() {
LoadUser(baseUserHex = userList[0]) {
LoadUser(baseUserHex = userList[0], accountViewModel) {
it?.let {
UsernameDisplay(it, Modifier.weight(1f), fontWeight = fontWeight)
}
@@ -352,7 +363,7 @@ fun DisplayUserSetAsSubject(
} else {
Row() {
userList.take(4).forEachIndexedExtended { index, isFirst, isLast, value ->
LoadUser(baseUserHex = value) {
LoadUser(baseUserHex = value, accountViewModel) {
it?.let {
ShortUsernameDisplay(baseUser = it, fontWeight = fontWeight)
}
@@ -417,15 +428,14 @@ private fun WatchNotificationChanges(
}
@Composable
fun LoadUser(baseUserHex: String, content: @Composable (User?) -> Unit) {
fun LoadUser(baseUserHex: String, accountViewModel: AccountViewModel, content: @Composable (User?) -> Unit) {
var user by remember(baseUserHex) {
mutableStateOf(LocalCache.getUserIfExists(baseUserHex))
mutableStateOf(accountViewModel.getUserIfExists(baseUserHex))
}
if (user == null) {
LaunchedEffect(key1 = baseUserHex) {
launch(Dispatchers.IO) {
val newUser = LocalCache.checkGetOrCreateUser(baseUserHex)
accountViewModel.checkGetOrCreateUser(baseUserHex) { newUser ->
if (user != newUser) {
user = newUser
}
@@ -444,6 +454,7 @@ fun ChannelName(
channelLastTime: Long?,
channelLastContent: String?,
hasNewMessages: MutableState<Boolean>,
loadProfilePicture: Boolean,
onClick: () -> Unit
) {
ChannelName(
@@ -457,7 +468,8 @@ fun ChannelName(
.width(Size55dp)
.height(Size55dp)
.clip(shape = CircleShape)
}
},
loadProfilePicture = loadProfilePicture
)
},
channelTitle,
@@ -518,7 +530,7 @@ private fun SecondRow(channelLastContent: String?, hasNewMessages: MutableState<
if (channelLastContent != null) {
Text(
channelLastContent,
color = MaterialTheme.colors.grayText,
color = MaterialTheme.colorScheme.grayText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
@@ -527,7 +539,7 @@ private fun SecondRow(channelLastContent: String?, hasNewMessages: MutableState<
} else {
Text(
stringResource(R.string.referenced_event_not_found),
color = MaterialTheme.colors.grayText,
color = MaterialTheme.colorScheme.grayText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
@@ -561,7 +573,7 @@ private fun TimeAgo(channelLastTime: Long?) {
}
Text(
text = timeAgo,
color = MaterialTheme.colors.grayText,
color = MaterialTheme.colorScheme.grayText,
maxLines = 1
)
}
@@ -574,7 +586,7 @@ fun NewItemsBubble() {
.width(10.dp)
.height(10.dp)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.primary),
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
@@ -15,16 +15,17 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -42,8 +43,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
@@ -66,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists
@@ -228,9 +232,9 @@ fun NormalChatNote(
}
}
val loggedInColors = MaterialTheme.colors.mediumImportanceLink
val otherColors = MaterialTheme.colors.subtleBorder
val defaultBackground = MaterialTheme.colors.background
val loggedInColors = MaterialTheme.colorScheme.mediumImportanceLink
val otherColors = MaterialTheme.colorScheme.subtleBorder
val defaultBackground = MaterialTheme.colorScheme.background
val backgroundBubbleColor = remember {
if (accountViewModel.isLoggedUser(note.author)) {
@@ -285,7 +289,7 @@ fun NormalChatNote(
modifier = modif,
horizontalArrangement = alignment
) {
val availableBubbleSize = remember { mutableStateOf(0) }
val availableBubbleSize = remember { mutableIntStateOf(0) }
var popupExpanded by remember { mutableStateOf(false) }
val modif2 = remember {
@@ -355,7 +359,7 @@ private fun RenderBubble(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val bubbleSize = remember { mutableStateOf(0) }
val bubbleSize = remember { mutableIntStateOf(0) }
val bubbleModifier = remember {
Modifier
@@ -398,10 +402,19 @@ private fun MessageBubbleLines(
bubbleSize: MutableState<Int>,
availableBubbleSize: MutableState<Int>
) {
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
if (drawAuthorInfo) {
DrawAuthorInfo(
baseNote,
alignment,
automaticallyShowProfilePicture,
nav
)
} else {
@@ -559,13 +572,13 @@ private fun StatusRow(
Column(modifier = ReactionRowHeightChat) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav)
ZapReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav)
Spacer(modifier = DoubleHorzSpacer)
ReplyReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colors.placeholderText,
grayTint = MaterialTheme.colorScheme.placeholderText,
accountViewModel = accountViewModel,
showCounter = false,
iconSize = Size15dp
@@ -586,7 +599,7 @@ fun IncognitoBadge(baseNote: Note) {
modifier = Modifier
.padding(top = 1.dp)
.size(14.dp),
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
} else if (baseNote.event is PrivateDmEvent) {
Icon(
@@ -595,7 +608,7 @@ fun IncognitoBadge(baseNote: Note) {
modifier = Modifier
.padding(top = 1.dp)
.size(14.dp),
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
}
@@ -612,7 +625,7 @@ fun ChatTimeAgo(baseNote: Note) {
Text(
text = time,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
fontSize = Font12SP,
maxLines = 1
)
@@ -626,8 +639,8 @@ private fun RenderRegularTextNote(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
val eventContent = remember { accountViewModel.decrypt(note) }
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
val modifier = remember { Modifier.padding(top = 5.dp) }
if (eventContent != null) {
@@ -636,7 +649,7 @@ private fun RenderRegularTextNote(
accountViewModel = accountViewModel
) {
TranslatableRichTextViewer(
content = eventContent,
content = eventContent!!,
canPreview = canPreview,
modifier = modifier,
tags = tags,
@@ -710,6 +723,7 @@ private fun RenderCreateChannelNote(note: Note) {
private fun DrawAuthorInfo(
baseNote: Note,
alignment: Arrangement.Horizontal,
loadProfilePicture: Boolean,
nav: (String) -> Unit
) {
Row(
@@ -717,26 +731,28 @@ private fun DrawAuthorInfo(
horizontalArrangement = alignment,
modifier = Modifier.padding(top = 5.dp)
) {
DisplayAndWatchNoteAuthor(baseNote, nav)
DisplayAndWatchNoteAuthor(baseNote, loadProfilePicture, nav)
}
}
@Composable
private fun DisplayAndWatchNoteAuthor(
baseNote: Note,
loadProfilePicture: Boolean,
nav: (String) -> Unit
) {
val author = remember {
baseNote.author
}
author?.let {
WatchAndDisplayUser(it, nav)
WatchAndDisplayUser(it, loadProfilePicture, nav)
}
}
@Composable
private fun WatchAndDisplayUser(
author: User,
loadProfilePicture: Boolean,
nav: (String) -> Unit
) {
val pubkeyHex = remember { author.pubkeyHex }
@@ -762,7 +778,7 @@ private fun WatchAndDisplayUser(
}
}
UserIcon(pubkeyHex, userProfilePicture, nav, route)
UserIcon(pubkeyHex, userProfilePicture, loadProfilePicture, nav, route)
userDisplayName?.let {
DisplayMessageUsername(it, userTags, route, nav)
@@ -773,6 +789,7 @@ private fun WatchAndDisplayUser(
private fun UserIcon(
pubkeyHex: String,
userProfilePicture: String?,
loadProfilePicture: Boolean,
nav: (String) -> Unit,
route: String
) {
@@ -780,6 +797,7 @@ private fun UserIcon(
robot = pubkeyHex,
model = userProfilePicture,
contentDescription = stringResource(id = R.string.profile_image),
loadProfilePicture = loadProfilePicture,
modifier = remember {
Modifier
.width(Size25dp)
@@ -806,7 +824,7 @@ private fun DisplayMessageUsername(
maxLines = 1,
tags = userTags,
fontWeight = FontWeight.Bold,
overrideColor = MaterialTheme.colors.onBackground, // we do not want clickable names in purple here.
overrideColor = MaterialTheme.colorScheme.onBackground, // we do not want clickable names in purple here.
route = route,
nav = nav
)
@@ -1,8 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Bolt
@@ -20,6 +18,8 @@ import androidx.compose.material.icons.filled.VolumeOff
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material.icons.outlined.BarChart
import androidx.compose.material.icons.outlined.PlayCircle
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -61,7 +61,7 @@ fun ArrowBackIcon() {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colors.grayText
tint = MaterialTheme.colorScheme.grayText
)
}
@@ -71,7 +71,7 @@ fun MessageIcon(modifier: Modifier) {
painter = painterResource(R.drawable.ic_dm),
null,
modifier = modifier,
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}
@@ -81,7 +81,7 @@ fun DownloadForOfflineIcon(iconSize: Dp) {
imageVector = Icons.Default.DownloadForOffline,
null,
modifier = remember(iconSize) { Modifier.size(iconSize) },
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}
@@ -181,7 +181,7 @@ fun ExpandLessIcon(modifier: Modifier) {
imageVector = Icons.Default.ExpandLess,
null,
modifier = modifier,
tint = MaterialTheme.colors.subtleButton
tint = MaterialTheme.colorScheme.subtleButton
)
}
@@ -191,7 +191,7 @@ fun ExpandMoreIcon(modifier: Modifier) {
imageVector = Icons.Default.ExpandMore,
null,
modifier = modifier,
tint = MaterialTheme.colors.subtleButton
tint = MaterialTheme.colorScheme.subtleButton
)
}
@@ -226,7 +226,7 @@ fun PollIcon() {
painter = painterResource(R.drawable.ic_poll),
null,
modifier = Size20Modifier,
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
}
@@ -236,7 +236,7 @@ fun RegularPostIcon() {
painter = painterResource(R.drawable.ic_lists),
null,
modifier = Size20Modifier,
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
}
@@ -246,7 +246,7 @@ fun CancelIcon() {
imageVector = Icons.Default.Cancel,
null,
modifier = Size30Modifier,
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
@@ -255,8 +255,7 @@ fun CloseIcon() {
Icon(
painter = painterResource(id = R.drawable.ic_close),
contentDescription = stringResource(id = R.string.cancel),
modifier = Size20Modifier,
tint = Color.White
modifier = Size20Modifier
)
}
@@ -265,7 +264,7 @@ fun MutedIcon() {
Icon(
imageVector = Icons.Default.VolumeOff,
contentDescription = stringResource(id = R.string.muted_button),
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
modifier = Size30Modifier
)
}
@@ -275,7 +274,7 @@ fun MuteIcon() {
Icon(
imageVector = Icons.Default.VolumeUp,
contentDescription = stringResource(id = R.string.mute_button),
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
modifier = Size30Modifier
)
}
@@ -354,7 +353,7 @@ fun VerticalDotsIcon() {
imageVector = Icons.Default.MoreVert,
null,
modifier = Size18Modifier,
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
@@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -43,9 +43,9 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
val scope = rememberCoroutineScope()
val defaultBackgroundColor = MaterialTheme.colors.background
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
LaunchedEffect(key1 = messageSetCard) {
launch(Dispatchers.IO) {
@@ -17,9 +17,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
@@ -30,6 +30,7 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -42,8 +43,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
@@ -67,8 +70,9 @@ import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.EmptyTagList
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -87,9 +91,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
val scope = rememberCoroutineScope()
val defaultBackgroundColor = MaterialTheme.colors.background
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
LaunchedEffect(key1 = multiSetCard) {
launch(Dispatchers.IO) {
@@ -169,7 +173,6 @@ private fun Galeries(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
val likeEvents by remember { derivedStateOf { multiSetCard.likeEventsByType } }
@@ -178,6 +181,16 @@ private fun Galeries(
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
if (hasZapEvents) {
var zapEvents by remember(multiSetCard.zapEvents) {
mutableStateOf<ImmutableList<ZapAmountCommentNotification>>(persistentListOf())
}
LaunchedEffect(key1 = Unit) {
accountViewModel.decryptAmountMessageInGroup(multiSetCard.zapEvents) {
zapEvents = it
}
}
val (value, elapsed) = measureTimedValue {
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
}
@@ -247,7 +260,7 @@ fun RenderLikeGallery(
@Composable
fun RenderZapGallery(
zapEvents: ImmutableList<CombinedZap>,
zapEvents: ImmutableList<ZapAmountCommentNotification>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
@@ -294,10 +307,29 @@ fun RenderBoostGallery(
}
}
@Composable
fun MapZaps(
zaps: ImmutableList<CombinedZap>,
accountViewModel: AccountViewModel,
content: @Composable (ImmutableList<ZapAmountCommentNotification>) -> Unit
) {
var zapEvents by remember(zaps) {
mutableStateOf<ImmutableList<ZapAmountCommentNotification>>(persistentListOf())
}
LaunchedEffect(key1 = Unit) {
accountViewModel.decryptAmountMessageInGroup(zaps) {
zapEvents = it
}
}
content(zapEvents)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AuthorGalleryZaps(
authorNotes: ImmutableList<CombinedZap>,
authorNotes: ImmutableList<ZapAmountCommentNotification>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
@@ -305,24 +337,12 @@ fun AuthorGalleryZaps(
Column(modifier = StdStartPadding) {
FlowRow() {
authorNotes.forEach {
ParseAuthorCommentAndAmount(it, backgroundColor, nav, accountViewModel)
RenderState(it, backgroundColor, accountViewModel, nav)
}
}
}
}
@Composable
private fun ParseAuthorCommentAndAmount(
zap: CombinedZap,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
ParseAuthorCommentAndAmount(zap.request, zap.response, accountViewModel) { state ->
RenderState(state, backgroundColor, accountViewModel, nav)
}
}
@Immutable
data class ZapAmountCommentNotification(
val user: User?,
@@ -358,19 +378,21 @@ private fun ParseAuthorCommentAndAmount(
onReady(content)
}
fun click(content: ZapAmountCommentNotification, nav: (String) -> Unit) {
content.user?.let {
nav(routeFor(it))
}
}
@Composable
private fun RenderState(
content: MutableState<ZapAmountCommentNotification>,
content: ZapAmountCommentNotification,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Row(
modifier = Modifier.clickable {
content.value.user?.let {
nav(routeFor(it))
}
},
modifier = Modifier.clickable { click(content, nav) },
verticalAlignment = Alignment.CenterVertically
) {
DisplayAuthorCommentAndAmount(
@@ -396,76 +418,65 @@ val commentTextSize = 12.sp
@Composable
private fun DisplayAuthorCommentAndAmount(
authorComment: MutableState<ZapAmountCommentNotification>,
authorComment: ZapAmountCommentNotification,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Box(modifier = Size35Modifier, contentAlignment = Alignment.BottomCenter) {
CrossfadeToDisplayPicture(authorComment, accountViewModel)
CrossfadeToDisplayAmount(authorComment)
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(authorComment.user, accountViewModel)
authorComment.amount?.let {
CrossfadeToDisplayAmount(it)
}
}
CrossfadeToDisplayComment(authorComment, backgroundColor, nav, accountViewModel)
}
@Composable
fun CrossfadeToDisplayPicture(authorComment: MutableState<ZapAmountCommentNotification>, accountViewModel: AccountViewModel) {
Crossfade(authorComment.value) {
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(it.user, accountViewModel)
authorComment.comment?.let {
CrossfadeToDisplayComment(it, backgroundColor, nav, accountViewModel)
}
}
@Composable
fun CrossfadeToDisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
Crossfade(authorComment.value, modifier = amountBoxModifier) {
it.amount?.let {
Box(
modifier = amountBoxModifier,
contentAlignment = Alignment.BottomCenter
) {
val backgroundColor = MaterialTheme.colors.overPictureBackground
Box(
modifier = remember {
Modifier
.width(Size35dp)
.background(backgroundColor)
},
contentAlignment = Alignment.BottomCenter
) {
Text(
text = it,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.bitcoinColor,
fontSize = commentTextSize,
modifier = bottomPadding1dp
)
}
}
fun CrossfadeToDisplayAmount(amount: String) {
Box(
modifier = amountBoxModifier,
contentAlignment = Alignment.BottomCenter
) {
val backgroundColor = MaterialTheme.colorScheme.overPictureBackground
Box(
modifier = remember {
Modifier
.width(Size35dp)
.background(backgroundColor)
},
contentAlignment = Alignment.BottomCenter
) {
Text(
text = amount,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.bitcoinColor,
fontSize = commentTextSize,
modifier = bottomPadding1dp
)
}
}
}
@Composable
fun CrossfadeToDisplayComment(
authorComment: MutableState<ZapAmountCommentNotification>,
comment: String,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Crossfade(authorComment.value) {
it.comment?.let {
TranslatableRichTextViewer(
content = it,
canPreview = true,
tags = remember { ImmutableListOfLists() },
modifier = textBoxModifier,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
}
}
TranslatableRichTextViewer(
content = comment,
canPreview = true,
tags = EmptyTagList,
modifier = textBoxModifier,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
}
@OptIn(ExperimentalLayoutApi::class)
@@ -516,26 +527,35 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
author: User,
accountViewModel: AccountViewModel
) {
WatchUserMetadata(author) { baseUserPicture ->
Crossfade(targetState = baseUserPicture) { userPicture ->
RobohashAsyncImageProxy(
robot = author.pubkeyHex,
model = userPicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = MaterialTheme.colors.profile35dpModifier,
contentScale = ContentScale.Crop
)
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
WatchUserMetadata(author) { baseUserPicture ->
// Crossfade(targetState = baseUserPicture) { userPicture ->
RobohashAsyncImageProxy(
robot = author.pubkeyHex,
model = baseUserPicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = MaterialTheme.colorScheme.profile35dpModifier,
contentScale = ContentScale.Crop,
loadProfilePicture = automaticallyShowProfilePicture
)
// }
}
WatchUserFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
Crossfade(targetState = isFollowing) {
if (it) {
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
FollowingIcon(Size10dp)
}
// Crossfade(targetState = isFollowing) {
if (isFollowing) {
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
FollowingIcon(Size10dp)
}
}
// }
}
}
@@ -6,13 +6,13 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
@@ -106,7 +106,7 @@ fun ObserveDisplayNip05Status(
) {
val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05())
LoadStatuses(baseUser) { statuses ->
LoadStatuses(baseUser, accountViewModel) { statuses ->
Crossfade(targetState = nip05, modifier = columnModifier, label = "ObserveDisplayNip05StatusCrossfade") {
VerifyAndDisplayNIP05OrStatusLine(it, statuses, baseUser, columnModifier, accountViewModel, nav)
}
@@ -172,7 +172,7 @@ fun DisplayUsersNpub(npub: String) {
Text(
text = npub,
fontSize = 14.sp,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -205,7 +205,7 @@ fun DisplayStatus(
painter = painterResource(id = R.drawable.tunestr),
null,
modifier = Size15Modifier.padding(end = Size5dp),
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
else -> {}
}
@@ -213,7 +213,7 @@ fun DisplayStatus(
Text(
text = content,
fontSize = Font14SP,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -229,11 +229,11 @@ fun DisplayStatus(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
tint = MaterialTheme.colorScheme.lessImportantLink
)
}
} else if (nostrATag != null) {
LoadAddressableNote(nostrATag) { note ->
LoadAddressableNote(nostrATag, accountViewModel) { note ->
if (note != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
@@ -249,13 +249,13 @@ fun DisplayStatus(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
tint = MaterialTheme.colorScheme.lessImportantLink
)
}
}
}
} else if (nostrHexID != null) {
LoadNote(baseNoteHex = nostrHexID) {
LoadNote(baseNoteHex = nostrHexID, accountViewModel) {
if (it != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
@@ -271,7 +271,7 @@ fun DisplayStatus(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
tint = MaterialTheme.colorScheme.lessImportantLink
)
}
}
@@ -298,7 +298,7 @@ private fun DisplayNIP05(
Text(
text = remember(nip05) { AnnotatedString(user) },
fontSize = Font14SP,
color = MaterialTheme.colors.nip05,
color = MaterialTheme.colorScheme.nip05,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -309,7 +309,7 @@ private fun DisplayNIP05(
ClickableText(
text = remember(nip05) { AnnotatedString(domain) },
onClick = { runCatching { uri.openUri("https://$domain") } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.nip05, fontSize = Font14SP),
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
maxLines = 1,
overflow = TextOverflow.Visible
)
@@ -349,7 +349,7 @@ fun DisplayNip05ProfileStatus(user: User, accountViewModel: AccountViewModel) {
if (user != "_") {
Text(
text = remember { AnnotatedString(user + "@") },
color = MaterialTheme.colors.primary,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -360,7 +360,7 @@ fun DisplayNip05ProfileStatus(user: User, accountViewModel: AccountViewModel) {
ClickableText(
text = AnnotatedString(domain),
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = domainPadStart),
maxLines = 1,
overflow = TextOverflow.Ellipsis

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