Compare commits

...
145 Commits
Author SHA1 Message Date
Vitor Pamplona 55a4b1937f v0.79.3 2023-10-03 11:50:42 -04:00
Vitor Pamplona b8b41f840a Hide's the Video Full Screen dialog buttons together with video controls. 2023-10-03 11:50:22 -04:00
Vitor Pamplona 9f5a71020d Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-10-03 10:21:15 -04:00
Vitor Pamplona e8c9e73985 Fixes the saving of the video position between screens 2023-10-03 10:21:06 -04:00
Vitor PamplonaandGitHub e0dba87286 Merge pull request #632 from atrifat/update-indonesian-translation
Update Indonesian translation
2023-10-03 09:47:45 -04:00
Vitor PamplonaandGitHub 3c308788d8 Merge pull request #635 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-10-03 08:55:05 -04:00
Crowdin Bot 3d28715713 New Crowdin translations by GitHub Action 2023-10-03 12:52:46 +00:00
Vitor PamplonaandGitHub 61d2030934 Merge pull request #634 from davotoula/main
Updated translations for cs/de/sv
2023-10-03 08:51:26 -04:00
David Kaspar 56c67b9ec6 Updated translations for cs/de/sv 2023-10-03 12:07:11 +02:00
David Kaspar 77ed529c26 Updated translations for cs/de/sv 2023-10-03 11:58:38 +02:00
Vitor Pamplona 7eddf4a12a Moves Following icon from crossfade to animatedVisibility. 2023-10-02 18:11:05 -04:00
Vitor Pamplona ff69a4168b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-10-02 17:43:43 -04:00
Vitor Pamplona cec204b7ae Breaks down Compose components in the Discovery tab. 2023-10-02 17:43:38 -04:00
Vitor Pamplona 188ef3762d Allows Base64 images on profiles. 2023-10-02 17:31:34 -04:00
Vitor Pamplona 64c9acb4b2 Removes duplicated method 2023-10-02 17:30:43 -04:00
Vitor PamplonaandGitHub c25018bb64 Merge pull request #633 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-10-02 15:27:37 -04:00
Crowdin Bot c1a5b855df New Crowdin translations by GitHub Action 2023-10-02 19:18:03 +00:00
Vitor Pamplona 0da031fae4 Fixes bug of messages not reappearing after memory trimming. 2023-10-02 15:07:08 -04:00
Vitor Pamplona 72dff060d2 Moves activeOnScreen calculations to a LaunchedEffect 2023-10-02 14:45:04 -04:00
Vitor Pamplona d6f4ffafa1 Only changes shouldShow if the value is different. 2023-10-02 14:44:29 -04:00
Vitor Pamplona 4b36217699 Moves audiothumbnail loader to the viewModel scope 2023-10-02 14:43:49 -04:00
Rif'at Ahdi R 9a109e6fc1 Update Indonesian translation 2023-10-02 09:17:26 +08:00
Vitor Pamplona 227aac5b76 v0.79.2 2023-10-01 12:07:51 -04:00
Vitor Pamplona 82ea72d2d1 Removes last mention to a string 2023-10-01 12:07:17 -04:00
Vitor Pamplona f1637c3d20 v0.79.1 2023-10-01 11:42:53 -04:00
Vitor Pamplona 07be841246 waits 3 seconds before starting the rest. 2023-10-01 11:42:47 -04:00
Vitor Pamplona 3ce5d92556 Making sure shared flows don't suspend. 2023-10-01 11:42:31 -04:00
Vitor Pamplona 8f11396e00 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-10-01 11:29:30 -04:00
Vitor PamplonaandGitHub 2259523387 Merge pull request #630 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-10-01 11:29:25 -04:00
Vitor Pamplona d67711b123 Speeds up cold startup. 2023-10-01 11:28:59 -04:00
Vitor Pamplona c89ca51a1b Updates dependencies 2023-10-01 11:10:37 -04:00
Crowdin Bot c5c951dd59 New Crowdin translations by GitHub Action 2023-10-01 15:09:21 +00:00
Vitor Pamplona 9f68f0227e Moves many Toasts to better designed Information Dialogs. 2023-10-01 11:06:05 -04:00
Vitor Pamplona 7faed2ddd3 stringResource seems to be slower than context.getString 2023-09-30 18:46:21 -04:00
Vitor Pamplona 7dceb701b2 Moves check if it is online to the viewModel scope 2023-09-30 18:45:58 -04:00
Vitor Pamplona e454ccc326 Uses the "host" property as the creator of the stream. 2023-09-30 17:43:46 -04:00
Vitor Pamplona bac50668e8 Speeds the composition of the Topbar 2023-09-30 17:23:27 -04:00
Vitor Pamplona ff9e1556d5 Increases the performance of the Bottom bar's Notification dot calculations 2023-09-30 16:24:51 -04:00
Vitor Pamplona dffb49b071 Uses Lists name instead of d-tag on the Top Bar 2023-09-30 11:45:46 -04:00
Vitor Pamplona 7387d9060f Improves error zap message screens for Material3 2023-09-30 11:37:23 -04:00
Vitor Pamplona 52e1842174 Fixes the repeat payments when the NWC is not setup correctly. 2023-09-30 11:17:17 -04:00
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
192 changed files with 6692 additions and 3815 deletions
+6 -6
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 300
versionName "0.77.3"
versionCode 312
versionName "0.79.3"
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
@@ -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
@@ -171,7 +171,7 @@ dependencies {
playImplementation 'com.google.mlkit:translate:17.0.1'
// PushNotifications
playImplementation platform('com.google.firebase:firebase-bom:32.2.3')
playImplementation platform('com.google.firebase:firebase-bom:32.3.1')
playImplementation 'com.google.firebase:firebase-messaging-ktx'
// Charts
@@ -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"
@@ -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
@@ -7,6 +7,7 @@ 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
@@ -16,6 +17,7 @@ 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
@@ -76,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"
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 {
@@ -272,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()
}
@@ -352,7 +367,11 @@ object LocalPreferences {
val latestContactList = try {
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
println("Decoding Contact List: " + it)
Event.fromJson(it) as ContactListEvent?
if (it != null) {
Event.fromJson(it) as ContactListEvent?
} else {
null
}
}
} catch (e: Throwable) {
Log.w("LocalPreferences", "Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", e)
@@ -413,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, "")
}
@@ -84,6 +84,7 @@ class MultiPlayerPlaybackManager(
playingMap.put(id, mediaSession)
} else {
player.setWakeMode(C.WAKE_MODE_NONE)
cachedPositions.add(uri, player.currentPosition)
cache.put(id, mediaSession)
playingMap.remove(id, mediaSession)
}
@@ -92,7 +93,10 @@ class MultiPlayerPlaybackManager(
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
STATE_IDLE -> {
cachedPositions.add(uri, player.currentPosition)
// only saves if it wqs playing
if (abs(player.currentPosition) > 1) {
cachedPositions.add(uri, player.currentPosition)
}
}
STATE_READY -> {
cachedPositions.get(uri)?.let { lastPosition ->
@@ -102,7 +106,10 @@ class MultiPlayerPlaybackManager(
}
}
else -> {
cachedPositions.add(uri, player.currentPosition)
// only saves if it wqs playing
if (abs(player.currentPosition) > 1) {
cachedPositions.add(uri, player.currentPosition)
}
}
}
}
@@ -33,6 +33,10 @@ import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
object ServiceManager {
@@ -87,9 +91,12 @@ object ServiceManager {
// Notification Elements
NostrHomeDataSource.start()
NostrAccountDataSource.start()
NostrChatroomListDataSource.start()
NostrDiscoveryDataSource.start()
NostrVideoDataSource.start()
GlobalScope.launch(Dispatchers.IO) {
delay(3000)
NostrChatroomListDataSource.start()
NostrDiscoveryDataSource.start()
NostrVideoDataSource.start()
}
// More Info Data Sources
NostrSingleEventDataSource.start()
@@ -135,6 +142,8 @@ object ServiceManager {
account?.let {
LocalCache.pruneOldAndHiddenMessages(it)
NostrChatroomDataSource.clearEOSEs(it)
LocalCache.pruneHiddenMessages(it)
LocalCache.pruneContactLists(accounts)
LocalCache.pruneRepliesAndReactions(accounts)
@@ -142,6 +151,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)
@@ -98,13 +98,13 @@ class Account(
// Observers line up here.
val live: AccountLiveData = AccountLiveData(this)
val liveLanguages: AccountLiveData = AccountLiveData(this)
val liveLastRead: AccountLiveData = AccountLiveData(this)
val saveable: AccountLiveData = AccountLiveData(this)
@Immutable
data class LiveHiddenUsers(
val hiddenUsers: ImmutableSet<String>,
val spammers: ImmutableSet<String>,
val hiddenWords: ImmutableSet<String>,
val showSensitiveContent: Boolean?
)
@@ -129,23 +129,25 @@ class Account(
LiveHiddenUsers(
hiddenUsers = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers
?: persistentSetOf(),
hiddenWords = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
} else {
blockList.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id]
val liveBlockedUsers = blockList.publicAndPrivateUsers(blockList.decryptedContent ?: "")
val liveBlockedWords = blockList.publicAndPrivateWords(blockList.decryptedContent ?: "")
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers,
spammers = localLive?.account?.transientHiddenUsers
?: persistentSetOf(),
hiddenWords = liveBlockedWords,
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
}
} else {
LiveHiddenUsers(
hiddenUsers = persistentSetOf(),
hiddenWords = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers
?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
@@ -153,8 +155,10 @@ class Account(
}
} else {
val liveBlockedUsers = blockList?.publicAndPrivateUsers(keyPair.privKey)
val liveBlockedWords = blockList?.publicAndPrivateWords(keyPair.privKey)
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
hiddenWords = liveBlockedWords ?: persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
@@ -180,6 +184,22 @@ class Account(
saveable.invalidateData()
}
fun updateAutomaticallyShowProfilePicture(
automaticallyShowProfilePicture: ConnectivityType
) {
settings.automaticallyShowProfilePictures = automaticallyShowProfilePicture
live.invalidateData()
saveable.invalidateData()
}
fun updateAutomaticallyHideHavBars(
automaticallyHideHavBars: BooleanType
) {
settings.automaticallyHideNavigationBars = automaticallyHideHavBars
live.invalidateData()
saveable.invalidateData()
}
fun updateAutomaticallyShowImages(
automaticallyShowImages: ConnectivityType
) {
@@ -265,6 +285,7 @@ class Account(
var event = MetadataEvent.create(toString, identities, keyPair.pubKey.toHexKey(), keyPair.privKey)
if (loginWithExternalSigner) {
ExternalSignerUtils.openSigner(event)
val content = ExternalSignerUtils.content[event.id]
if (content.isBlank()) {
return
@@ -874,7 +895,7 @@ class Account(
return returningContactList
}
fun follow(user: User) {
suspend fun follow(user: User) {
if (!isWriteable() && !loginWithExternalSigner) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
@@ -1043,7 +1064,7 @@ class Account(
LocalCache.consume(event)
}
fun unfollow(user: User) {
suspend fun unfollow(user: User) {
if (!isWriteable() && !loginWithExternalSigner) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
@@ -1202,7 +1223,7 @@ class Account(
size = headerInfo.size.toString(),
dimensions = headerInfo.dim,
blurhash = headerInfo.blurHash,
description = headerInfo.description,
alt = headerInfo.alt,
sensitiveContent = headerInfo.sensitiveContent,
pubKey = keyPair.pubKey.toHexKey()
)
@@ -1234,7 +1255,7 @@ class Account(
size = headerInfo.size.toString(),
dimensions = headerInfo.dim,
blurhash = headerInfo.blurHash,
description = headerInfo.description,
alt = headerInfo.alt,
sensitiveContent = headerInfo.sensitiveContent,
privateKey = keyPair.privKey!!
)
@@ -1273,7 +1294,7 @@ class Account(
size = headerInfo.size.toString(),
dimensions = headerInfo.dim,
blurhash = headerInfo.blurHash,
description = headerInfo.description,
alt = headerInfo.alt,
sensitiveContent = headerInfo.sensitiveContent,
keyPair = keyPair
)
@@ -1298,7 +1319,7 @@ class Account(
size = headerInfo.size.toString(),
dimensions = headerInfo.dim,
blurhash = headerInfo.blurHash,
description = headerInfo.description,
alt = headerInfo.alt,
sensitiveContent = headerInfo.sensitiveContent,
keyPair = keyPair
)
@@ -2267,6 +2288,157 @@ class Account(
return returningList
}
fun hideWord(word: String) {
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
if (loginWithExternalSigner) {
val id = blockList?.id
val encryptedContent = if (id == null) {
val privateTags = listOf(listOf("word", word))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypted")
val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: ""
ExternalSignerUtils.content.remove("encrypted")
if (encryptedContent.isBlank()) return
encryptedContent
} else {
var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id]
if (decryptedContent == null) {
ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), id)
val content = ExternalSignerUtils.content[id] ?: ""
if (content.isBlank()) return
decryptedContent = content
}
val privateTags = blockList.privateTagsOrEmpty(decryptedContent).plus(element = listOf("word", word))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), id)
val eventContent = ExternalSignerUtils.content[id] ?: ""
if (eventContent.isBlank()) return
eventContent
}
var event = if (blockList != null) {
PeopleListEvent.addWord(
earlierVersion = blockList,
word = word,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent
)
} else {
PeopleListEvent.createListWithWord(
name = PeopleListEvent.blockList,
word = word,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent
)
}
ExternalSignerUtils.openSigner(event)
val eventContent = ExternalSignerUtils.content[event.id] ?: ""
if (eventContent.isBlank()) return
event = PeopleListEvent(
event.id,
event.pubKey,
event.createdAt,
event.tags,
event.content,
eventContent
)
Client.send(event)
LocalCache.consume(event)
} else {
val event = if (blockList != null) {
PeopleListEvent.addWord(
earlierVersion = blockList,
word = word,
isPrivate = true,
privateKey = keyPair.privKey!!
)
} else {
PeopleListEvent.createListWithWord(
name = PeopleListEvent.blockList,
word = word,
isPrivate = true,
privateKey = keyPair.privKey!!
)
}
Client.send(event)
LocalCache.consume(event)
}
live.invalidateData()
saveable.invalidateData()
}
fun showWord(word: String) {
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
if (blockList != null) {
if (loginWithExternalSigner) {
val content = blockList.content
val encryptedContent = if (content.isBlank()) {
val privateTags = listOf(listOf("word", word))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id)
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
if (eventContent.isBlank()) return
eventContent
} else {
var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id]
if (decryptedContent == null) {
ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), blockList.id)
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
if (eventContent.isBlank()) return
decryptedContent = eventContent
}
val privateTags = blockList.privateTagsOrEmpty(decryptedContent).minus(element = listOf("word", word))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id)
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
if (eventContent.isBlank()) return
eventContent
}
var event = PeopleListEvent.removeTag(
earlierVersion = blockList,
tag = word,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent
)
ExternalSignerUtils.openSigner(event)
val eventContent = ExternalSignerUtils.content[event.id] ?: ""
if (eventContent.isBlank()) return
event = PeopleListEvent.create(event, eventContent)
Client.send(event)
LocalCache.consume(event)
} else {
val event = PeopleListEvent.removeWord(
earlierVersion = blockList,
word = word,
isPrivate = true,
privateKey = keyPair.privKey!!
)
Client.send(event)
LocalCache.consume(event)
}
}
transientHiddenUsers = (transientHiddenUsers - word).toImmutableSet()
live.invalidateData()
saveable.invalidateData()
}
fun hideUser(pubkeyHex: String) {
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
if (loginWithExternalSigner) {
@@ -2275,7 +2447,7 @@ class Account(
val privateTags = listOf(listOf("p", pubkeyHex))
val msg = Event.mapper.writeValueAsString(privateTags)
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypt")
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypted")
val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: ""
ExternalSignerUtils.content.remove("encrypted")
if (encryptedContent.isBlank()) return
@@ -2384,9 +2556,9 @@ class Account(
eventContent
}
var event = PeopleListEvent.addUser(
var event = PeopleListEvent.removeTag(
earlierVersion = blockList,
pubKeyHex = pubkeyHex,
tag = pubkeyHex,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent
@@ -2782,7 +2954,10 @@ class Account(
// Takes a User's relay list and adds the types of feeds they are active for.
fun activeRelays(): Array<Relay>? {
var usersRelayList = userProfile().latestContactList?.relays()?.map {
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet()
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes
?: Constants.defaultRelays.filter { defaultRelay -> defaultRelay.url == it.key }.firstOrNull()?.feedTypes
?: FeedType.values().toSet()
Relay(it.key, it.value.read, it.value.write, localFeedTypes, proxy)
} ?: return null
@@ -2937,12 +3112,14 @@ class Account(
live.invalidateData()
}
fun markAsRead(route: String, timestampInSecs: Long) {
fun markAsRead(route: String, timestampInSecs: Long): Boolean {
val lastTime = lastReadPerRoute[route]
if (lastTime == null || timestampInSecs > lastTime) {
return if (lastTime == null || timestampInSecs > lastTime) {
lastReadPerRoute = lastReadPerRoute + Pair(route, timestampInSecs)
saveable.invalidateData()
liveLastRead.invalidateData()
true
} else {
false
}
}
@@ -22,6 +22,7 @@ import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import java.io.File
@@ -348,7 +349,12 @@ object LocalCache {
val channel = getOrCreateChannel(note.idHex) {
LiveActivitiesChannel(note.address)
} as? LiveActivitiesChannel
channel?.updateChannelInfo(author, event, event.createdAt)
val creator = event.host()?.ifBlank { null }?.let {
checkGetOrCreateUser(it)
} ?: author
channel?.updateChannelInfo(creator, event, event.createdAt)
refreshObservers(note)
}
@@ -1548,7 +1554,7 @@ object LocalCache {
@Stable
class LocalCacheLiveData {
private val _newEventBundles = MutableSharedFlow<Set<Note>>()
private val _newEventBundles = MutableSharedFlow<Set<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
// Refreshes observers in batches.
@@ -673,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
}
}
}
@@ -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) }
}
@@ -1,8 +1,10 @@
package com.vitorpamplona.amethyst.service
import android.content.Context
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.quartz.events.Event
@@ -45,7 +47,7 @@ class CashuProcessor {
}
}
suspend fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
suspend fun melt(token: CashuToken, lud16: String, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context) {
checkNotInMainThread()
runCatching {
@@ -54,16 +56,17 @@ class CashuProcessor {
milliSats = token.redeemInvoiceAmount * 1000, // Make invoice and leave room for fees
message = "Redeem Cashu",
onSuccess = { invoice ->
meltInvoice(token, invoice, onSuccess, onError)
meltInvoice(token, invoice, onSuccess, onError, context)
},
onProgress = {
},
onError = onError
onError = onError,
context = context
)
}
}
private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context) {
try {
val client = HttpClient.getHttpClient()
val url = token.mint + "/melt" // Melt cashu tokens at Mint
@@ -88,13 +91,24 @@ class CashuProcessor {
val successful = tree?.get("paid")?.asText() == "true"
if (successful) {
onSuccess("Redeemed ${token.totalAmount} Sats" + " (Fees: ${token.fees} Sats)")
onSuccess(
context.getString(R.string.cashu_sucessful_redemption),
context.getString(R.string.cashu_sucessful_redemption_explainer, token.totalAmount.toString(), token.fees.toString())
)
} else {
onError(tree?.get("detail")?.asText()?.split('.')?.getOrNull(0) ?: "Cashu: Tokens already spent.")
val msg = tree?.get("detail")?.asText()?.split('.')?.getOrNull(0)?.ifBlank { null }
onError(
context.getString(R.string.cashu_failed_redemption),
if (msg != null) {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, msg)
} else {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg)
}
)
}
}
} catch (e: Exception) {
onError("Token melt failure: " + e.message)
onError(context.getString(R.string.cashu_sucessful_redemption), context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message))
}
}
}
@@ -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()
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
@@ -34,6 +35,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
fun createAccountContactListFilter(): TypedFilter {
return TypedFilter(
@@ -141,10 +143,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
)
val accountChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
if (hasLoadedTheBasics[account.userProfile()] != null) {
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
} else {
hasLoadedTheBasics[account.userProfile()] = true
}
}
override fun consume(event: Event, relay: Relay) {
checkNotInMainThread()
if (LocalCache.justVerify(event)) {
if (event is GiftWrapEvent) {
val privateKey = account.keyPair.privKey
@@ -199,18 +207,28 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
}
override fun updateChannelFilters() {
// gets everthing about the user logged in
accountChannel.typedFilters = listOf(
createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createNotificationFilter(),
createGiftWrapsToMeFilter(),
createAccountReportsFilter(),
createAccountAcceptedAwardsFilter(),
createAccountBookmarkListFilter(),
createAccountLastPostsListFilter()
).ifEmpty { null }
return if (hasLoadedTheBasics[account.userProfile()] != null) {
// gets everthing about the user logged in
accountChannel.typedFilters = listOf(
createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createNotificationFilter(),
createGiftWrapsToMeFilter(),
createAccountReportsFilter(),
createAccountAcceptedAwardsFilter(),
createAccountBookmarkListFilter(),
createAccountLastPostsListFilter()
).ifEmpty { null }
} else {
// just the basics.
accountChannel.typedFilters = listOf(
createAccountMetadataFilter(),
createAccountContactListFilter(),
createAccountRelayListFilter(),
createAccountBookmarkListFilter()
).ifEmpty { null }
}
}
override fun auth(relay: Relay, challenge: String) {
@@ -56,6 +56,10 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
}
}
fun clearEOSEs(account: Account) {
latestEOSEs.removeDataFor(account.userProfile())
}
val inandoutChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), withRoom.hashCode().toString(), relayUrl, time)
}
@@ -35,7 +35,7 @@ class ZapPaymentHandler(val account: Account) {
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
zapType: LnZapEvent.ZapType
@@ -48,7 +48,10 @@ class ZapPaymentHandler(val account: Account) {
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))
onError(
context.getString(R.string.missing_lud16),
context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats)
)
return@withContext
}
@@ -121,6 +124,9 @@ class ZapPaymentHandler(val account: Account) {
)
} else {
onError(
context.getString(
R.string.missing_lud16
),
context.getString(
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
@@ -149,7 +155,7 @@ class ZapPaymentHandler(val account: Account) {
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayInvoiceThroughIntent: (String) -> Unit,
zapType: LnZapEvent.ZapType,
@@ -181,9 +187,13 @@ class ZapPaymentHandler(val account: Account) {
if (response is PayInvoiceErrorResponse) {
onProgress(0.0f)
onError(
response.error?.message
?: response.error?.code?.toString()
?: "Error parsing error message"
context.getString(R.string.error_dialog_pay_invoice_error),
context.getString(
R.string.wallet_connect_pay_invoice_error_error,
response.error?.message
?: response.error?.code?.toString()
?: "Error parsing error message"
)
)
} else {
onProgress(1f)
@@ -192,16 +202,13 @@ class ZapPaymentHandler(val account: Account) {
)
onProgress(0.8f)
} else {
try {
onPayInvoiceThroughIntent(it)
} catch (e: Exception) {
onError(context.getString(R.string.lightning_wallets_not_found2))
}
onPayInvoiceThroughIntent(it)
onProgress(0f)
}
},
onError = onError,
onProgress = onProgress
onProgress = onProgress,
context = context
)
}
}
@@ -1,7 +1,9 @@
package com.vitorpamplona.amethyst.service.lnurl
import android.content.Context
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
@@ -31,13 +33,24 @@ class LightningAddressResolver() {
return null
}
private suspend fun fetchLightningAddressJson(lnaddress: String, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
private suspend fun fetchLightningAddressJson(
lnaddress: String,
onSuccess: suspend (String) -> Unit,
onError: (String, String) -> Unit,
context: Context
) = withContext(Dispatchers.IO) {
checkNotInMainThread()
val url = assembleUrl(lnaddress)
if (url == null) {
onError("Could not assemble LNUrl from Lightning Address \"${lnaddress}\". Check the user's setup")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup,
lnaddress
)
)
return@withContext
}
@@ -51,16 +64,39 @@ class LightningAddressResolver() {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server is up and if the lightning address is correct")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct,
url,
lnaddress,
it.code.toString()
)
)
}
}
} catch (e: Exception) {
e.printStackTrace()
onError("Could not resolve $url. Check if the server is up and if the lightning address $lnaddress is correct")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct,
url,
lnaddress
)
)
}
}
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (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, String) -> Unit,
context: Context
) = withContext(Dispatchers.IO) {
val encodedMessage = URLEncoder.encode(message, "utf-8")
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
@@ -80,18 +116,22 @@ class LightningAddressResolver() {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("Could not fetch invoice from $lnCallback")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.could_not_fetch_invoice_from, lnCallback)
)
}
}
}
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) {
fetchLightningAddressJson(
lnaddress,
onSuccess = {
onSuccess(it.toByteArray().toLnUrl())
},
onError = onError
onError = onError,
context = context
)
}
@@ -101,8 +141,9 @@ class LightningAddressResolver() {
message: String,
nostrRequest: String? = null,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
context: Context
) {
val mapper = jacksonObjectMapper()
@@ -114,14 +155,20 @@ class LightningAddressResolver() {
val lnurlp = try {
mapper.readTree(lnAddressJson)
} catch (t: Throwable) {
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup)
)
null
}
val callback = lnurlp?.get("callback")?.asText()
if (callback == null) {
onError("Callback URL not found in the User's lightning address server configuration")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration)
)
}
val allowsNostr = lnurlp?.get("allowsNostr")?.asBoolean() ?: false
@@ -138,7 +185,10 @@ class LightningAddressResolver() {
val lnInvoice = try {
mapper.readTree(it)
} catch (t: Throwable) {
onError("Error Parsing JSON from Lightning Address's invoice fetch. Check the user's lightning setup")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup)
)
null
}
@@ -151,21 +201,40 @@ class LightningAddressResolver() {
onSuccess(pr)
} else {
onProgress(0.0f)
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from $lnaddress. It should have been $expectedAmountInSats")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.incorrect_invoice_amount_sats_from_it_should_have_been,
invoiceAmount.toLong().toString(),
lnaddress,
expectedAmountInSats.toString()
)
)
}
} ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason ->
onProgress(0.0f)
onError("Unable to create a lightning invoice before sending the zap. The receiver's lightning wallet sent the following error: $reason")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error,
reason
)
)
} ?: run {
onProgress(0.0f)
onError("nable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON.")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json)
)
}
},
onError = onError
onError = onError,
context
)
}
},
onError = onError
onError = onError,
context
)
}
}
@@ -43,6 +43,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
private suspend fun consumeIfMatchesAccount(pushWrappedEvent: GiftWrapEvent, account: Account) {
val key = account.keyPair.privKey
if (account.loginWithExternalSigner) {
ExternalSignerUtils.account = account
var cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id]
if (cached == null) {
ExternalSignerUtils.decrypt(
@@ -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 && acc.isWriteable()) {
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)
@@ -66,5 +70,6 @@ class RegisterAccounts(
postRegistrationEvent(
signEventsToProveControlOfAccounts(accounts, notificationToken)
)
PushNotificationUtils.hasInit = true
}
}
@@ -43,4 +43,8 @@ class EOSEAccount(var users: Map<User, EOSEFollowList> = emptyMap()) {
followList.addOrUpdate(listCode, relayUrl, time)
}
}
fun removeDataFor(user: User) {
users = users.minus(user)
}
}
@@ -16,8 +16,8 @@ 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
@@ -70,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)
@@ -0,0 +1,54 @@
package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@Composable
fun InformationDialog(
title: String,
textContent: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(title)
},
text = {
SelectionContainer {
Text(textContent)
}
},
confirmButton = {
Button(onClick = onDismiss, colors = buttonColors, contentPadding = PaddingValues(horizontal = Size16dp)) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Done,
contentDescription = null
)
Spacer(StdHorzSpacer)
Text(stringResource(R.string.error_dialog_button_ok))
}
}
}
)
}
@@ -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(
@@ -14,6 +14,7 @@ import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@@ -22,11 +23,11 @@ open class NewMediaModel : ViewModel() {
var account: Account? = null
var isUploadingImage by mutableStateOf(false)
val imageUploadingError = MutableSharedFlow<String?>()
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
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 +80,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 {
@@ -103,7 +104,7 @@ open class NewMediaModel : ViewModel() {
createNIP94Record(
imageUrl,
mimeType,
description,
alt,
sensitiveContent,
relayList = relayList
)
@@ -139,7 +140,7 @@ open class NewMediaModel : ViewModel() {
uploadingDescription.value = null
uploadingPercentage.value = 0.0f
description = ""
alt = ""
selectedServer = account?.defaultFileServer
}
@@ -147,7 +148,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"
@@ -166,7 +167,7 @@ open class NewMediaModel : ViewModel() {
imageData,
imageUrl,
mimeType,
description,
alt,
sensitiveContent,
onReady = {
uploadingPercentage.value = 0.90f
@@ -200,7 +201,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"
@@ -209,7 +210,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
@@ -135,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
)
}
}
@@ -287,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(
@@ -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)
)
}
@@ -19,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
@@ -32,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
@@ -46,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
@@ -82,6 +81,7 @@ 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
@@ -119,6 +119,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewPostView(
onClose: () -> Unit,
@@ -187,9 +188,7 @@ fun NewPostView(
TopAppBar(
title = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(end = 10.dp),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -206,28 +205,37 @@ fun NewPostView(
painter = painterResource(R.drawable.relays),
contentDescription = null,
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
}
}
PostButton(
onPost = {
postViewModel.sendPost(relayList = relayList)
onClose()
scope.launch {
delay(100)
onClose()
}
},
isActive = postViewModel.canPost()
)
}
},
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 ->
@@ -248,7 +256,6 @@ fun NewPostView(
) {
Column(
modifier = Modifier
.padding(top = 10.dp)
.imePadding()
.weight(1f)
) {
@@ -269,7 +276,7 @@ fun NewPostView(
makeItShort = true,
unPackReply = false,
isQuotedNote = true,
modifier = MaterialTheme.colors.replyModifier,
modifier = MaterialTheme.colorScheme.replyModifier,
accountViewModel = accountViewModel,
nav = nav
)
@@ -338,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 = {
@@ -373,6 +380,9 @@ fun NewPostView(
},
onClose = {
postViewModel.wantsInvoice = false
},
onError = { title, message ->
accountViewModel.toast(title, message)
}
)
}
@@ -405,7 +415,7 @@ fun NewPostView(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
)
)
@@ -415,7 +425,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)
}
@@ -462,7 +472,7 @@ fun NewPostView(
) {
UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
) {
postViewModel.selectImage(it)
@@ -526,10 +536,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(
@@ -541,7 +551,7 @@ private fun PollField(postViewModel: NewPostViewModel) {
}
}
@OptIn(ExperimentalComposeUiApi::class)
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun MessageField(
postViewModel: NewPostViewModel
@@ -568,7 +578,7 @@ private fun MessageField(
.fillMaxWidth()
.border(
width = 1.dp,
color = MaterialTheme.colors.surface,
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(8.dp)
)
.focusRequester(focusRequester)
@@ -580,15 +590,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)
)
}
@@ -639,7 +648,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)
)
}
@@ -669,18 +678,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
)
)
}
@@ -706,18 +713,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
)
)
}
@@ -771,7 +776,7 @@ fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewMo
Text(
text = stringResource(R.string.zap_split_explainer),
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(vertical = 10.dp)
)
@@ -816,12 +821,12 @@ fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewMo
placeholder = {
Text(
text = stringResource(R.string.zap_split_serarch_and_add_user_placeholder),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
singleLine = true,
visualTransformation = UrlUserTagTransformation(
MaterialTheme.colors.primary
MaterialTheme.colorScheme.primary
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
@@ -869,7 +874,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
imageVector = Icons.Default.LocationOn,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}
@@ -889,7 +894,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)
)
}
@@ -950,7 +955,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)
)
@@ -964,7 +969,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)
@@ -1023,7 +1028,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,
@@ -1031,7 +1036,7 @@ private fun AddZapraiserButton(
modifier = Modifier
.size(13.dp)
.align(Alignment.BottomEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1067,14 +1072,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
)
}
}
@@ -1095,7 +1100,7 @@ private fun AddLnInvoiceButton(
imageVector = Icons.Default.CurrencyBitcoin,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1130,7 +1135,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,
@@ -1138,7 +1143,7 @@ private fun ForwardZapTo(
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1184,7 +1189,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,
@@ -1192,7 +1197,7 @@ private fun MarkAsSensitive(
modifier = Modifier
.size(10.dp)
.align(Alignment.TopEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1218,15 +1223,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()
}
@@ -1236,17 +1235,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)
}
@@ -1255,19 +1248,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))
}
}
@@ -1281,10 +1266,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)
}
@@ -1332,7 +1316,7 @@ fun ImageVideoDescription(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
)
) {
@@ -1473,7 +1457,7 @@ fun ImageVideoDescription(
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -1492,7 +1476,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)
@@ -1526,21 +1510,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
)
}
@@ -33,6 +33,7 @@ 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.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.mapLatest
@@ -58,7 +59,7 @@ open class NewPostViewModel() : ViewModel() {
var message by mutableStateOf(TextFieldValue(""))
var urlPreview by mutableStateOf<String?>(null)
var isUploadingImage by mutableStateOf(false)
val imageUploadingError = MutableSharedFlow<String?>()
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
var userSuggestions by mutableStateOf<List<User>>(emptyList())
var userSuggestionAnchor: TextRange? = null
@@ -299,7 +300,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
@@ -314,7 +315,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(
@@ -325,7 +326,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)
@@ -537,13 +538,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)
@@ -568,13 +569,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)
@@ -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,
@@ -334,15 +350,10 @@ fun ServerConfig(
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
}
scope.launch {
Toast
.makeText(
context,
msg,
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg
)
}
)
}
@@ -351,6 +362,7 @@ fun ServerConfig(
@Composable
fun ServerConfigClickableLine(
loadProfilePicture: Boolean,
item: RelaySetupInfo,
onToggleDownload: (RelaySetupInfo) -> Unit,
onToggleUpload: (RelaySetupInfo) -> Unit,
@@ -368,7 +380,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 +453,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 +466,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -477,9 +489,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 +502,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -512,7 +524,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 +532,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -542,7 +554,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 +562,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
@@ -571,7 +583,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 +613,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 +646,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 +679,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 +712,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 +746,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 +779,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 +815,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 +829,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 +840,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 +858,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,13 +11,12 @@ 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.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -38,7 +37,6 @@ import kotlinx.coroutines.withContext
fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
val postViewModel: NewUserMetadataViewModel = viewModel()
val context = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
postViewModel.load(account)
@@ -88,43 +86,22 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
.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))
@@ -138,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(
@@ -157,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)
@@ -182,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)
@@ -207,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
@@ -223,7 +200,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "_@mywebsite.com",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
singleLine = true
@@ -238,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
@@ -254,7 +231,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.lnurl),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
)
@@ -269,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
)
}
)
@@ -284,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
)
}
)
@@ -299,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
)
}
)
@@ -15,6 +15,7 @@ import com.vitorpamplona.quartz.events.GitHubIdentity
import com.vitorpamplona.quartz.events.MastodonIdentity
import com.vitorpamplona.quartz.events.TwitterIdentity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
@@ -23,7 +24,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("")
@@ -41,13 +42,13 @@ class NewUserMetadataViewModel : ViewModel() {
var isUploadingImageForPicture by mutableStateOf(false)
var isUploadingImageForBanner by mutableStateOf(false)
val imageUploadingError = MutableSharedFlow<String?>()
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
fun load(account: Account) {
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 +83,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())
@@ -128,7 +129,7 @@ class NewUserMetadataViewModel : ViewModel() {
}
fun clear() {
userName.value = ""
// userName.value = ""
displayName.value = ""
about.value = ""
picture.value = ""
@@ -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))
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
@@ -12,19 +11,18 @@ 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.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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -34,7 +32,6 @@ import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
data class RelayList(
val relay: Relay,
@@ -55,7 +52,6 @@ fun RelaySelectionDialog(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
var relays by remember {
@@ -69,6 +65,13 @@ fun RelaySelectionDialog(
}
)
}
val hasSelectedRelay by remember {
derivedStateOf {
relays.any { it.isSelected }
}
}
var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) }
relayInfo?.let {
@@ -121,21 +124,15 @@ fun RelaySelectionDialog(
SaveButton(
onPost = {
val selectedRelays = relays.filter { it.isSelected }
if (selectedRelays.isEmpty()) {
scope.launch {
Toast.makeText(context, context.getString(R.string.select_a_relay_to_continue), Toast.LENGTH_SHORT).show()
}
return@SaveButton
}
onPost(selectedRelays.map { it.relay })
onClose()
},
isActive = true
isActive = hasSelectedRelay
)
}
RelaySwitch(
text = stringResource(R.string.select_deselect_all),
text = context.getString(R.string.select_deselect_all),
checked = selected,
onClick = {
selected = !selected
@@ -181,15 +178,10 @@ fun RelaySelectionDialog(
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
}
scope.launch {
Toast
.makeText(
context,
msg,
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg
)
}
)
}
@@ -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,6 +19,7 @@ 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) {
@@ -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) }
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
@@ -10,7 +9,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 +75,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
@@ -133,31 +132,25 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
CashuProcessor().melt(
token,
lud16,
onSuccess = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
onSuccess = { title, message ->
accountViewModel.toast(title, message)
},
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
}
onError = { title, message ->
accountViewModel.toast(title, message)
},
context
)
}
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.no_lightning_address_set),
Toast.LENGTH_SHORT
).show()
}
accountViewModel.toast(
context.getString(R.string.no_lightning_address_set),
context.getString(R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, accountViewModel.account.userProfile().toBestDisplayName())
)
}
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(
@@ -177,16 +170,12 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
startActivity(context, intent, null)
} else {
// Copying the token to clipboard for now
var orignaltoken = token.token
clipboardManager.setText(AnnotatedString("$orignaltoken"))
scope.launch {
Toast.makeText(context, context.getString(R.string.copied_token_to_clipboard), Toast.LENGTH_SHORT).show()
}
clipboardManager.setText(AnnotatedString(token.token))
}
},
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
@@ -307,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()
@@ -368,7 +367,7 @@ fun CreateTextWithEmoji(
val textColor = color.takeOrElse {
LocalTextStyle.current.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
LocalContentColor.current
}
}
@@ -424,7 +423,7 @@ fun CreateTextWithEmoji(
val textColor = color.takeOrElse {
LocalTextStyle.current.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
LocalContentColor.current
}
}
@@ -537,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
)
}
@@ -677,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)
)
}
@@ -1,19 +1,17 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
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
import androidx.compose.ui.text.style.TextDirection
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.quartz.encoders.LnWithdrawalUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -43,29 +41,28 @@ fun MayBeWithdrawal(lnurlWord: String) {
@Composable
fun ClickableWithdrawal(withdrawalString: String) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val withdraw = remember(withdrawalString) {
AnnotatedString("$withdrawalString ")
}
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = context.getString(R.string.error_dialog_pay_withdraw_error),
textContent = showErrorMessageDialog ?: "",
onDismiss = { showErrorMessageDialog = null }
)
}
ClickableText(
text = withdraw,
onClick = {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$withdrawalString"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
} catch (e: Exception) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.lightning_wallets_not_found),
Toast.LENGTH_LONG
).show()
}
payViaIntent(withdrawalString, context) {
showErrorMessageDialog = it
}
},
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)
) {
@@ -1,8 +1,5 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
@@ -10,7 +7,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
@@ -23,8 +20,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
@@ -67,14 +65,23 @@ fun MayBeInvoicePreview(lnbcWord: String) {
@Composable
fun InvoicePreview(lnInvoice: String, amount: String?) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = context.getString(R.string.error_dialog_pay_invoice_error),
textContent = showErrorMessageDialog ?: "",
onDismiss = { showErrorMessageDialog = null }
)
}
Column(
modifier = Modifier
.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
@@ -120,23 +127,13 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
.fillMaxWidth()
.padding(vertical = 10.dp),
onClick = {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$lnInvoice"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(context, intent, null)
} catch (e: Exception) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.lightning_wallets_not_found),
Toast.LENGTH_LONG
).show()
}
payViaIntent(lnInvoice, context) {
showErrorMessageDialog = it
}
},
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)
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.components
import android.widget.Toast
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -8,13 +7,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
@@ -50,21 +49,22 @@ fun InvoiceRequestCard(
titleText: String? = null,
buttonText: String? = null,
onSuccess: (String) -> Unit,
onClose: () -> Unit
onClose: () -> Unit,
onError: (String, String) -> Unit
) {
Column(
modifier = Modifier
.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
.fillMaxWidth()
.padding(30.dp)
) {
InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose)
InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose, onError)
}
}
}
@@ -77,7 +77,8 @@ fun InvoiceRequest(
titleText: String? = null,
buttonText: String? = null,
onSuccess: (String) -> Unit,
onClose: () -> Unit
onClose: () -> Unit,
onError: (String, String) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -116,7 +117,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 +142,7 @@ fun InvoiceRequest(
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -162,20 +163,16 @@ fun InvoiceRequest(
message,
zapRequest?.toJson(),
onSuccess = onSuccess,
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
onClose()
}
},
onError = onError,
onProgress = {
}
},
context = context
)
}
},
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
}
)
@@ -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
}
}
}
@@ -657,7 +487,7 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Co
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]")
@@ -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
@@ -1,7 +1,13 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
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
@@ -11,9 +17,19 @@ import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.decode.DataSource
import coil.fetch.DrawableResult
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.request.ImageRequest
import coil.request.Options
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import java.util.Base64
@Composable
fun RobohashAsyncImage(
@@ -58,27 +74,49 @@ 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) {
AsyncImage(
model = model,
contentDescription = contentDescription,
modifier = modifier,
placeholder = painter,
fallback = painter,
error = painter,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
if (model != null && loadProfilePicture) {
val isBase64 by remember {
derivedStateOf {
model.startsWith("data:image/jpeg;base64,")
}
}
if (isBase64) {
val painter = rememberAsyncImagePainter(
model = Base64Requester.imageRequest(context, model)
)
Image(
painter = painter,
contentDescription = null,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
colorFilter = colorFilter
)
} else {
AsyncImage(
model = model,
contentDescription = contentDescription,
modifier = modifier,
placeholder = painter,
fallback = painter,
error = painter,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
}
} else {
Image(
painter = painter,
@@ -101,7 +139,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 +151,49 @@ fun RobohashAsyncImageProxy(
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
filterQuality = filterQuality,
loadProfilePicture = loadProfilePicture
)
}
object Base64Requester {
fun imageRequest(context: Context, message: String): ImageRequest {
return ImageRequest
.Builder(context)
.data(message)
.fetcherFactory(Base64Fetcher.Factory)
.build()
}
}
@Stable
class Base64Fetcher(
private val options: Options,
private val data: Uri
) : Fetcher {
override suspend fun fetch(): FetchResult {
checkNotInMainThread()
val base64String = data.toString().removePrefix("data:image/jpeg;base64,")
val byteArray = Base64.getDecoder().decode(base64String)
val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
if (bitmap == null) {
throw Exception("Unable to load base64 $base64String")
}
return DrawableResult(
drawable = bitmap.toDrawable(options.context.resources),
isSampled = false,
dataSource = DataSource.MEMORY
)
}
object Factory : Fetcher.Factory<Uri> {
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher {
return Base64Fetcher(options, data)
}
}
}
@@ -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(
@@ -14,11 +14,11 @@ 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
@@ -98,7 +98,7 @@ fun SpinnerSelectionDialog(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = item.title, color = MaterialTheme.colors.onSurface)
Text(text = item.title, color = MaterialTheme.colorScheme.onSurface)
}
item.explainer?.let {
Spacer(modifier = Modifier.height(5.dp))
@@ -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 = 5.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,9 +1,9 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
@@ -21,8 +21,8 @@ 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
@@ -58,8 +58,6 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import coil.imageLoader
import coil.request.ImageRequest
import com.linc.audiowaveform.infiniteLinearGradient
import com.vitorpamplona.amethyst.PlaybackClientController
import com.vitorpamplona.amethyst.model.ConnectivityType
@@ -97,24 +95,23 @@ fun LoadThumbAndThenVideoView(
onDialog: ((Boolean) -> Unit)? = null
) {
var loadingFinished by remember { mutableStateOf<Pair<Boolean, Drawable?>>(Pair(false, null)) }
val context = LocalContext.current
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
try {
val request = ImageRequest.Builder(context).data(thumbUri).build()
val myCover = context.imageLoader.execute(request).drawable
if (myCover != null) {
loadingFinished = Pair(true, myCover)
accountViewModel.loadThumb(
context,
thumbUri,
onReady = {
if (it != null) {
loadingFinished = Pair(true, it)
} else {
loadingFinished = Pair(true, null)
}
} catch (e: Exception) {
Log.e("VideoView", "Fail to load cover $thumbUri", e)
},
onError = {
loadingFinished = Pair(true, null)
}
}
)
}
if (loadingFinished.first) {
@@ -157,24 +154,26 @@ fun VideoView(
authorName: String? = null,
nostrUriCallback: String? = null,
onDialog: ((Boolean) -> Unit)? = null,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
accountViewModel: AccountViewModel,
alwaysShowVideo: Boolean = false
) {
val defaultToStart by remember(videoUri) { mutableStateOf(DefaultMutedSetting.value) }
VideoViewInner(
videoUri,
defaultToStart,
title,
thumb,
roundedCorner,
waveform,
artworkUri,
authorName,
nostrUriCallback,
alwaysShowVideo,
accountViewModel,
onDialog
videoUri = videoUri,
defaultToStart = defaultToStart,
title = title,
thumb = thumb,
roundedCorner = roundedCorner,
waveform = waveform,
artworkUri = artworkUri,
authorName = authorName,
nostrUriCallback = nostrUriCallback,
alwaysShowVideo = alwaysShowVideo,
accountViewModel = accountViewModel,
onControllerVisibilityChanged = onControllerVisibilityChanged,
onDialog = onDialog
)
}
@@ -192,6 +191,7 @@ fun VideoViewInner(
nostrUriCallback: String? = null,
alwaysShowVideo: Boolean = false,
accountViewModel: AccountViewModel,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
onDialog: ((Boolean) -> Unit)? = null
) {
val automaticallyStartPlayback = remember {
@@ -211,27 +211,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(
@@ -248,6 +250,7 @@ fun VideoViewInner(
keepPlaying = keepPlaying,
automaticallyStartPlayback = automaticallyStartPlayback,
activeOnScreen = activeOnScreen,
onControllerVisibilityChanged = onControllerVisibilityChanged,
onDialog = onDialog
)
}
@@ -258,7 +261,7 @@ fun VideoViewInner(
@Composable
@OptIn(UnstableApi::class)
fun GetVideoController(
mediaItem: MediaItem,
mediaItem: MutableState<MediaItem>,
videoUri: String,
defaultToStart: Boolean = false,
nostrUriCallback: String? = null,
@@ -282,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
@@ -309,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
@@ -327,7 +334,7 @@ fun GetVideoController(
it.volume = if (defaultToStart) 0f else 1f
}
it.setMediaItem(mediaItem)
it.setMediaItem(mediaItem.value)
it.prepare()
}
}
@@ -346,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
@@ -384,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
@@ -408,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
}
}
}
@@ -457,7 +467,7 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Bo
}
}
Box(
val myModifier = remember(videoUri) {
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 70.dp)
@@ -486,7 +496,9 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Bo
}
}
}
) {
}
Box(modifier = myModifier) {
inner(active)
}
}
@@ -506,10 +518,9 @@ private fun RenderVideoPlayer(
keepPlaying: MutableState<Boolean>,
automaticallyStartPlayback: MutableState<Boolean>,
activeOnScreen: MutableState<Boolean>,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
onDialog: ((Boolean) -> Unit)?
) {
val context = LocalContext.current
ControlWhenPlayerIsActive(controller, keepPlaying, automaticallyStartPlayback, activeOnScreen)
val controllerVisible = remember(controller) {
@@ -517,17 +528,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(
@@ -546,16 +563,24 @@ private fun RenderVideoPlayer(
}
}
setControllerVisibilityListener(
PlayerView.ControllerVisibilityListener {
controllerVisible.value = it == View.VISIBLE
PlayerView.ControllerVisibilityListener { visible ->
controllerVisible.value = visible == View.VISIBLE
onControllerVisibilityChanged?.let { callback ->
callback(visible == View.VISIBLE)
}
}
)
}
}
}
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) {
@@ -576,7 +601,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) {
@@ -665,36 +690,38 @@ fun ControlWhenPlayerIsActive(
automaticallyStartPlayback: MutableState<Boolean>,
activeOnScreen: MutableState<Boolean>
) {
// active means being fully visible
if (activeOnScreen.value) {
// should auto start video from settings?
if (!automaticallyStartPlayback.value) {
if (controller.isPlaying) {
// if it is visible, it's playing but it wasn't supposed to start automatically.
LaunchedEffect(key1 = activeOnScreen.value) {
// active means being fully visible
if (activeOnScreen.value) {
// should auto start video from settings?
if (!automaticallyStartPlayback.value) {
if (controller.isPlaying) {
// if it is visible, it's playing but it wasn't supposed to start automatically.
controller.pause()
}
} else if (!controller.isPlaying) {
// if it is visible, was supposed to start automatically, but it's not
// If something else is playing, play on mute.
if (keepPlayingMutex != null && keepPlayingMutex != controller) {
controller.volume = 0f
}
controller.play()
}
} else {
// Pauses the video when it becomes invisible.
// Destroys the video later when it Disposes the element
// meanwhile if the user comes back, the position in the track is saved.
if (!keepPlaying.value) {
controller.pause()
}
} else if (!controller.isPlaying) {
// if it is visible, was supposed to start automatically, but it's not
// If something else is playing, play on mute.
if (keepPlayingMutex != null && keepPlayingMutex != controller) {
controller.volume = 0f
}
controller.play()
}
} else {
// Pauses the video when it becomes invisible.
// Destroys the video later when it Disposes the element
// meanwhile if the user comes back, the position in the track is saved.
if (!keepPlaying.value) {
controller.pause()
}
}
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
@@ -776,7 +803,7 @@ private fun MuteButton(
.clip(CircleShape)
.fillMaxSize(0.6f)
.align(Alignment.Center)
.background(MaterialTheme.colors.background)
.background(MaterialTheme.colorScheme.background)
)
IconButton(
@@ -817,7 +844,7 @@ private fun KeepPlayingButton(
.clip(CircleShape)
.fillMaxSize(0.6f)
.align(Alignment.Center)
.background(MaterialTheme.colors.background)
.background(MaterialTheme.colorScheme.background)
)
IconButton(
@@ -828,9 +855,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(
@@ -6,7 +6,9 @@ import android.content.ContextWrapper
import android.os.Build
import android.util.Log
import android.view.Window
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
@@ -16,6 +18,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 +32,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
@@ -49,11 +51,9 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
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.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
@@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.service.BlurHashRequester
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
import com.vitorpamplona.amethyst.ui.note.BlankNote
@@ -86,15 +87,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 +212,7 @@ fun ZoomableContentView(
}
var mainImageModifier = if (roundedCorner) {
MaterialTheme.colors.imageModifier
MaterialTheme.colorScheme.imageModifier
} else {
Modifier.fillMaxWidth()
}
@@ -553,7 +553,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]")
@@ -614,7 +614,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()
@@ -637,49 +637,89 @@ 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 }
DialogContent(allImages, imageUrl, onDismiss, accountViewModel)
}
}
}
}
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
val page = allImages.indexOf(imageUrl)
if (page > -1) {
pagerState.scrollToPage(page)
}
}
if (allImages.size > 1) {
SlidingCarousel(
pagerState = pagerState
) { index ->
RenderImageOrVideo(allImages[index], false, accountViewModel)
}
} else {
RenderImageOrVideo(imageUrl, false, accountViewModel)
}
Row(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onPress = onDismiss)
val myContent = allImages[pagerState.currentPage]
if (myContent is ZoomableUrlContent) {
Row() {
CopyToClipboard(content = myContent)
SaveToGallery(url = myContent.url)
}
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
SaveToGallery(
localFile = myContent.localFile,
mimeType = myContent.mimeType
)
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun DialogContent(
allImages: ImmutableList<ZoomableContent>,
imageUrl: ZoomableContent,
onDismiss: () -> Unit,
accountViewModel: AccountViewModel
) {
val pagerState: PagerState = rememberPagerState() { allImages.size }
val controllerVisible = remember { mutableStateOf(false) }
val holdOn = remember { mutableStateOf<Boolean>(true) }
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
launch {
val page = allImages.indexOf(imageUrl)
if (page > -1) {
pagerState.scrollToPage(page)
}
}
launch(Dispatchers.Default) {
delay(2000)
holdOn.value = false
}
}
if (allImages.size > 1) {
SlidingCarousel(
pagerState = pagerState
) { index ->
RenderImageOrVideo(
content = allImages[index],
roundedCorner = false,
onControllerVisibilityChanged = {
controllerVisible.value = it
},
accountViewModel = accountViewModel
)
}
} else {
RenderImageOrVideo(
content = imageUrl,
roundedCorner = false,
onControllerVisibilityChanged = {
controllerVisible.value = it
},
accountViewModel = accountViewModel
)
}
AnimatedVisibility(
visible = holdOn.value || controllerVisible.value,
enter = fadeIn(),
exit = fadeOut()
) {
Row(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onPress = onDismiss)
val myContent = allImages[pagerState.currentPage]
if (myContent is ZoomableUrlContent) {
Row() {
CopyToClipboard(content = myContent)
Spacer(modifier = StdHorzSpacer)
SaveToGallery(url = myContent.url)
}
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
SaveToGallery(
localFile = myContent.localFile,
mimeType = myContent.mimeType
)
}
}
}
@@ -691,17 +731,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)
@@ -725,26 +759,46 @@ 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()
}
)
}
}
}
@Composable
private fun RenderImageOrVideo(content: ZoomableContent, roundedCorner: Boolean, accountViewModel: AccountViewModel) {
private fun RenderImageOrVideo(
content: ZoomableContent,
roundedCorner: Boolean,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
accountViewModel: AccountViewModel
) {
val mainModifier = Modifier
.fillMaxSize()
.zoomable(rememberZoomState())
@@ -759,6 +813,7 @@ private fun RenderImageOrVideo(content: ZoomableContent, roundedCorner: Boolean,
artworkUri = content.artworkUri,
authorName = content.authorName,
roundedCorner = roundedCorner,
onControllerVisibilityChanged = onControllerVisibilityChanged,
accountViewModel = accountViewModel,
alwaysShowVideo = true
)
@@ -774,6 +829,7 @@ private fun RenderImageOrVideo(content: ZoomableContent, roundedCorner: Boolean,
artworkUri = content.artworkUri,
authorName = content.authorName,
roundedCorner = roundedCorner,
onControllerVisibilityChanged = onControllerVisibilityChanged,
accountViewModel = accountViewModel,
alwaysShowVideo = true
)
@@ -800,7 +856,17 @@ private fun verifyHash(content: ZoomableUrlContent, context: Context): Boolean?
@Composable
private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
val localContext = LocalContext.current
val scope = rememberCoroutineScope()
val openDialogMsg = remember { mutableStateOf<String?>(null) }
openDialogMsg.value?.let {
InformationDialog(
title = localContext.getString(R.string.hash_verification_info_title),
textContent = it
) {
openDialogMsg.value = null
}
}
Box(
modifier
@@ -811,13 +877,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
if (verifiedHash) {
IconButton(
onClick = {
scope.launch {
Toast.makeText(
localContext,
localContext.getString(R.string.hash_verification_passed),
Toast.LENGTH_LONG
).show()
}
openDialogMsg.value = localContext.getString(R.string.hash_verification_passed)
}
) {
HashCheckIcon(Size30dp)
@@ -825,13 +885,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
} else {
IconButton(
onClick = {
scope.launch {
Toast.makeText(
localContext,
localContext.getString(R.string.hash_verification_failed),
Toast.LENGTH_LONG
).show()
}
openDialogMsg.value = localContext.getString(R.string.hash_verification_failed)
}
) {
HashCheckFailedIcon(Size30dp)
@@ -32,6 +32,32 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
}
}
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
)
}
}
@@ -6,28 +6,23 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.height
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
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
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
import androidx.compose.ui.draw.clip
@@ -39,13 +34,14 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size0dp
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import kotlinx.collections.immutable.persistentListOf
val bottomNavigationItems = listOf(
val bottomNavigationItems = persistentListOf(
Route.Home,
Route.Message,
Route.Video,
@@ -61,6 +57,7 @@ enum class Keyboard {
fun keyboardAsState(): State<Keyboard> {
val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
@@ -104,13 +101,9 @@ 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 = Size0dp) {
bottomNavigationItems.forEach { item ->
HasNewItemsIcon(item, accountViewModel, navEntryState, nav)
}
@@ -125,16 +118,12 @@ private fun RowScope.HasNewItemsIcon(
navEntryState: State<NavBackStackEntry?>,
nav: (Route, Boolean) -> Unit
) {
var hasNewItems by remember { mutableStateOf(false) }
WatchPossibleNotificationChanges(route, accountViewModel) {
if (it != hasNewItems) {
hasNewItems = it
val selected by remember(navEntryState.value) {
derivedStateOf {
navEntryState.value?.destination?.route?.substringBefore("?") == route.base
}
}
val scope = rememberCoroutineScope()
val size = remember {
if ("Home" == route.base) 25.dp else 23.dp
}
@@ -142,78 +131,12 @@ private fun RowScope.HasNewItemsIcon(
if ("Home" == route.base) 24.dp else 20.dp
}
BottomIcon(
icon = route.icon,
size = size,
iconSize = iconSize,
base = route.base,
hasNewItems = hasNewItems,
navEntryState = navEntryState
) { selected ->
scope.launch {
nav(route, selected)
}
}
}
@Composable
fun WatchPossibleNotificationChanges(
route: Route,
accountViewModel: AccountViewModel,
onChange: (Boolean) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val notifState by accountViewModel.accountLastReadLiveData.observeAsState()
LaunchedEffect(key1 = notifState, key2 = accountState) {
launch(Dispatchers.IO) {
onChange(route.hasNewItems(accountViewModel.account, emptySet()))
}
}
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect {
launch(Dispatchers.IO) {
onChange(route.hasNewItems(accountViewModel.account, it))
}
}
}
}
}
@Composable
private fun RowScope.BottomIcon(
icon: Int,
size: Dp,
iconSize: Dp,
base: String,
hasNewItems: Boolean,
navEntryState: State<NavBackStackEntry?>,
onClick: (Boolean) -> Unit
) {
val selected by remember(navEntryState.value) {
derivedStateOf {
navEntryState.value?.destination?.route?.substringBefore("?") == base
}
}
NavigationIcon(icon, size, iconSize, selected, hasNewItems, onClick)
}
@Composable
private fun RowScope.NavigationIcon(
icon: Int,
size: Dp,
iconSize: Dp,
selected: Boolean,
hasNewItems: Boolean,
onClick: (Boolean) -> Unit
) {
BottomNavigationItem(
NavigationBarItem(
icon = {
val hasNewItems = accountViewModel.notificationDots.hasNewItems[route]?.collectAsState()
NotifiableIcon(
icon,
route.icon,
size,
iconSize,
selected,
@@ -221,51 +144,56 @@ private fun RowScope.NavigationIcon(
)
},
selected = selected,
onClick = { onClick(selected) }
onClick = { nav(route, selected) }
)
}
@Composable
private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, hasNewItems: Boolean) {
private fun NotifiableIcon(
icon: Int,
size: Dp,
iconSize: Dp,
selected: Boolean,
hasNewItems: State<Boolean>?
) {
Box(remember { Modifier.size(size) }) {
Icon(
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) {
Box(
remember {
Modifier
.width(10.dp)
.height(10.dp)
.align(Alignment.TopEnd)
}
) {
Box(
modifier = remember {
Modifier
.width(10.dp)
.height(10.dp)
.clip(shape = CircleShape)
}.background(MaterialTheme.colors.primary),
contentAlignment = Alignment.TopEnd
) {
Text(
"",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 12.sp,
modifier = remember {
Modifier
.wrapContentHeight()
.align(Alignment.TopEnd)
}
)
}
}
if (hasNewItems?.value == true) {
NotificationDotIcon(
Modifier.align(Alignment.TopEnd)
)
}
}
}
@Composable
private fun NotificationDotIcon(modifier: Modifier) {
Box(modifier.size(Size10dp)) {
Box(
modifier = remember {
Modifier
.size(Size10dp)
.clip(shape = CircleShape)
}.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.TopEnd
) {
Text(
"",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 12.sp,
modifier = remember {
Modifier
.wrapContentHeight()
.align(Alignment.TopEnd)
}
)
}
}
}
@@ -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,18 @@ 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
@@ -51,7 +47,6 @@ 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
@@ -62,9 +57,9 @@ 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
@@ -83,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
@@ -110,9 +106,9 @@ 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.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
@@ -136,7 +132,7 @@ import kotlinx.coroutines.launch
fun AppTopBar(
followLists: FollowListViewModel,
navEntryState: State<NavBackStackEntry?>,
scaffoldState: ScaffoldState,
drawerState: DrawerState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
navPopBack: () -> Unit
@@ -153,7 +149,7 @@ fun AppTopBar(
}
}
RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav, navPopBack)
RenderTopRouteBar(currentRoute, id, followLists, drawerState, accountViewModel, nav, navPopBack)
}
@Composable
@@ -161,16 +157,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) {
@@ -181,10 +177,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)
}
}
}
@@ -355,11 +351,6 @@ private fun ChannelTopBar(
) {
LoadChannel(baseChannelHex = id, accountViewModel) { baseChannel ->
FlexibleTopBarWithBackButton(
prefixRow = {
if (baseChannel is LiveActivitiesChannel) {
ShowVideoStreaming(baseChannel, accountViewModel)
}
},
title = {
ShortChannelHeader(
baseChannel = baseChannel,
@@ -382,15 +373,13 @@ 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(
FollowListWithRoutes(
followListsModel = followLists,
listName = list,
withGlobal = true,
withRoutes = false
listName = list
) { listName ->
accountViewModel.account.changeDefaultStoriesFollowList(listName.code)
}
@@ -398,15 +387,13 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState
}
@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(
FollowListWithRoutes(
followListsModel = followLists,
listName = list,
withGlobal = true,
withRoutes = true
listName = list
) { listName ->
if (listName.type == CodeNameType.ROUTE) {
nav(listName.code)
@@ -418,15 +405,13 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a
}
@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(
FollowListWithoutRoutes(
followListsModel = followLists,
listName = list,
withGlobal = true,
withRoutes = false
listName = list
) { listName ->
accountViewModel.account.changeDefaultNotificationFollowList(listName.code)
}
@@ -434,15 +419,13 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold
}
@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(
FollowListWithoutRoutes(
followListsModel = followLists,
listName = list,
withGlobal = true,
withRoutes = false
listName = list
) { listName ->
accountViewModel.account.changeDefaultDiscoveryFollowList(listName.code)
}
@@ -450,24 +433,25 @@ fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldSta
}
@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(),
@@ -490,7 +474,7 @@ fun GenericMainTopBar(
val coroutineScope = rememberCoroutineScope()
LoggedInUserPictureDrawer(accountViewModel) {
coroutineScope.launch {
scaffoldState.drawerState.open()
drawerState.open()
}
}
},
@@ -500,7 +484,7 @@ fun GenericMainTopBar(
}
}
)
Divider(thickness = 0.25.dp)
Divider(thickness = DividerThickness)
}
}
@@ -519,9 +503,16 @@ private fun LoggedInUserPictureDrawer(
onClick: () -> Unit
) {
val profilePicture by accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState()
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
) {
@@ -530,48 +521,42 @@ 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(
fun FollowListWithRoutes(
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.peopleLists.collectAsState()
val routeList by followListsModel.routes.collectAsState()
val allLists = remember(followLists) {
if (withRoutes) {
(defaultOptions + followLists + routeList)
} else {
(defaultOptions + followLists)
}
}
val followNames by remember(followLists) {
derivedStateOf {
allLists.map { it.name }.toImmutableList()
}
}
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsState()
SimpleTextSpinner(
placeholder = allLists.firstOrNull { it.code == listName }?.name?.name() ?: "Select an Option",
options = followNames,
placeholderCode = listName,
options = allLists,
onSelect = {
onChange(allLists.getOrNull(it) ?: kind3Follow)
onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow)
}
)
}
@Composable
fun FollowListWithoutRoutes(
followListsModel: FollowListViewModel,
listName: String,
onChange: (CodeName) -> Unit
) {
val allLists by followListsModel.kind3GlobalPeople.collectAsState()
SimpleTextSpinner(
placeholderCode = listName,
options = allLists,
onSelect = {
onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow)
}
)
}
@@ -582,6 +567,7 @@ enum class CodeNameType {
abstract class Name {
abstract fun name(): String
open fun name(context: Context) = name()
}
class GeoHashName(val geoHashTag: String) : Name() {
@@ -590,12 +576,13 @@ class GeoHashName(val geoHashTag: String) : Name() {
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 ResourceName(val resourceId: Int) : Name() {
override fun name() = " $resourceId " // Space to make sure it goes first
override fun name(context: Context) = context.getString(resourceId)
}
class PeopleListName(val note: AddressableNote) : Name() {
override fun name() = note.dTag() ?: ""
override fun name() = (note.event as? PeopleListEvent)?.nameOrTitle() ?: note.dTag() ?: ""
}
class CommunityName(val note: AddressableNote) : Name() {
override fun name() = "/n/${(note.dTag() ?: "")}"
@@ -606,11 +593,14 @@ data class CodeName(val code: String, val name: Name, val type: CodeNameType)
@Stable
class FollowListViewModel(val account: Account) : ViewModel() {
private var _peopleLists = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
val peopleLists = _peopleLists.asStateFlow()
val kind3Follow = CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows), CodeNameType.HARDCODED)
val globalFollow = CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global), CodeNameType.HARDCODED)
private var _routes = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
val routes = _routes.asStateFlow()
private var _kind3GlobalPeopleRoutes = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.asStateFlow()
private var _kind3GlobalPeople = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
val kind3GlobalPeople = _kind3GlobalPeople.asStateFlow()
fun refresh() {
viewModelScope.launch(Dispatchers.Default) {
@@ -632,11 +622,7 @@ class FollowListViewModel(val account: Account) : ViewModel() {
} else {
null
}
}.sortedBy { it.name.name() }.toImmutableList()
if (!equalImmutableLists(_peopleLists.value, newFollowLists)) {
_peopleLists.emit(newFollowLists)
}
}.sortedBy { it.name.name() }
val communities = account.userProfile().cachedFollowingCommunitiesSet().mapNotNull {
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
@@ -652,10 +638,18 @@ class FollowListViewModel(val account: Account) : ViewModel() {
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
}
val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() }.toImmutableList()
val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() }
if (!equalImmutableLists(_routes.value, routeList)) {
_routes.emit(routeList)
val kind3GlobalPeopleRouteList = listOf(listOf(kind3Follow, globalFollow), newFollowLists, routeList).flatten().toImmutableList()
if (!equalImmutableLists(_kind3GlobalPeopleRoutes.value, kind3GlobalPeopleRouteList)) {
_kind3GlobalPeopleRoutes.emit(kind3GlobalPeopleRouteList)
}
val kind3GlobalPeopleList = listOf(listOf(kind3Follow, globalFollow), newFollowLists).flatten().toImmutableList()
if (!equalImmutableLists(_kind3GlobalPeople.value, kind3GlobalPeopleList)) {
_kind3GlobalPeople.emit(kind3GlobalPeopleList)
}
}
@@ -691,14 +685,24 @@ class FollowListViewModel(val account: Account) : ViewModel() {
@Composable
fun SimpleTextSpinner(
placeholder: String,
options: ImmutableList<Name>,
placeholderCode: String,
options: ImmutableList<CodeName>,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
var optionsShowing by remember { mutableStateOf(false) }
var currentText by remember(placeholder) { mutableStateOf(placeholder) }
val context = LocalContext.current
val selectAnOption = stringResource(
id = R.string.select_an_option
)
var currentText by remember {
mutableStateOf(
options.firstOrNull { it.code == placeholderCode }?.name?.name(context) ?: selectAnOption
)
}
Box(
modifier = modifier,
@@ -706,12 +710,12 @@ fun SimpleTextSpinner(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.size(20.dp))
Text(placeholder)
Text(currentText)
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
Box(
@@ -732,27 +736,27 @@ fun SimpleTextSpinner(
options = options,
onDismiss = { optionsShowing = false },
onSelect = {
currentText = options[it].name()
currentText = options[it].name.name(context)
optionsShowing = false
onSelect(it)
}
) {
RenderOption(it)
RenderOption(it.name)
}
}
}
}
@Composable
fun RenderOption(it: Name) {
when (it) {
fun RenderOption(option: Name) {
when (option) {
is GeoHashName -> {
LoadCityName(it.geoHashTag) {
LoadCityName(option.geoHashTag) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "/g/$it", color = MaterialTheme.colors.onSurface)
Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
}
}
}
@@ -761,7 +765,7 @@ fun RenderOption(it: Name) {
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = it.name(), color = MaterialTheme.colors.onSurface)
Text(text = option.name(), color = MaterialTheme.colorScheme.onSurface)
}
}
is ResourceName -> {
@@ -769,7 +773,7 @@ fun RenderOption(it: Name) {
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = it.resourceId), color = MaterialTheme.colors.onSurface)
Text(text = stringResource(id = option.resourceId), color = MaterialTheme.colorScheme.onSurface)
}
}
is PeopleListName -> {
@@ -777,7 +781,7 @@ fun RenderOption(it: Name) {
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(text = it.name(), color = MaterialTheme.colors.onSurface)
Text(text = option.name(), color = MaterialTheme.colorScheme.onSurface)
}
}
is CommunityName -> {
@@ -785,21 +789,21 @@ fun RenderOption(it: Name) {
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
val name by it.note.live().metadata.map {
val name by option.note.live().metadata.map {
"/n/" + (it.note as? AddressableNote)?.dTag()
}.observeAsState()
Text(text = name ?: "", color = MaterialTheme.colors.onSurface)
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(
@@ -817,15 +821,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 = {
@@ -903,135 +904,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()
}
})
) {
@@ -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
)
}
@@ -566,7 +566,7 @@ fun ListContent(
NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav)
}
if (backupDialogOpen) {
AccountBackupDialog(accountViewModel.account, onClose = { backupDialogOpen = false })
AccountBackupDialog(accountViewModel, onClose = { backupDialogOpen = false })
}
if (conectOrbotDialogOpen) {
ConnectOrbotDialog(
@@ -577,6 +577,12 @@ fun ListContent(
checked = true
enableTor(accountViewModel.account, true, proxyPort, context, coroutineScope)
},
onError = {
accountViewModel.toast(
context.getString(R.string.could_not_connect_to_tor),
it
)
},
proxyPort
)
}
@@ -649,7 +655,7 @@ private fun RenderRelayStatus(
}
}
val placeHolder = MaterialTheme.colors.placeholderText
val placeHolder = MaterialTheme.colorScheme.placeholderText
val color by remember(relayPool) {
derivedStateOf {
@@ -664,7 +670,7 @@ private fun RenderRelayStatus(
Text(
text = text,
color = color,
style = MaterialTheme.typography.subtitle1
style = MaterialTheme.typography.titleMedium
)
}
@@ -674,14 +680,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 +741,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 +758,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 +797,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 +820,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,10 +10,10 @@ 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
@@ -138,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
@@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.screen.BadgeCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@@ -59,16 +58,12 @@ 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) {
val isNew = likeSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
accountViewModel.account.markAsRead(routeForLastRead, likeSetCard.createdAt())
accountViewModel.loadAndMarkAsRead(routeForLastRead, likeSetCard.createdAt()) { isNew ->
val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor)
} else {
@@ -117,7 +112,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 +129,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 +141,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,10 +15,11 @@ 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.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
@@ -30,7 +31,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -50,7 +50,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -61,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@@ -74,6 +74,7 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_ENDED
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
import com.vitorpamplona.quartz.events.Participant
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -275,39 +276,26 @@ private fun CheckNewAndRenderChannelCard(
showPopup: () -> Unit,
nav: (String) -> Unit
) {
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colors.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember {
mutableStateOf<Color>(
parentBackgroundColor?.value ?: defaultBackgroundColor
)
}
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
launch(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = accountViewModel.account.loadLastRead(it)
val createdAt = baseNote.createdAt()
if (createdAt != null) {
accountViewModel.account.markAsRead(it, createdAt)
val isNew = createdAt > lastTime
val newBackgroundColor = if (isNew) {
if (parentBackgroundColor != null) {
newItemColor.compositeOver(parentBackgroundColor.value)
} else {
newItemColor.compositeOver(defaultBackgroundColor)
}
routeForLastRead?.let {
accountViewModel.loadAndMarkAsRead(routeForLastRead, baseNote.createdAt()) { isNew ->
val newBackgroundColor = if (isNew) {
if (parentBackgroundColor != null) {
newItemColor.compositeOver(parentBackgroundColor.value)
} else {
parentBackgroundColor?.value ?: defaultBackgroundColor
}
if (newBackgroundColor != backgroundColor.value) {
launch(Dispatchers.Main) {
backgroundColor.value = newBackgroundColor
}
newItemColor.compositeOver(defaultBackgroundColor)
}
} else {
parentBackgroundColor?.value ?: defaultBackgroundColor
}
} ?: run {
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
if (newBackgroundColor != backgroundColor.value) {
launch(Dispatchers.Main) {
@@ -315,6 +303,13 @@ private fun CheckNewAndRenderChannelCard(
}
}
}
} ?: run {
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
if (newBackgroundColor != backgroundColor.value) {
launch(Dispatchers.Main) {
backgroundColor.value = newBackgroundColor
}
}
}
}
@@ -377,6 +372,18 @@ private fun RenderNoteRow(
}
}
@Immutable
data class LiveActivityCard(
val name: String,
val cover: String?,
val media: String?,
val subject: String?,
val content: String?,
val participants: ImmutableList<Participant>,
val status: String?,
val starts: Long?
)
@Composable
fun RenderLiveActivityThumb(
baseNote: Note,
@@ -385,38 +392,279 @@ fun RenderLiveActivityThumb(
) {
val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return
val eventUpdates by baseNote.live().metadata.observeAsState()
val card by baseNote.live().metadata.map {
val noteEvent = it.note.event as? LiveActivitiesEvent
val media = remember(eventUpdates) { noteEvent.streaming() }
val cover by remember(eventUpdates) {
derivedStateOf {
noteEvent.image()?.ifBlank { null }
}
}
val subject = remember(eventUpdates) { noteEvent.title()?.ifBlank { null } }
val content = remember(eventUpdates) { noteEvent.summary() }
val participants = remember(eventUpdates) { noteEvent.participants() }
val status = remember(eventUpdates) { noteEvent.status() }
val starts = remember(eventUpdates) { noteEvent.starts() }
LiveActivityCard(
name = noteEvent?.dTag() ?: "",
cover = noteEvent?.image()?.ifBlank { null },
media = noteEvent?.streaming(),
subject = noteEvent?.title()?.ifBlank { null },
content = noteEvent?.summary(),
participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(),
status = noteEvent?.status(),
starts = noteEvent?.starts()
)
}.distinctUntilChanged().observeAsState(
LiveActivityCard(
name = noteEvent.dTag(),
cover = noteEvent.image()?.ifBlank { null },
media = noteEvent.streaming(),
subject = noteEvent.title()?.ifBlank { null },
content = noteEvent.summary(),
participants = noteEvent.participants().toImmutableList(),
status = noteEvent.status(),
starts = noteEvent.starts()
)
)
var isOnline by remember { mutableStateOf(false) }
LaunchedEffect(key1 = media) {
launch(Dispatchers.IO) {
val newIsOnline = OnlineChecker.isOnline(media)
LaunchedEffect(key1 = card.media) {
accountViewModel.checkIsOnline(card.media) { newIsOnline ->
if (isOnline != newIsOnline) {
isOnline = newIsOnline
}
}
}
Column(
modifier = Modifier.fillMaxWidth()
) {
Box(
contentAlignment = TopEnd,
modifier = Modifier
.aspectRatio(ratio = 16f / 9f)
.fillMaxWidth()
) {
card.cover?.let {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(QuoteBorder)
)
} ?: run {
baseNote.author?.let {
DisplayAuthorBanner(it)
}
}
Box(Modifier.padding(10.dp)) {
Crossfade(targetState = card.status) {
when (it) {
STATUS_LIVE -> {
if (card.media.isNullOrBlank()) {
LiveFlag()
} else if (isOnline) {
LiveFlag()
} else {
OfflineFlag()
}
}
STATUS_ENDED -> {
EndedFlag()
}
STATUS_PLANNED -> {
ScheduledFlag(card.starts)
}
else -> {
EndedFlag()
}
}
}
}
LoadParticipants(card.participants, baseNote, accountViewModel) { participantUsers ->
Box(
Modifier
.padding(10.dp)
.align(BottomStart)
) {
if (participantUsers.isNotEmpty()) {
Gallery(participantUsers, accountViewModel)
}
}
}
}
Spacer(modifier = DoubleVertSpacer)
ChannelHeader(
channelHex = remember { baseNote.idHex },
showVideo = false,
showBottomDiviser = false,
showFlag = false,
sendToChannel = true,
modifier = remember {
Modifier.padding(start = 0.dp, end = 0.dp, top = 5.dp, bottom = 5.dp)
},
accountViewModel = accountViewModel,
nav = nav
)
}
}
@Immutable
data class CommunityCard(
val name: String,
val description: String?,
val cover: String?,
val moderators: ImmutableList<Participant>
)
@Composable
fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteEvent = baseNote.event as? CommunityDefinitionEvent ?: return
val card by baseNote.live().metadata.map {
val noteEvent = it.note.event as? CommunityDefinitionEvent
CommunityCard(
name = noteEvent?.dTag() ?: "",
description = noteEvent?.description(),
cover = noteEvent?.image()?.ifBlank { null },
moderators = noteEvent?.moderators()?.toImmutableList() ?: persistentListOf()
)
}.distinctUntilChanged().observeAsState(
CommunityCard(
name = noteEvent.dTag(),
description = noteEvent.description(),
cover = noteEvent.image()?.ifBlank { null },
moderators = noteEvent.moderators().toImmutableList()
)
)
Row(Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth(0.3f)
.aspectRatio(ratio = 1f)
) {
card.cover?.let {
Box(contentAlignment = BottomStart) {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(QuoteBorder)
)
}
} ?: run {
baseNote.author?.let {
DisplayAuthorBanner(it)
}
}
}
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = card.name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
}
card.description?.let {
Spacer(modifier = StdVertSpacer)
Row() {
Text(
text = it,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp
)
}
}
LoadModerators(card.moderators, baseNote, accountViewModel) { participantUsers ->
if (participantUsers.isNotEmpty()) {
Spacer(modifier = StdVertSpacer)
Row(modifier = Modifier.fillMaxWidth()) {
Gallery(participantUsers, accountViewModel)
}
}
}
}
}
}
@Composable
fun LoadModerators(
moderators: ImmutableList<Participant>,
baseNote: Note,
accountViewModel: AccountViewModel,
content: @Composable (ImmutableList<User>) -> Unit
) {
var participantUsers by remember {
mutableStateOf<ImmutableList<User>>(
persistentListOf()
)
}
LaunchedEffect(key1 = eventUpdates) {
LaunchedEffect(key1 = moderators) {
launch(Dispatchers.IO) {
val hosts = moderators.mapNotNull { part ->
if (part.key != baseNote.author?.pubkeyHex) {
LocalCache.checkGetOrCreateUser(part.key)
} else {
null
}
}
val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList)
val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
val newParticipantUsers = if (followingKeySet == null) {
val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS)
val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
(hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
} else {
(hosts + allParticipants).toImmutableList()
}
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
participantUsers = newParticipantUsers
}
}
}
content(participantUsers)
}
@Composable
private fun LoadParticipants(
participants: ImmutableList<Participant>,
baseNote: Note,
accountViewModel: AccountViewModel,
inner: @Composable (ImmutableList<User>) -> Unit
) {
var participantUsers by remember {
mutableStateOf<ImmutableList<User>>(
persistentListOf()
)
}
LaunchedEffect(key1 = participants) {
launch(Dispatchers.IO) {
val hosts = participants.mapNotNull { part ->
if (part.key != baseNote.author?.pubkeyHex) {
@@ -450,196 +698,7 @@ fun RenderLiveActivityThumb(
}
}
Column(
modifier = Modifier.fillMaxWidth()
) {
Box(
contentAlignment = TopEnd,
modifier = Modifier
.aspectRatio(ratio = 16f / 9f)
.fillMaxWidth()
) {
cover?.let {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(QuoteBorder)
)
} ?: run {
baseNote.author?.let {
DisplayAuthorBanner(it)
}
}
Box(Modifier.padding(10.dp)) {
Crossfade(targetState = status) {
when (it) {
STATUS_LIVE -> {
if (media.isNullOrBlank()) {
LiveFlag()
} else if (isOnline) {
LiveFlag()
} else {
OfflineFlag()
}
}
STATUS_ENDED -> {
EndedFlag()
}
STATUS_PLANNED -> {
ScheduledFlag(starts)
}
else -> {
EndedFlag()
}
}
}
}
Box(
Modifier
.padding(10.dp)
.align(BottomStart)
) {
if (participantUsers.isNotEmpty()) {
Gallery(participantUsers, accountViewModel)
}
}
}
ChannelHeader(
channelHex = remember { baseNote.idHex },
showVideo = false,
showBottomDiviser = false,
showFlag = false,
sendToChannel = true,
modifier = remember {
Modifier.padding(start = 0.dp, end = 0.dp, top = 5.dp, bottom = 5.dp)
},
accountViewModel = accountViewModel,
nav = nav
)
}
}
@Composable
fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteEvent = baseNote.event as? CommunityDefinitionEvent ?: return
val eventUpdates by baseNote.live().metadata.observeAsState()
val name = remember(eventUpdates) { noteEvent.dTag() }
val description = remember(eventUpdates) { noteEvent.description() }
val cover by remember(eventUpdates) {
derivedStateOf {
noteEvent.image()?.ifBlank { null }
}
}
val moderators = remember(eventUpdates) { noteEvent.moderators() }
var participantUsers by remember {
mutableStateOf<ImmutableList<User>>(
persistentListOf()
)
}
LaunchedEffect(key1 = eventUpdates) {
launch(Dispatchers.IO) {
val hosts = moderators.mapNotNull { part ->
if (part.key != baseNote.author?.pubkeyHex) {
LocalCache.checkGetOrCreateUser(part.key)
} else {
null
}
}
val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList)
val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
val newParticipantUsers = if (followingKeySet == null) {
val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS)
val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
(hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
} else {
(hosts + allParticipants).toImmutableList()
}
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
participantUsers = newParticipantUsers
}
}
}
Row(Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth(0.3f)
.aspectRatio(ratio = 1f)
) {
cover?.let {
Box(contentAlignment = BottomStart) {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(QuoteBorder)
)
}
} ?: run {
baseNote.author?.let {
DisplayAuthorBanner(it)
}
}
}
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
}
description?.let {
Spacer(modifier = StdVertSpacer)
Row() {
Text(
text = it,
color = MaterialTheme.colors.placeholderText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp
)
}
}
if (participantUsers.isNotEmpty()) {
Spacer(modifier = StdVertSpacer)
Row(modifier = Modifier.fillMaxWidth()) {
Gallery(participantUsers, accountViewModel)
}
}
}
}
inner(participantUsers)
}
@Composable
@@ -716,7 +775,9 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier.fillMaxWidth().fillMaxHeight()
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
@@ -728,9 +789,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 +799,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 +829,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,8 +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.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
@@ -185,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
@@ -200,6 +210,7 @@ private fun ChannelRoomCompose(
channelLastTime = remember(note) { note.createdAt() },
channelLastContent = remember(note) { "$authorName: $description" },
hasNewMessages = hasNewMessages,
loadProfilePicture = automaticallyShowProfilePicture,
onClick = { nav(route) }
)
}
@@ -207,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(
@@ -404,9 +415,7 @@ private fun WatchNotificationChanges(
accountViewModel: AccountViewModel,
onNewStatus: (Boolean) -> Unit
) {
val cacheState by accountViewModel.accountLastReadLiveData.observeAsState()
LaunchedEffect(key1 = note, cacheState) {
LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.value) {
launch(Dispatchers.IO) {
note.event?.createdAt()?.let {
val lastTime = accountViewModel.account.loadLastRead(route)
@@ -443,6 +452,7 @@ fun ChannelName(
channelLastTime: Long?,
channelLastContent: String?,
hasNewMessages: MutableState<Boolean>,
loadProfilePicture: Boolean,
onClick: () -> Unit
) {
ChannelName(
@@ -456,7 +466,8 @@ fun ChannelName(
.width(Size55dp)
.height(Size55dp)
.clip(shape = CircleShape)
}
},
loadProfilePicture = loadProfilePicture
)
},
channelTitle,
@@ -517,7 +528,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),
@@ -526,7 +537,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
@@ -560,7 +571,7 @@ private fun TimeAgo(channelLastTime: Long?) {
}
Text(
text = timeAgo,
color = MaterialTheme.colors.grayText,
color = MaterialTheme.colorScheme.grayText,
maxLines = 1
)
}
@@ -573,7 +584,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,10 +15,10 @@ 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
@@ -43,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
@@ -71,8 +73,6 @@ import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -230,9 +230,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)) {
@@ -258,12 +258,7 @@ fun NormalChatNote(
if (routeForLastRead != null) {
LaunchedEffect(key1 = routeForLastRead) {
launch(Dispatchers.IO) {
val createdAt = note.createdAt()
if (createdAt != null) {
accountViewModel.account.markAsRead(routeForLastRead, createdAt)
}
}
accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) { }
}
}
@@ -400,10 +395,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 {
@@ -561,13 +565,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
@@ -588,7 +592,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(
@@ -597,7 +601,7 @@ fun IncognitoBadge(baseNote: Note) {
modifier = Modifier
.padding(top = 1.dp)
.size(14.dp),
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
}
@@ -614,7 +618,7 @@ fun ChatTimeAgo(baseNote: Note) {
Text(
text = time,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
fontSize = Font12SP,
maxLines = 1
)
@@ -712,6 +716,7 @@ private fun RenderCreateChannelNote(note: Note) {
private fun DrawAuthorInfo(
baseNote: Note,
alignment: Arrangement.Horizontal,
loadProfilePicture: Boolean,
nav: (String) -> Unit
) {
Row(
@@ -719,26 +724,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 }
@@ -764,7 +771,7 @@ private fun WatchAndDisplayUser(
}
}
UserIcon(pubkeyHex, userProfilePicture, nav, route)
UserIcon(pubkeyHex, userProfilePicture, loadProfilePicture, nav, route)
userDisplayName?.let {
DisplayMessageUsername(it, userTags, route, nav)
@@ -775,6 +782,7 @@ private fun WatchAndDisplayUser(
private fun UserIcon(
pubkeyHex: String,
userProfilePicture: String?,
loadProfilePicture: Boolean,
nav: (String) -> Unit,
route: String
) {
@@ -782,6 +790,7 @@ private fun UserIcon(
robot = pubkeyHex,
model = userProfilePicture,
contentDescription = stringResource(id = R.string.profile_image),
loadProfilePicture = loadProfilePicture,
modifier = remember {
Modifier
.width(Size25dp)
@@ -808,7 +817,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
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@@ -43,16 +42,12 @@ 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) {
val isNew = messageSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
accountViewModel.account.markAsRead(routeForLastRead, messageSetCard.createdAt())
accountViewModel.loadAndMarkAsRead(routeForLastRead, messageSetCard.createdAt()) { isNew ->
val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor)
} else {
@@ -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
@@ -43,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
@@ -89,16 +91,12 @@ 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) {
val isNew = multiSetCard.maxCreatedAt > accountViewModel.account.loadLastRead(routeForLastRead)
accountViewModel.account.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
accountViewModel.loadAndMarkAsRead(routeForLastRead, multiSetCard.maxCreatedAt) { isNew ->
val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor)
} else {
@@ -439,7 +437,7 @@ fun CrossfadeToDisplayAmount(amount: String) {
modifier = amountBoxModifier,
contentAlignment = Alignment.BottomCenter
) {
val backgroundColor = MaterialTheme.colors.overPictureBackground
val backgroundColor = MaterialTheme.colorScheme.overPictureBackground
Box(
modifier = remember {
Modifier
@@ -451,7 +449,7 @@ fun CrossfadeToDisplayAmount(amount: String) {
Text(
text = amount,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.bitcoinColor,
color = MaterialTheme.colorScheme.bitcoinColor,
fontSize = commentTextSize,
modifier = bottomPadding1dp
)
@@ -525,14 +523,23 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
author: User,
accountViewModel: AccountViewModel
) {
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.colors.profile35dpModifier,
contentScale = ContentScale.Crop
modifier = MaterialTheme.colorScheme.profile35dpModifier,
contentScale = ContentScale.Crop,
loadProfilePicture = automaticallyShowProfilePicture
)
// }
}
@@ -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
@@ -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,7 +229,7 @@ fun DisplayStatus(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
tint = MaterialTheme.colorScheme.lessImportantLink
)
}
} else if (nostrATag != null) {
@@ -249,7 +249,7 @@ fun DisplayStatus(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
tint = MaterialTheme.colorScheme.lessImportantLink
)
}
}
@@ -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
@@ -28,19 +28,19 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
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.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.darkColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.lightColors
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
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.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
@@ -88,7 +88,6 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
@@ -162,6 +161,7 @@ import com.vitorpamplona.amethyst.ui.theme.replyBackground
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
@@ -233,7 +233,7 @@ fun NoteCompose(
) {
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent) {
Crossfade(targetState = hasEvent, label = "Event presence") {
if (it) {
CheckHiddenNoteCompose(
note = baseNote,
@@ -624,7 +624,7 @@ fun LongCommunityHeader(
Modifier.weight(1f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
val defaultBackground = MaterialTheme.colors.background
val defaultBackground = MaterialTheme.colorScheme.background
val background = remember {
mutableStateOf(defaultBackground)
}
@@ -728,6 +728,14 @@ fun ShortCommunityHeader(baseNote: AddressableNote, fontWeight: FontWeight = Fon
val noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
noteEvent.image()?.let {
RobohashAsyncImageProxy(
@@ -735,7 +743,8 @@ fun ShortCommunityHeader(baseNote: AddressableNote, fontWeight: FontWeight = Fon
model = it,
contentDescription = stringResource(R.string.profile_image),
contentScale = ContentScale.Crop,
modifier = HeaderPictureModifier
modifier = HeaderPictureModifier,
loadProfilePicture = automaticallyShowProfilePicture
)
}
@@ -774,9 +783,9 @@ private fun ShortCommunityActionOptions(
nav: (String) -> Unit
) {
Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
LikeReaction(baseNote = note, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
ZapReaction(baseNote = note, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
if (!isFollowing) {
@@ -826,39 +835,22 @@ private fun CheckNewAndRenderNote(
showPopup: () -> Unit,
nav: (String) -> Unit
) {
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colors.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember(baseNote) { mutableStateOf<Color>(parentBackgroundColor?.value ?: defaultBackgroundColor) }
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
launch(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = accountViewModel.account.loadLastRead(it)
val createdAt = baseNote.createdAt()
if (createdAt != null) {
accountViewModel.account.markAsRead(it, createdAt)
val isNew = createdAt > lastTime
val newBackgroundColor = if (isNew) {
if (parentBackgroundColor != null) {
newItemColor.compositeOver(parentBackgroundColor.value)
} else {
newItemColor.compositeOver(defaultBackgroundColor)
}
routeForLastRead?.let {
accountViewModel.loadAndMarkAsRead(it, baseNote.createdAt()) { isNew ->
val newBackgroundColor = if (isNew) {
if (parentBackgroundColor != null) {
newItemColor.compositeOver(parentBackgroundColor.value)
} else {
parentBackgroundColor?.value ?: defaultBackgroundColor
}
if (newBackgroundColor != backgroundColor.value) {
launch(Dispatchers.Main) {
backgroundColor.value = newBackgroundColor
}
newItemColor.compositeOver(defaultBackgroundColor)
}
} else {
parentBackgroundColor?.value ?: defaultBackgroundColor
}
} ?: run {
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
if (newBackgroundColor != backgroundColor.value) {
launch(Dispatchers.Main) {
@@ -866,6 +858,14 @@ private fun CheckNewAndRenderNote(
}
}
}
} ?: run {
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
if (newBackgroundColor != backgroundColor.value) {
launch(Dispatchers.Main) {
backgroundColor.value = newBackgroundColor
}
}
}
}
@@ -1112,10 +1112,15 @@ fun DisplayZapSplits(noteEvent: EventInterface, accountViewModel: AccountViewMod
ClickableText(
text = AnnotatedString(it.lnAddressOrPubKeyHex),
onClick = { },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
} else {
UserPicture(userHex = it.lnAddressOrPubKeyHex, size = 25.dp, accountViewModel = accountViewModel, nav = nav)
UserPicture(
userHex = it.lnAddressOrPubKeyHex,
size = Size25dp,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
@@ -1286,8 +1291,8 @@ fun routeFor(note: Note, loggedIn: User): String? {
return null
}
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex))
fun routeToMessage(user: HexKey, draftMessage: String?, accountViewModel: AccountViewModel): String {
val withKey = ChatroomKey(persistentSetOf(user))
accountViewModel.account.userProfile().createChatroom(withKey)
return if (draftMessage != null) {
"Room/${withKey.hashCode()}?message=$draftMessage"
@@ -1296,6 +1301,10 @@ fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountV
}
}
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
return routeToMessage(user.pubkeyHex, draftMessage, accountViewModel)
}
fun routeFor(note: Channel): String {
return "Channel/${note.idHex}"
}
@@ -1334,7 +1343,7 @@ fun RenderTextEvent(
if (makeItShort && isAuthorTheLoggedUser) {
Text(
text = eventContent,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -1378,7 +1387,7 @@ fun RenderPoll(
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
Text(
text = eventContent,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -1494,12 +1503,12 @@ fun RenderAppDefinition(
modifier = Modifier
.border(
3.dp,
MaterialTheme.colors.background,
MaterialTheme.colorScheme.background,
CircleShape
)
.clip(shape = CircleShape)
.fillMaxSize()
.background(MaterialTheme.colors.background)
.background(MaterialTheme.colorScheme.background)
.combinedClickable(
onClick = { zoomImageDialogOpen = true },
onLongClick = {
@@ -1543,12 +1552,12 @@ fun RenderAppDefinition(
val website = remember(it) { it.website }
if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
LinkIcon(Size16Modifier, MaterialTheme.colors.placeholderText)
LinkIcon(Size16Modifier, MaterialTheme.colorScheme.placeholderText)
ClickableText(
text = AnnotatedString(website.removePrefix("https://")),
onClick = { website.let { runCatching { uri.openUri(it) } } },
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 = 5.dp)
)
}
@@ -1559,7 +1568,7 @@ fun RenderAppDefinition(
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
) {
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
val bgColor = MaterialTheme.colors.background
val bgColor = MaterialTheme.colorScheme.background
val backgroundColor = remember {
mutableStateOf(bgColor)
}
@@ -1637,7 +1646,7 @@ private fun RenderPrivateMessage(
if (makeItShort && isAuthorTheLoggedUser) {
Text(
text = eventContent!!,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -2034,7 +2043,7 @@ fun RenderPostApproval(
note.replyTo?.forEach {
NoteCompose(
it,
modifier = MaterialTheme.colors.replyModifier,
modifier = MaterialTheme.colorScheme.replyModifier,
unPackReply = false,
makeItShort = true,
isQuotedNote = true,
@@ -2239,10 +2248,9 @@ fun RemoveButton(onClick: () -> Unit) {
modifier = Modifier.padding(start = 3.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
),
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.remove), color = Color.White)
@@ -2250,15 +2258,23 @@ fun RemoveButton(onClick: () -> Unit) {
}
@Composable
fun AddButton(text: Int = R.string.add, onClick: () -> Unit) {
fun AddButton(
text: Int = R.string.add,
isActive: Boolean = true,
modifier: Modifier = Modifier.padding(start = 3.dp),
onClick: () -> Unit
) {
Button(
modifier = Modifier.padding(start = 3.dp),
onClick = onClick,
modifier = modifier,
onClick = {
if (isActive) {
onClick()
}
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
colors = ButtonDefaults.buttonColors(
containerColor = if (isActive) MaterialTheme.colorScheme.primary else Color.Gray
),
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
) {
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
@@ -2302,7 +2318,7 @@ fun RenderPinListEvent(
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
pinsToShow.forEach { pin ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) {
PinIcon(modifier = Size15Modifier, tint = MaterialTheme.colors.onBackground.copy(0.32f))
PinIcon(modifier = Size15Modifier, tint = MaterialTheme.colorScheme.onBackground.copy(0.32f))
Spacer(modifier = Modifier.width(5.dp))
@@ -2425,7 +2441,7 @@ private fun RenderReport(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
),
unPackReply = false,
@@ -2469,7 +2485,7 @@ private fun ReplyRow(
showVideo = false,
showBottomDiviser = false,
sendToChannel = true,
modifier = MaterialTheme.colors.replyModifier.padding(10.dp),
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
accountViewModel = accountViewModel,
nav = nav
)
@@ -2502,7 +2518,7 @@ private fun ReplyNoteComposition(
val replyBackgroundColor = remember {
mutableStateOf(backgroundColor.value)
}
val defaultReplyBackground = MaterialTheme.colors.replyBackground
val defaultReplyBackground = MaterialTheme.colorScheme.replyBackground
LaunchedEffect(key1 = backgroundColor.value, key2 = defaultReplyBackground) {
launch(Dispatchers.Default) {
@@ -2517,7 +2533,7 @@ private fun ReplyNoteComposition(
NoteCompose(
baseNote = replyingDirectlyTo,
isQuotedNote = true,
modifier = MaterialTheme.colors.replyModifier,
modifier = MaterialTheme.colorScheme.replyModifier,
unPackReply = false,
makeItShort = true,
parentBackgroundColor = replyBackgroundColor,
@@ -2604,7 +2620,7 @@ fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
text = AnnotatedString(cityName),
onClick = { nav("Geohash/$geohash") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
),
fontSize = Font14SP,
@@ -2635,7 +2651,7 @@ fun FirstUserInfoRow(
}
}
val textColor = if (isRepost) MaterialTheme.colors.grayText else Color.Unspecified
val textColor = if (isRepost) MaterialTheme.colorScheme.grayText else Color.Unspecified
if (showAuthorPicture) {
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
@@ -2664,7 +2680,7 @@ private fun BoostedMark() {
Text(
stringResource(id = R.string.boosted),
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
modifier = HalfStartPadding
)
@@ -2707,7 +2723,7 @@ fun TimeAgo(time: Long) {
Text(
text = timeStr,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1
)
}
@@ -2770,11 +2786,19 @@ private fun RenderAuthorImages(
val isChannel = baseNote.event is ChannelMessageEvent && baseNote.channelHex() != null
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
if (isChannel) {
val baseChannelHex = remember { baseNote.channelHex() }
if (baseChannelHex != null) {
LoadChannel(baseChannelHex, accountViewModel) { channel ->
ChannelNotePicture(channel)
ChannelNotePicture(channel, loadProfilePicture = automaticallyShowProfilePicture)
}
}
}
@@ -2802,12 +2826,12 @@ fun LoadChannel(baseChannelHex: String, accountViewModel: AccountViewModel, cont
}
@Composable
private fun ChannelNotePicture(baseChannel: Channel) {
private fun ChannelNotePicture(baseChannel: Channel, loadProfilePicture: Boolean) {
val model by baseChannel.live.map {
it.channel.profilePicture()
}.distinctUntilChanged().observeAsState()
val backgroundColor = MaterialTheme.colors.background
val backgroundColor = MaterialTheme.colorScheme.background
val modifier = remember {
Modifier
@@ -2827,7 +2851,8 @@ private fun ChannelNotePicture(baseChannel: Channel) {
robot = baseChannel.idHex,
model = model,
contentDescription = stringResource(R.string.group_picture),
modifier = modifier
modifier = modifier,
loadProfilePicture = loadProfilePicture
)
}
}
@@ -2850,7 +2875,7 @@ private fun RepostNoteAuthorPicture(
}
Box(Size18Modifier.align(Alignment.BottomStart).padding(1.dp)) {
RepostedIcon(modifier = Size18Modifier, MaterialTheme.colors.placeholderText)
RepostedIcon(modifier = Size18Modifier, MaterialTheme.colorScheme.placeholderText)
}
Box(Size35Modifier.align(Alignment.BottomEnd)) {
@@ -2953,7 +2978,7 @@ private fun LoadAndDisplayPost(postAddress: ATag, accountViewModel: AccountViewM
nav(it)
}
},
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
}
}
@@ -3055,7 +3080,7 @@ private fun DisplayTagList(firstTag: String, nav: (String) -> Unit) {
text = displayTag,
onClick = { nav(route) },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
),
@@ -3076,7 +3101,7 @@ private fun DisplayCommunity(note: Note, nav: (String) -> Unit) {
text = displayTag,
onClick = { nav(route) },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
),
@@ -3101,26 +3126,28 @@ fun DisplayUncitedHashtags(
eventContent: String,
nav: (String) -> Unit
) {
val hasHashtags = remember {
val hasHashtags = remember(eventContent) {
hashtags.isNotEmpty()
}
if (hasHashtags) {
val unusedHashtags = remember(eventContent) {
hashtags.filter { !eventContent.contains(it, true) }
}
FlowRow(
modifier = remember { Modifier.padding(top = 5.dp) }
) {
hashtags.forEach { hashtag ->
if (!eventContent.contains(hashtag, true)) {
ClickableText(
text = AnnotatedString("#$hashtag "),
onClick = { nav("Hashtag/$hashtag") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
alpha = 0.52f
)
unusedHashtags.forEach { hashtag ->
ClickableText(
text = remember { AnnotatedString("#$hashtag ") },
onClick = { nav("Hashtag/$hashtag") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
)
}
)
}
}
}
@@ -3136,7 +3163,7 @@ fun DisplayPoW(
Text(
powStr,
color = MaterialTheme.colors.lessImportantLink,
color = MaterialTheme.colorScheme.lessImportantLink,
fontSize = Font14SP,
fontWeight = FontWeight.Bold,
maxLines = 1
@@ -3164,7 +3191,7 @@ fun DisplayReward(
text = AnnotatedString("#bounty"),
onClick = { nav("Hashtag/bounty") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
)
@@ -3220,19 +3247,19 @@ private fun RenderPledgeAmount(
if (hasPledge) {
ZappedIcon(modifier = Size20Modifier)
} else {
ZapIcon(modifier = Size20Modifier, MaterialTheme.colors.placeholderText)
ZapIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText)
}
Text(
text = reward,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1
)
}
@Composable
fun BadgeDisplay(baseNote: Note) {
val background = MaterialTheme.colors.background
val background = MaterialTheme.colorScheme.background
val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return
val image = remember { badgeData.thumb()?.ifBlank { null } ?: badgeData.image() }
@@ -3248,9 +3275,9 @@ fun BadgeDisplay(baseNote: Note) {
val backgroundColor = it.drawable.toBitmap(200, 200).copy(Bitmap.Config.ARGB_8888, false).get(0, 199)
val colorFromImage = Color(backgroundColor)
val textBackground = if (colorFromImage.luminance() > 0.5) {
lightColors().onBackground
lightColorScheme().onBackground
} else {
darkColors().onBackground
darkColorScheme().onBackground
}
launch(Dispatchers.Main) {
@@ -3266,7 +3293,7 @@ fun BadgeDisplay(baseNote: Note) {
.clip(shape = CutCornerShape(20, 20, 20, 20))
.border(
5.dp,
MaterialTheme.colors.mediumImportanceLink,
MaterialTheme.colorScheme.mediumImportanceLink,
CutCornerShape(20)
)
.background(backgroundFromImage.first)
@@ -3309,7 +3336,7 @@ private fun RenderBadge(
name?.let {
Text(
text = it,
style = MaterialTheme.typography.body1,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
@@ -3321,7 +3348,7 @@ private fun RenderBadge(
description?.let {
Text(
text = it,
style = MaterialTheme.typography.caption,
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
@@ -3343,7 +3370,7 @@ fun FileHeaderDisplay(note: Note, roundedCorner: Boolean, accountViewModel: Acco
val blurHash = event.blurhash()
val hash = event.hash()
val dimensions = event.dimensions()
val description = event.content
val description = event.alt() ?: event.content
val isImage = imageExtensions.any { fullUrl.split("?")[0].lowercase().endsWith(it) }
val uri = note.toNostrUri()
@@ -3407,7 +3434,7 @@ private fun ObserverAndRenderNIP95(
val localDir = note?.idHex?.let { File(File(appContext.cacheDir, "NIP95"), it) }
val blurHash = eventHeader.blurhash()
val dimensions = eventHeader.dimensions()
val description = eventHeader.content
val description = eventHeader.alt() ?: eventHeader.content
val mimeType = eventHeader.mimeType()
val newContent = if (mimeType?.startsWith("image") == true) {
@@ -3492,7 +3519,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
it.first.role?.let {
Text(
text = it.capitalize(Locale.ROOT),
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1
)
}
@@ -3533,7 +3560,7 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou
val waveform = remember { noteEvent.wavefrom()?.toImmutableList()?.ifEmpty { null } }
val content = remember { noteEvent.content().ifBlank { null } }
val defaultBackground = MaterialTheme.colors.background
val defaultBackground = MaterialTheme.colorScheme.background
val background = remember { mutableStateOf(defaultBackground) }
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
@@ -3621,8 +3648,10 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
var isOnline by remember { mutableStateOf(false) }
LaunchedEffect(key1 = media) {
launch(Dispatchers.IO) {
isOnline = OnlineChecker.isOnline(media)
accountViewModel.checkIsOnline(media) { newIsOnline ->
if (isOnline != newIsOnline) {
isOnline = newIsOnline
}
}
}
@@ -3688,7 +3717,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
it.first.role?.let {
Text(
text = it.capitalize(Locale.ROOT),
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1
)
}
@@ -3720,7 +3749,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
) {
Text(
text = stringResource(id = R.string.live_stream_is_offline),
color = MaterialTheme.colors.onBackground,
color = MaterialTheme.colorScheme.onBackground,
fontWeight = FontWeight.Bold
)
}
@@ -3734,7 +3763,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
) {
Text(
text = stringResource(id = R.string.live_stream_has_ended),
color = MaterialTheme.colors.onBackground,
color = MaterialTheme.colorScheme.onBackground,
fontWeight = FontWeight.Bold
)
}
@@ -3754,7 +3783,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
)
) {
@@ -3784,7 +3813,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
title?.let {
Text(
text = it,
style = MaterialTheme.typography.body1,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
@@ -3794,7 +3823,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
summary?.let {
Text(
text = it,
style = MaterialTheme.typography.caption,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
@@ -3820,7 +3849,7 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
)
) {
@@ -3843,7 +3872,7 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi
title?.let {
Text(
text = it,
style = MaterialTheme.typography.body1,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
modifier = Modifier.weight(1f)
)
@@ -3863,7 +3892,7 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi
Text(
text = priceTag,
maxLines = 1,
color = MaterialTheme.colors.primary,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
modifier = remember {
Modifier
@@ -3880,7 +3909,7 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi
summary?.let {
Text(
text = it,
style = MaterialTheme.typography.caption,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.weight(1f),
color = Color.Gray,
@@ -3892,7 +3921,7 @@ private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountVi
location?.let {
Text(
text = it,
style = MaterialTheme.typography.caption,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
@@ -15,16 +16,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.Block
@@ -35,6 +26,17 @@ import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.PersonRemove
import androidx.compose.material.icons.filled.Report
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@@ -65,6 +67,7 @@ import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.amethyst.ui.theme.WarningColor
import com.vitorpamplona.amethyst.ui.theme.isLight
import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.FileHeaderEvent
@@ -114,10 +117,11 @@ private fun VerticalDivider(color: Color) =
fun LongPressToQuickAction(baseNote: Note, accountViewModel: AccountViewModel, content: @Composable (() -> Unit) -> Unit) {
val popupExpanded = remember { mutableStateOf(false) }
val showPopup = remember { { popupExpanded.value = true } }
val hidePopup = remember { { popupExpanded.value = false } }
content(showPopup)
NoteQuickActionMenu(baseNote, popupExpanded.value, { popupExpanded.value = false }, accountViewModel)
NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel)
}
@Composable
@@ -176,15 +180,15 @@ private fun RenderMainPopup(
showReportDialog: MutableState<Boolean>
) {
val context = LocalContext.current
val primaryLight = lightenColor(MaterialTheme.colors.primary, 0.1f)
val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f)
val cardShape = RoundedCornerShape(5.dp)
val clipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
val backgroundColor = if (MaterialTheme.colors.isLight) {
MaterialTheme.colors.primary
val backgroundColor = if (MaterialTheme.colorScheme.isLight) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colors.secondaryButtonBackground
MaterialTheme.colorScheme.secondaryButtonBackground
}
val showToast = { stringResource: Int ->
@@ -204,7 +208,7 @@ private fun RenderMainPopup(
Card(
modifier = Modifier.shadow(elevation = 6.dp, shape = cardShape),
shape = cardShape,
backgroundColor = backgroundColor
colors = CardDefaults.cardColors(containerColor = backgroundColor)
) {
Column(modifier = Modifier.width(IntrinsicSize.Min)) {
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
@@ -287,20 +291,16 @@ private fun RenderMainPopup(
Icons.Default.PersonRemove,
stringResource(R.string.quick_action_unfollow)
) {
scope.launch(Dispatchers.IO) {
accountViewModel.unfollow(note.author!!)
onDismiss()
}
accountViewModel.unfollow(note.author!!)
onDismiss()
}
} else {
NoteQuickActionItem(
Icons.Default.PersonAdd,
stringResource(R.string.quick_action_follow)
) {
scope.launch(Dispatchers.IO) {
accountViewModel.follow(note.author!!)
onDismiss()
}
accountViewModel.follow(note.author!!)
onDismiss()
}
}
@@ -412,7 +412,7 @@ private fun BlockAlertDialog(note: Note, accountViewModel: AccountViewModel, onD
buttonIcon = Icons.Default.Block,
buttonText = stringResource(R.string.quick_action_block_dialog_btn),
buttonColors = ButtonDefaults.buttonColors(
backgroundColor = WarningColor,
containerColor = WarningColor,
contentColor = Color.White
),
onClickDoOnce = {
@@ -502,7 +502,7 @@ fun QuickActionAlertDialog(
text = {
Text(textContent)
},
buttons = {
confirmButton = {
Row(
modifier = Modifier
.padding(all = 8.dp)
@@ -512,7 +512,7 @@ fun QuickActionAlertDialog(
TextButton(onClick = onClickDontShowAgain) {
Text(stringResource(R.string.quick_action_dont_show_again_button))
}
Button(onClick = onClickDoOnce, colors = buttonColors) {
Button(onClick = onClickDoOnce, colors = buttonColors, contentPadding = PaddingValues(horizontal = 16.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
@@ -1,12 +1,13 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
@@ -27,6 +28,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP
@@ -128,7 +130,7 @@ private fun OptionNote(
val color = if (poolOption.consensusThreadhold) {
Color.Green.copy(alpha = 0.32f)
} else {
MaterialTheme.colors.mediumImportanceLink
MaterialTheme.colorScheme.mediumImportanceLink
}
ZapVote(
@@ -252,7 +254,7 @@ private fun RenderOptionBeforeVote(
.clip(shape = QuoteBorder)
.border(
2.dp,
MaterialTheme.colors.primary,
MaterialTheme.colorScheme.primary,
QuoteBorder
)
) {
@@ -294,7 +296,7 @@ fun ZapVote(
}
var zappingProgress by remember { mutableStateOf(0f) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<StringToastMsg?>(null) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -305,50 +307,30 @@ fun ZapVote(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.combinedClickable(
role = Role.Button,
// interactionSource = remember { MutableInteractionSource() },
// indication = rememberRipple(bounded = false, radius = 24.dp),
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = 24.dp),
onClick = {
if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_send_zaps
)
} else if (pollViewModel.isPollClosed()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.poll_is_closed),
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
R.string.poll_unable_to_vote,
R.string.poll_is_closed_explainer
)
} else if (isLoggedUser) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.poll_author_no_vote),
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
R.string.poll_unable_to_vote,
R.string.poll_author_no_vote
)
} else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn) {
// only allow one vote per option when min==max, i.e. atomic vote amount specified
scope.launch {
Toast
.makeText(
context,
R.string.one_vote_per_user_on_atomic_votes,
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
R.string.poll_unable_to_vote,
R.string.one_vote_per_user_on_atomic_votes
)
return@combinedClickable
} else if (accountViewModel.account.zapAmountChoices.size == 1 &&
pollViewModel.isValidInputVoteAmount(accountViewModel.account.zapAmountChoices.first())
@@ -359,13 +341,9 @@ fun ZapVote(
poolOption.option,
"",
context,
onError = {
scope.launch {
zappingProgress = 0f
Toast
.makeText(context, it, Toast.LENGTH_SHORT)
.show()
}
onError = { title, message ->
zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(title, message)
},
onProgress = {
scope.launch(Dispatchers.Main) {
@@ -395,11 +373,9 @@ fun ZapVote(
onChangeAmount = {
wantsToZap = false
},
onError = {
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
}
onError = { title, message ->
showErrorMessageDialog = StringToastMsg(title, message)
zappingProgress = 0f
},
onProgress = {
scope.launch(Dispatchers.Main) {
@@ -413,18 +389,32 @@ fun ZapVote(
}
if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog(payingInvoices = wantsToPay, accountViewModel = accountViewModel) {
wantsToPay = persistentListOf()
}
PayViaIntentDialog(
payingInvoices = wantsToPay,
accountViewModel = accountViewModel,
onClose = {
wantsToPay = persistentListOf()
},
onError = {
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(
context.getString(R.string.error_dialog_zap_error),
it
)
}
}
)
}
if (showErrorMessageDialog != null) {
showErrorMessageDialog?.let { toast ->
ErrorMessageDialog(
title = stringResource(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "",
title = toast.title,
textContent = toast.msg,
onClickStartMessage = {
baseNote.author?.let {
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
nav(routeToMessage(it, toast.msg, accountViewModel))
}
},
onDismiss = { showErrorMessageDialog = null }
@@ -447,7 +437,7 @@ fun ZapVote(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
} else {
Spacer(Modifier.width(3.dp))
@@ -468,7 +458,7 @@ fun ZapVote(
Text(
text = amountStr,
fontSize = Font14SP,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
modifier = modifier
)
}
@@ -483,7 +473,7 @@ fun FilteredZapAmountChoicePopup(
pollOption: Int,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (text: String) -> Unit,
onError: (title: String, text: String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
@@ -532,7 +522,7 @@ fun FilteredZapAmountChoicePopup(
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(
@@ -33,9 +33,12 @@ class PollNoteViewModel : ViewModel() {
private var pollEvent: PollNoteEvent? = null
private var pollOptions: Map<Int, String>? = null
private var valueMaximum: Int? = null
private var valueMinimum: Int? = null
private var closedAt: Int? = null
private var valueMaximum: Long? = null
private var valueMinimum: Long? = null
private var valueMaximumBD: BigDecimal? = null
private var valueMinimumBD: BigDecimal? = null
private var closedAt: Long? = null
private var consensusThreshold: BigDecimal? = null
private var totalZapped: BigDecimal = BigDecimal.ZERO
@@ -49,10 +52,12 @@ class PollNoteViewModel : ViewModel() {
pollNote = note
pollEvent = pollNote?.event as PollNoteEvent
pollOptions = pollEvent?.pollOptions()
valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM)
valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM)
consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal()
closedAt = pollEvent?.getTagInt(CLOSED_AT)
valueMaximum = pollEvent?.getTagLong(VALUE_MAXIMUM)
valueMinimum = pollEvent?.getTagLong(VALUE_MINIMUM)
valueMinimumBD = valueMinimum?.let { BigDecimal(it) }
valueMaximumBD = valueMaximum?.let { BigDecimal(it) }
consensusThreshold = pollEvent?.getTagLong(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal()
closedAt = pollEvent?.getTagLong(CLOSED_AT)
}
fun refreshTallies() {
@@ -110,6 +115,29 @@ class PollNoteViewModel : ViewModel() {
} catch (e: Exception) { null }
}
fun isValidInputVoteAmount(amount: BigDecimal?): Boolean {
if (amount == null) {
return false
} else if (valueMinimum == null && valueMaximum == null) {
if (amount > BigDecimal.ZERO) {
return true
}
} else if (valueMinimum == null) {
if (amount > BigDecimal.ZERO && amount <= valueMaximumBD!!) {
return true
}
} else if (valueMaximum == null) {
if (amount >= valueMinimumBD!!) {
return true
}
} else {
if ((valueMinimumBD!! <= amount) && (amount <= valueMaximumBD!!)) {
return true
}
}
return false
}
fun isValidInputVoteAmount(amount: Long?): Boolean {
if (amount == null) {
return false
@@ -145,24 +173,29 @@ class PollNoteViewModel : ViewModel() {
private fun zappedPollOptionAmount(option: Int): BigDecimal {
return pollNote?.zaps?.values?.sumOf {
val event = it?.event as? LnZapEvent
if (event?.zappedPollOption() == option) {
event.amount ?: BigDecimal(0)
val zapAmount = event?.amount ?: BigDecimal.ZERO
val isValidAmount = isValidInputVoteAmount(event?.amount)
if (isValidAmount && event?.zappedPollOption() == option) {
zapAmount
} else {
BigDecimal(0)
BigDecimal.ZERO
}
} ?: BigDecimal(0)
} ?: BigDecimal.ZERO
}
private fun totalZapped(): BigDecimal {
return pollNote?.zaps?.values?.sumOf {
val zapEvent = (it?.event as? LnZapEvent)
val zapAmount = zapEvent?.amount ?: BigDecimal.ZERO
val isValidAmount = isValidInputVoteAmount(zapEvent?.amount)
if (zapEvent?.zappedPollOption() != null) {
zapEvent.amount ?: BigDecimal(0)
if (isValidAmount && zapEvent?.zappedPollOption() != null) {
zapAmount
} else {
BigDecimal(0)
BigDecimal.ZERO
}
} ?: BigDecimal(0)
} ?: BigDecimal.ZERO
}
fun createZapOptionsThatMatchThePollingParameters(): List<Long> {
@@ -176,8 +209,8 @@ class PollNoteViewModel : ViewModel() {
}
}
}
valueMinimum?.let { options.add(it.toLong()) }
valueMaximum?.let { options.add(it.toLong()) }
valueMinimum?.let { options.add(it) }
valueMaximum?.let { options.add(it) }
return options.toSet().sorted()
}
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
@@ -29,16 +28,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
@@ -110,7 +109,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigDecimal
@@ -172,7 +170,7 @@ private fun InnerReactionRow(
) {
val (value, elapsed) = measureTimedValue {
Row(verticalAlignment = CenterVertically) {
ReplyReactionWithDialog(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
ReplyReactionWithDialog(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
}
}
Log.d("Rendering Metrics", "Reaction Reply: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
@@ -186,7 +184,7 @@ private fun InnerReactionRow(
Row(verticalAlignment = CenterVertically) {
BoostWithDialog(
baseNote,
MaterialTheme.colors.placeholderText,
MaterialTheme.colorScheme.placeholderText,
accountViewModel,
nav
)
@@ -201,7 +199,7 @@ private fun InnerReactionRow(
) {
val (value, elapsed) = measureTimedValue {
Row(verticalAlignment = CenterVertically) {
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
}
}
Log.d("Rendering Metrics", "Reaction Likes: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
@@ -213,7 +211,7 @@ private fun InnerReactionRow(
) {
val (value, elapsed) = measureTimedValue {
Row(verticalAlignment = CenterVertically) {
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav)
ZapReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav)
}
}
Log.d("Rendering Metrics", "Reaction Zaps: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
@@ -227,8 +225,8 @@ private fun InnerReactionRow(
Row(verticalAlignment = CenterVertically) {
ViewCountReaction(
note = baseNote,
grayTint = MaterialTheme.colors.placeholderText,
viewCountColorFilter = MaterialTheme.colors.placeholderTextColorFilter
grayTint = MaterialTheme.colorScheme.placeholderText,
viewCountColorFilter = MaterialTheme.colorScheme.placeholderTextColorFilter
)
}
}
@@ -285,7 +283,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
val color = if (zapraiserStatus.progress > 0.99) {
DarkerGreen
} else {
MaterialTheme.colors.mediumImportanceLink
MaterialTheme.colorScheme.mediumImportanceLink
}
LinearProgressIndicator(
@@ -310,7 +308,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
Text(
text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserStatus.left),
modifier = NoSoTinyBorders,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
fontSize = Font14SP,
maxLines = 1
)
@@ -389,7 +387,7 @@ private fun ReactionDetailGallery(
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
val defaultBackgroundColor = MaterialTheme.colors.background
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val hasReactions by baseNote.live().hasReactions.observeAsState(
@@ -540,9 +538,6 @@ fun ReplyReaction(
iconSize: Dp = Size17dp,
onPress: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
IconButton(
modifier = remember {
Modifier.size(iconSize)
@@ -554,13 +549,10 @@ fun ReplyReaction(
if (accountViewModel.loggedInWithExternalSigner()) {
onPress()
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_reply),
Toast.LENGTH_SHORT
).show()
}
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_reply
)
}
}
}
@@ -643,9 +635,6 @@ fun BoostReaction(
iconSize: Dp = 20.dp,
onQuotePress: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var wantsToBoost by remember { mutableStateOf(false) }
val iconButtonModifier = remember {
@@ -657,29 +646,22 @@ fun BoostReaction(
onClick = {
if (accountViewModel.isWriteable()) {
if (accountViewModel.hasBoosted(baseNote)) {
scope.launch(Dispatchers.IO) {
accountViewModel.deleteBoostsTo(baseNote)
}
accountViewModel.deleteBoostsTo(baseNote)
} else {
wantsToBoost = true
}
} else {
if (accountViewModel.loggedInWithExternalSigner()) {
if (accountViewModel.hasBoosted(baseNote)) {
scope.launch(Dispatchers.IO) {
accountViewModel.deleteBoostsTo(baseNote)
}
accountViewModel.deleteBoostsTo(baseNote)
} else {
wantsToBoost = true
}
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts),
Toast.LENGTH_SHORT
).show()
}
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_boost_posts
)
}
}
}
@@ -699,9 +681,7 @@ fun BoostReaction(
onQuotePress()
},
onRepost = {
scope.launch(Dispatchers.IO) {
accountViewModel.boost(baseNote)
}
accountViewModel.boost(baseNote)
}
)
}
@@ -741,9 +721,6 @@ fun LikeReaction(
heartSize: Dp = 16.dp,
iconFontSize: TextUnit = Font14SP
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val iconButtonModifier = remember {
Modifier.size(iconSize)
}
@@ -761,21 +738,12 @@ fun LikeReaction(
likeClick(
baseNote,
accountViewModel,
scope,
context,
onMultipleChoices = {
wantsToReact = true
},
onWantsToSignReaction = {
if (accountViewModel.account.reactionChoices.size == 1) {
scope.launch(Dispatchers.IO) {
val reaction = accountViewModel.account.reactionChoices.first()
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
accountViewModel.deleteReactionTo(baseNote, reaction)
} else {
accountViewModel.reactTo(baseNote, reaction)
}
}
accountViewModel.reactToOrDelete(baseNote)
} else if (accountViewModel.account.reactionChoices.size > 1) {
wantsToReact = true
}
@@ -896,44 +864,25 @@ fun LikeText(baseNote: Note, grayTint: Color) {
private fun likeClick(
baseNote: Note,
accountViewModel: AccountViewModel,
scope: CoroutineScope,
context: Context,
onMultipleChoices: () -> Unit,
onWantsToSignReaction: () -> Unit
) {
if (accountViewModel.account.reactionChoices.isEmpty()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.no_reaction_type_setup_long_press_to_change),
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
R.string.no_reactions_setup,
R.string.no_reaction_type_setup_long_press_to_change
)
} else if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
onWantsToSignReaction()
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_like_posts),
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_like_posts
)
}
} else if (accountViewModel.account.reactionChoices.size == 1) {
scope.launch(Dispatchers.IO) {
val reaction = accountViewModel.account.reactionChoices.first()
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
accountViewModel.deleteReactionTo(baseNote, reaction)
} else {
accountViewModel.reactTo(baseNote, reaction)
}
}
accountViewModel.reactToOrDelete(baseNote)
} else if (accountViewModel.account.reactionChoices.size > 1) {
onMultipleChoices()
}
@@ -976,18 +925,19 @@ fun ZapReaction(
zapClick(
baseNote,
accountViewModel,
scope,
context,
onZappingProgress = { progress: Float ->
zappingProgress = progress
scope.launch {
zappingProgress = progress
}
},
onMultipleChoices = {
wantsToZap = true
},
onError = {
onError = { title, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
showErrorMessageDialog = message
}
},
onPayViaIntent = {
@@ -1015,10 +965,10 @@ fun ZapReaction(
wantsToZap = false
wantsToChangeZapAmount = true
},
onError = {
onError = { title, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
showErrorMessageDialog = message
}
},
onProgress = {
@@ -1056,18 +1006,29 @@ fun ZapReaction(
}
if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog(payingInvoices = wantsToPay, accountViewModel = accountViewModel) {
wantsToPay = persistentListOf()
}
PayViaIntentDialog(
payingInvoices = wantsToPay,
accountViewModel = accountViewModel,
onClose = {
wantsToPay = persistentListOf()
},
onError = {
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
}
}
)
}
if (wantsToSetCustomZap) {
ZapCustomDialog(
onClose = { wantsToSetCustomZap = false },
onError = {
onError = { title, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
showErrorMessageDialog = message
}
},
onProgress = {
@@ -1110,33 +1071,22 @@ fun ZapReaction(
private fun zapClick(
baseNote: Note,
accountViewModel: AccountViewModel,
scope: CoroutineScope,
context: Context,
onZappingProgress: (Float) -> Unit,
onMultipleChoices: () -> Unit,
onError: (String) -> Unit,
onError: (String, String) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.no_zap_amount_setup_long_press_to_change),
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
context.getString(R.string.error_dialog_zap_error),
context.getString(R.string.no_zap_amount_setup_long_press_to_change)
)
} else if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
context.getString(R.string.error_dialog_zap_error),
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps)
)
} else if (accountViewModel.account.zapAmountChoices.size == 1) {
accountViewModel.zap(
baseNote,
@@ -1146,9 +1096,7 @@ private fun zapClick(
context,
onError = onError,
onProgress = {
scope.launch(Dispatchers.Main) {
onZappingProgress(it)
}
onZappingProgress(it)
},
zapType = accountViewModel.account.defaultZapType,
onPayViaIntent = onPayViaIntent
@@ -1259,7 +1207,7 @@ private fun DrawViewCount(
.memoryCachePolicy(CachePolicy.ENABLED)
.build()
},
contentDescription = stringResource(R.string.view_count),
contentDescription = context.getString(R.string.view_count),
modifier = iconModifier,
colorFilter = viewCountColorFilter
)
@@ -1278,15 +1226,12 @@ private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel:
onDismissRequest = { onDismiss() }
) {
FlowRow {
val scope = rememberCoroutineScope()
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
if (accountViewModel.isWriteable()) {
scope.launch(Dispatchers.IO) {
accountViewModel.boost(baseNote)
onDismiss()
}
accountViewModel.boost(baseNote)
onDismiss()
} else {
onRepost()
onDismiss()
@@ -1295,7 +1240,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel:
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(stringResource(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
@@ -1307,7 +1252,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel:
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(stringResource(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
@@ -1366,35 +1311,28 @@ private fun ActionableReactionButton(
onChangeAmount: () -> Unit,
toRemove: Set<String>
) {
val scope = rememberCoroutineScope()
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.reactToOrDelete(
baseNote,
reactionType
)
onDismiss()
}
accountViewModel.reactToOrDelete(
baseNote,
reactionType
)
onDismiss()
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
val thisModifier = remember(reactionType) {
Modifier.combinedClickable(
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.reactToOrDelete(
baseNote,
reactionType
)
onDismiss()
}
accountViewModel.reactToOrDelete(
baseNote,
reactionType
)
onDismiss()
},
onLongClick = {
onChangeAmount()
@@ -1466,12 +1404,11 @@ fun ZapAmountChoicePopup(
accountViewModel: AccountViewModel,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (text: String) -> Unit,
onError: (title: String, text: String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapMessage = ""
@@ -1502,7 +1439,7 @@ fun ZapAmountChoicePopup(
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(
@@ -5,11 +5,11 @@ 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.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.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -73,7 +73,7 @@ fun RelayCompose(
Text(
"${relay.counter} ${stringResource(R.string.posts_received)}",
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -117,10 +117,9 @@ fun AddRelayButton(onClick: () -> Unit) {
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(text = stringResource(id = R.string.add), color = Color.White)
@@ -133,10 +132,9 @@ fun RemoveRelayButton(onClick: () -> Unit) {
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.remove), color = Color.White)
@@ -5,11 +5,11 @@ 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.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
@@ -88,7 +88,7 @@ private fun ShowMoreRelaysButton(onClick: () -> Unit) {
imageVector = Icons.Default.ExpandMore,
null,
modifier = ShowMoreRelaysButtonIconModifier,
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
}
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -10,12 +9,12 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -33,10 +32,12 @@ import androidx.compose.ui.unit.Dp
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.RelayBriefInfo
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -47,7 +48,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size15dp
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Composable
public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
@@ -100,7 +100,7 @@ fun ChatRelayExpandButton(onClick: () -> Unit) {
imageVector = Icons.Default.ChevronRight,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.placeholderText
tint = MaterialTheme.colorScheme.placeholderText
)
}
}
@@ -123,9 +123,18 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav:
val context = LocalContext.current
val scope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = Size15dp)
val automaticallyShowProfilePicture = remember {
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
val clickableModifier = remember(relay) {
Modifier
.padding(1.dp)
@@ -148,15 +157,10 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav:
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
}
scope.launch {
Toast
.makeText(
context,
msg,
Toast.LENGTH_SHORT
)
.show()
}
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg
)
}
)
}
@@ -166,13 +170,13 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav:
Box(
modifier = clickableModifier
) {
RenderRelayIcon(relay.favIcon)
RenderRelayIcon(relay.favIcon, automaticallyShowProfilePicture)
}
}
@Composable
fun RenderRelayIcon(iconUrl: String, size: Dp = Size13dp) {
val backgroundColor = MaterialTheme.colors.background
fun RenderRelayIcon(iconUrl: String, loadProfilePicture: Boolean, size: Dp = Size13dp) {
val backgroundColor = MaterialTheme.colorScheme.background
val iconModifier = remember {
Modifier
@@ -186,6 +190,7 @@ fun RenderRelayIcon(iconUrl: String, size: Dp = Size13dp) {
model = iconUrl,
contentDescription = stringResource(id = R.string.relay_icon),
colorFilter = RelayIconFilter,
modifier = iconModifier
modifier = iconModifier,
loadProfilePicture = loadProfilePicture
)
}
@@ -4,9 +4,9 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
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.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -73,7 +73,7 @@ private fun ReplyInformation(
Text(
stringResource(R.string.replying_to),
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
repliesToDisplay.forEachIndexed { idx, user ->
@@ -84,13 +84,13 @@ private fun ReplyInformation(
Text(
", ",
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
} else if (idx < repliesToDisplay.size - 1) {
Text(
stringResource(R.string.and),
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
} else {
@@ -98,25 +98,25 @@ private fun ReplyInformation(
Text(
", ",
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
} else if (idx < repliesToDisplay.size) {
Text(
stringResource(R.string.and),
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
ClickableText(
AnnotatedString("${sortedMentions.size - 2}"),
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.lessImportantLink, fontSize = 13.sp),
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.lessImportantLink, fontSize = 13.sp),
onClick = { expanded = true }
)
Text(
" ${stringResource(R.string.others)}",
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
}
@@ -176,7 +176,7 @@ fun ReplyInformationChannel(
Text(
stringResource(id = R.string.replying_to),
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
mentions.forEachIndexed { idx, user ->
@@ -186,13 +186,13 @@ fun ReplyInformationChannel(
Text(
", ",
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
} else if (idx < mentions.size - 1) {
Text(
" ${stringResource(id = R.string.and)} ",
fontSize = 13.sp,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
}
@@ -213,7 +213,7 @@ private fun ReplyInfoMention(
clickablePart = remember(innerUserState) { "$prefix${innerUserState?.user?.toBestDisplayName()}" },
tags = remember(innerUserState) { innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.lessImportantLink,
color = MaterialTheme.colorScheme.lessImportantLink,
fontSize = 13.sp
),
onClick = { onUserTagClick(user) }
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -19,13 +20,13 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
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.MutableState
@@ -212,7 +213,7 @@ fun UpdateReactionTypeDialog(
placeholder = {
Text(
text = "\uD83D\uDCAF, \uD83C\uDF89, \uD83D\uDC4E",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
singleLine = true,
@@ -225,7 +226,7 @@ fun UpdateReactionTypeDialog(
onClick = { postViewModel.addChoice() },
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(text = stringResource(R.string.add), color = Color.White)
@@ -254,11 +255,12 @@ private fun RenderReactionOption(
modifier = Modifier.padding(horizontal = 3.dp),
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
containerColor = MaterialTheme.colorScheme.primary
),
onClick = {
postViewModel.removeChoice(reactionType)
}
},
contentPadding = PaddingValues(horizontal = 5.dp)
) {
if (reactionType.startsWith(":")) {
val noStartColon = reactionType.removePrefix(":")
@@ -331,7 +333,7 @@ private fun EmojiSelector(accountViewModel: AccountViewModel, nav: (String) -> U
@Composable
fun EmojiCollectionGallery(emojiCollections: List<ATag>, accountViewModel: AccountViewModel, nav: (String) -> Unit, onClick: ((EmojiUrl) -> Unit)? = null) {
val color = MaterialTheme.colors.background
val color = MaterialTheme.colorScheme.background
val bgColor = remember { mutableStateOf(color) }
val listState = rememberLazyListState()

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