Compare commits

...

141 Commits

Author SHA1 Message Date
Vitor Pamplona
2892fa6fca v0.79.7 2023-10-18 18:27:35 -04:00
Vitor Pamplona
27286c7ffe Removing bold titles 2023-10-18 18:26:31 -04:00
Vitor Pamplona
d9b0fbe928 Merge pull request #636 from greenart7c3/main
fix floatingActionButton not hiding when scrolling
2023-10-18 18:11:26 -04:00
Vitor Pamplona
b15f203bc1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-10-18 18:04:18 -04:00
Vitor Pamplona
5d266cca20 - Moves app startup to an IO thread.
- Upgrades Shared Preferences serialization to a Single JSON object
- Simplifies Shared Preferences state changes
- LazyInitializes Video cache
2023-10-18 18:04:07 -04:00
Vitor Pamplona
37d4efd7ba Merge pull request #651 from greenart7c3/test
fix crash when sending giftwraps with amber
2023-10-18 08:22:55 -04:00
greenart7c3
ece8998ba6 fix crash when sending giftwraps with amber 2023-10-18 09:07:08 -03:00
Vitor Pamplona
904a50be1f v0.79.6 2023-10-17 18:30:50 -04:00
Vitor Pamplona
35f6ecad59 Stops PushNotifications when the app is killed. 2023-10-17 18:10:10 -04:00
Vitor Pamplona
2a328ca120 Dump memory states to debug OOO 2023-10-17 18:09:58 -04:00
Vitor Pamplona
1e83dbfbe3 Only changes Username if value changes. 2023-10-17 18:09:39 -04:00
Vitor Pamplona
e703c08634 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-10-17 16:33:21 -04:00
Vitor Pamplona
f8a574375a Updating Jackson 2023-10-17 16:33:16 -04:00
Vitor Pamplona
cfcafe5b2c Merge pull request #650 from believethehype/patch-1
Added #thenostr hashtag icon
2023-10-17 16:15:31 -04:00
believethehype
3ea7b8194d Added #thenostr hashtag icon 2023-10-17 22:13:39 +02:00
Vitor Pamplona
1a7ae33fa8 Activates color for the zaps in the Notification chart 2023-10-17 16:13:28 -04:00
Vitor Pamplona
2bbe126c8d Configuration cache was creating a lot of build issues 2023-10-17 15:39:36 -04:00
Vitor Pamplona
f2e6efe2b7 Improving Git hooks 2023-10-17 15:36:26 -04:00
Vitor Pamplona
404e6cd862 Moves playback services to a package 2023-10-17 15:23:43 -04:00
Vitor Pamplona
4ab04477f8 v0.79.5 2023-10-17 11:14:50 -04:00
Vitor Pamplona
611e8f1a6d Forces a filter reset after authentication 2023-10-16 14:39:05 -04:00
Vitor Pamplona
c8fd810bd3 Adds log of bad signature event. 2023-10-16 14:38:31 -04:00
Vitor Pamplona
c619a9000c Resolving a duplicated entry on relay list in Note. 2023-10-16 12:07:17 -04:00
Vitor Pamplona
3d70614797 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-10-16 12:05:52 -04:00
Vitor Pamplona
c29b4b8e5f Avoids testing the signature when the id or sig fields are blank 2023-10-16 12:05:48 -04:00
Vitor Pamplona
4286b64b41 Improves the feedback to messages from relays. 2023-10-16 11:54:14 -04:00
Vitor Pamplona
e8c50cbf54 Merge pull request #649 from jiftechnify/handle-geohash-error
Add exception handling for parsing geohash
2023-10-16 09:22:21 -04:00
jiftechnify
407807d871 add exception handling for parsing geohash 2023-10-16 16:52:28 +09:00
Vitor Pamplona
202af650de v0.79.4 2023-10-15 18:07:57 -04:00
Vitor Pamplona
e1e42ed500 Only adds a border to video controls when in full screen 2023-10-15 18:07:40 -04:00
Vitor Pamplona
1767cc74a9 Activates images controllers on click 2023-10-15 17:15:48 -04:00
Vitor Pamplona
4028018605 Adds a border in the image dialog to avoid overriding controllers. 2023-10-15 17:07:35 -04:00
Vitor Pamplona
33f8b6d6d8 Trying to fix the image bleed into the previous and next page in the Video feed. 2023-10-15 17:07:02 -04:00
Vitor Pamplona
c738436ddc Merge pull request #647 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-10-15 15:43:21 -04:00
Crowdin Bot
467174ebf8 New Crowdin translations by GitHub Action 2023-10-15 19:42:54 +00:00
Vitor Pamplona
f9fed8a04f Update dependencies 2023-10-15 15:41:24 -04:00
Vitor Pamplona
00194c2f0d Organize imports 2023-10-15 15:35:49 -04:00
Vitor Pamplona
37be1e8c46 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2023-10-14 14:52:24 -04:00
Vitor Pamplona
357f8f6f94 Merge pull request #646 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-10-14 14:51:09 -04:00
Vitor Pamplona
ba4a594a41 Fixes all the other mute list feeds 2023-10-14 14:50:54 -04:00
Crowdin Bot
928a87363d New Crowdin translations by GitHub Action 2023-10-14 18:47:24 +00:00
Vitor Pamplona
1708c48b0c Merge pull request #639 from davotoula/bugfix_mute_feed_is_always_empty
Bugfix mute feed is always empty
2023-10-14 14:46:05 -04:00
Vitor Pamplona
4f4cab6a6c Merge pull request #642 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-10-11 15:34:34 -04:00
Crowdin Bot
e848b58a60 New Crowdin translations by GitHub Action 2023-10-11 19:26:44 +00:00
Vitor Pamplona
5c8f0ad5ce Merge pull request #641 from greenart7c3/double_encryption_fix
fix double encryption on nip04 using amber
2023-10-11 15:25:20 -04:00
greenart7c3
173245d900 fix double encryption on nip04 2023-10-11 15:14:10 -03:00
Vitor Pamplona
dd2e590475 Merge pull request #640 from greenart7c3/encryption_amber
try to fix messages being encrypted twice using amber
2023-10-09 12:14:13 -04:00
greenart7c3
5bc4aab8d8 try to fix messages being encrypted twice 2023-10-09 11:38:29 -03:00
greenart7c3
94308a6098 Merge branch 'vitorpamplona:main' into main 2023-10-09 12:45:42 +00:00
David Kaspar
c7861dfac4 change mute test to equality with string including event 30000 and user hex 2023-10-09 13:53:09 +02:00
David Kaspar
071df0b6d3 change equality test of Mute list to regular expression 2023-10-09 13:13:10 +02:00
David Kaspar
ac0fbc7435 change equality test of Mute list from equals "mute" to ends with mute" 2023-10-09 13:01:32 +02:00
Vitor Pamplona
275051ed30 Fixes visibility of the bottom and top bar when the user comes back to the app. 2023-10-06 16:37:55 -04:00
Vitor Pamplona
3c4cf03684 Merge pull request #638 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2023-10-06 16:37:14 -04:00
Crowdin Bot
2f1c221a7c New Crowdin translations by GitHub Action 2023-10-06 20:29:37 +00:00
Vitor Pamplona
6748abbf70 Fixes failure to load long form content with d tags reserved url chars 2023-10-06 16:27:40 -04:00
Vitor Pamplona
e04a35afed Fixes bug of not immediately loading chat rooms 2023-10-06 16:27:09 -04:00
greenart7c3
f574a1dae5 fix floatingActionButton not hiding 2023-10-06 07:01:49 -03:00
Vitor Pamplona
f67ff43eb3 Moves the Post button on chat screens to a > Button 2023-10-04 09:20:46 -04:00
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 Pamplona
e0dba87286 Merge pull request #632 from atrifat/update-indonesian-translation
Update Indonesian translation
2023-10-03 09:47:45 -04:00
Vitor Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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 Pamplona
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
214 changed files with 6653 additions and 4415 deletions

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jlleitschuh.gradle.ktlint' version "11.5.1"
id 'org.jlleitschuh.gradle.ktlint' version "11.6.1"
id 'com.google.gms.google-services'
}
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 306
versionName "0.78.0"
versionCode 316
versionName "0.79.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -89,7 +89,7 @@ android {
dependencies {
implementation project(path: ':quartz')
implementation "androidx.core:core-ktx:$core_ktx_version"
implementation 'androidx.activity:activity-compose:1.7.2'
implementation 'androidx.activity:activity-compose:1.8.0'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
@@ -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"
@@ -200,10 +200,6 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}
ktlint {
disabledRules.set(["no-wildcard-imports"])
}
// https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds#differing-assetsdexoptbaselineprofm-easy-to-fix
// NB: Android Studio can't find the imports; this does not affect the
// actual build since Gradle can find them just fine.

View File

@@ -108,7 +108,7 @@
tools:replace="screenOrientation" />
<service
android:name=".PlaybackService"
android:name=".service.playback.PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="true"
android:exported="true">

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,15 +4,26 @@ import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import android.util.Log
import androidx.media3.common.util.UnstableApi
import coil.ImageLoader
import com.vitorpamplona.amethyst.service.playback.VideoCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.time.measureTimedValue
@UnstableApi class Amethyst : Application() {
val videoCache: VideoCache by lazy {
val newCache = VideoCache()
newCache.initFileCache(instance)
newCache
}
class Amethyst : Application() {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onCreate() {
super.onCreate()
instance = this
VideoCache.initFileCache(instance)
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
@@ -33,6 +44,20 @@ class Amethyst : Application() {
.build()
)
}
GlobalScope.launch(Dispatchers.IO) {
val (value, elapsed) = measureTimedValue {
// initializes the video cache in a thread
videoCache
}
Log.d("Rendering Metrics", "VideoCache initialized in $elapsed")
}
}
fun imageLoaderBuilder(): ImageLoader.Builder {
return ImageLoader.Builder(applicationContext).diskCache {
SingletonDiskCache.get(applicationContext)
}
}
companion object {

View File

@@ -6,6 +6,7 @@ import androidx.security.crypto.MasterKey
object EncryptedStorage {
private const val PREFERENCES_NAME = "secret_keeper"
// returns the preferences for each account or a global file if null.
fun prefsFileName(npub: String? = null): String {
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
}

View File

@@ -17,15 +17,20 @@ 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.ThemeType
import com.vitorpamplona.amethyst.model.parseBooleanType
import com.vitorpamplona.amethyst.model.parseConnectivityType
import com.vitorpamplona.amethyst.model.parseThemeType
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LnZapEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
@@ -80,21 +85,26 @@ private object PrefKeys {
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars"
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
const val SHARED_SETTINGS = "shared_settings"
}
object LocalPreferences {
private const val comma = ","
private var _currentAccount: String? = null
private var _savedAccounts: List<AccountInfo>? = null
fun currentAccount(): String? {
suspend fun currentAccount(): String? {
if (_currentAccount == null) {
_currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
}
return _currentAccount
}
private fun updateCurrentAccount(npub: String) {
private suspend fun updateCurrentAccount(npub: String) {
if (_currentAccount != npub) {
_currentAccount = npub
@@ -104,22 +114,41 @@ object LocalPreferences {
}
}
private var _savedAccounts: List<String>? = null
private fun savedAccounts(): List<String> {
private fun savedAccounts(): List<AccountInfo> {
if (_savedAccounts == null) {
_savedAccounts = encryptedPreferences()
.getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf()
with(encryptedPreferences()) {
val newSystemOfAccounts = getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let {
Event.mapper.readValue<List<AccountInfo>>(it)
}
if (newSystemOfAccounts != null && newSystemOfAccounts.isNotEmpty()) {
_savedAccounts = newSystemOfAccounts
} else {
val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf()
val migrated = oldAccounts.map { npub ->
AccountInfo(
npub,
encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false),
(encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank()
)
}
println("AAA migrated: $migrated")
_savedAccounts = migrated
}
}
}
return _savedAccounts!!
}
private fun updateSavedAccounts(accounts: List<String>) {
private suspend fun updateSavedAccounts(accounts: List<AccountInfo>) = withContext(Dispatchers.IO) {
if (_savedAccounts != accounts) {
_savedAccounts = accounts
encryptedPreferences().edit().apply {
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null })
putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(accounts))
}.apply()
}
}
@@ -127,7 +156,7 @@ object LocalPreferences {
private val prefsDirPath: String
get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/"
private fun addAccount(npub: String) {
private suspend fun addAccount(npub: AccountInfo) {
val accounts = savedAccounts().toMutableList()
if (npub !in accounts) {
accounts.add(npub)
@@ -135,22 +164,27 @@ object LocalPreferences {
}
}
private fun setCurrentAccount(account: Account) {
private suspend fun setCurrentAccount(account: Account) = withContext(Dispatchers.IO) {
val npub = account.userProfile().pubkeyNpub()
val accInfo = AccountInfo(
npub,
account.isWriteable(),
account.loginWithExternalSigner
)
updateCurrentAccount(npub)
addAccount(npub)
addAccount(accInfo)
}
fun switchToAccount(npub: String) {
updateCurrentAccount(npub)
suspend fun switchToAccount(accountInfo: AccountInfo) = withContext(Dispatchers.IO) {
updateCurrentAccount(accountInfo.npub)
}
/**
* Removes the account from the app level shared preferences
*/
private fun removeAccount(npub: String) {
private suspend fun removeAccount(accountInfo: AccountInfo) {
val accounts = savedAccounts().toMutableList()
if (accounts.remove(npub)) {
if (accounts.remove(accountInfo)) {
updateSavedAccounts(accounts)
}
}
@@ -159,6 +193,8 @@ object LocalPreferences {
* Deletes the npub-specific shared preference file
*/
private fun deleteUserPreferenceFile(npub: String) {
checkNotInMainThread()
val prefsDir = File(prefsDirPath)
prefsDir.list()?.forEach {
if (it.contains(npub)) {
@@ -168,6 +204,8 @@ object LocalPreferences {
}
private fun encryptedPreferences(npub: String? = null): SharedPreferences {
checkNotInMainThread()
return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
val preferenceFile = if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
@@ -185,40 +223,31 @@ object LocalPreferences {
* condition and the file will probably not be deleted
*/
@SuppressLint("ApplySharedPref")
fun updatePrefsForLogout(npub: String) {
val userPrefs = encryptedPreferences(npub)
suspend fun updatePrefsForLogout(accountInfo: AccountInfo) = withContext(Dispatchers.IO) {
val userPrefs = encryptedPreferences(accountInfo.npub)
userPrefs.edit().clear().commit()
removeAccount(npub)
deleteUserPreferenceFile(npub)
removeAccount(accountInfo)
deleteUserPreferenceFile(accountInfo.npub)
if (savedAccounts().isEmpty()) {
val appPrefs = encryptedPreferences()
appPrefs.edit().clear().apply()
} else if (currentAccount() == npub) {
updateCurrentAccount(savedAccounts().elementAt(0))
encryptedPreferences().edit().clear().apply()
} else if (currentAccount() == accountInfo.npub) {
updateCurrentAccount(savedAccounts().elementAt(0).npub)
}
}
fun updatePrefsForLogin(account: Account) {
suspend fun updatePrefsForLogin(account: Account) {
setCurrentAccount(account)
saveToEncryptedStorage(account)
}
fun allSavedAccounts(): List<AccountInfo> {
return savedAccounts().map { npub ->
AccountInfo(
npub,
hasPrivKey(npub),
getLoggedInWithExternalSigner(npub)
)
}
return savedAccounts()
}
fun allLocalAccountNPubs(): Set<String> {
return savedAccounts().toSet()
}
suspend fun saveToEncryptedStorage(account: Account) = withContext(Dispatchers.IO) {
checkNotInMainThread()
fun saveToEncryptedStorage(account: Account) {
val prefs = encryptedPreferences(account.userProfile().pubkeyNpub())
prefs.edit().apply {
account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
@@ -256,65 +285,102 @@ object LocalPreferences {
}
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.loginWithExternalSigner)
}.apply()
val globalPrefs = encryptedPreferences()
globalPrefs.edit().apply {
if (account.settings.automaticallyShowImages.prefCode == null) {
remove(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, account.settings.automaticallyShowImages.prefCode!!)
}
if (account.settings.automaticallyStartPlayback.prefCode == null) {
remove(PrefKeys.AUTOMATICALLY_START_PLAYBACK)
} else {
putBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, account.settings.automaticallyStartPlayback.prefCode!!)
}
if (account.settings.automaticallyShowUrlPreview.prefCode == null) {
remove(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)
} 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!!)
}
putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "")
}.apply()
}
fun updateTheme(theme: Int) {
encryptedPreferences().edit().apply {
putInt(PrefKeys.THEME, theme)
}.apply()
}
fun getTheme(): Int {
return encryptedPreferences().getInt(PrefKeys.THEME, 0)
}
fun getPreferredLanguage(): String {
return encryptedPreferences().getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
}
private fun getLoggedInWithExternalSigner(npub: String): Boolean {
return encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
}
private fun hasPrivKey(npub: String): Boolean {
return (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank()
}
fun loadFromEncryptedStorage(): Account? {
val acc = loadFromEncryptedStorage(currentAccount())
suspend fun loadCurrentAccountFromEncryptedStorage(): Account? {
val acc = loadCurrentAccountFromEncryptedStorage(currentAccount())
acc?.registerObservers()
return acc
}
fun loadFromEncryptedStorage(npub: String?): Account? {
encryptedPreferences(npub).apply {
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
suspend fun migrateOldSharedSettings(): Settings? {
val prefs = encryptedPreferences()
loadOldSharedSettings(prefs)?.let {
saveSharedSettings(it, prefs)
return it
}
return null
}
suspend fun saveSharedSettings(sharedSettings: Settings, prefs: SharedPreferences = encryptedPreferences()) {
with(prefs.edit()) {
putString(PrefKeys.SHARED_SETTINGS, Event.mapper.writeValueAsString(sharedSettings))
apply()
}
}
suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? {
with(prefs) {
return try {
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let {
Event.mapper.readValue<Settings>(it)
}
} catch (e: Throwable) {
Log.w("LocalPreferences", "Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}", e)
e.printStackTrace()
null
}
}
}
@Deprecated("Turned into a single JSON object")
suspend fun loadOldSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? {
with(prefs) {
if (!contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) {
return null
}
val automaticallyShowImages = if (contains(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)) {
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false))
} else {
ConnectivityType.ALWAYS
}
val automaticallyStartPlayback = if (contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) {
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false))
} else {
ConnectivityType.ALWAYS
}
val automaticallyShowUrlPreview = if (contains(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)) {
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false))
} else {
ConnectivityType.ALWAYS
}
val automaticallyHideNavigationBars = if (contains(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)) {
parseBooleanType(getBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, false))
} else {
BooleanType.ALWAYS
}
val automaticallyShowProfilePictures = if (contains(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE)) {
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE, false))
} else {
ConnectivityType.ALWAYS
}
val themeType = if (contains(PrefKeys.THEME)) {
parseThemeType(getInt(PrefKeys.THEME, ThemeType.SYSTEM.screenCode))
} else {
ThemeType.SYSTEM
}
return Settings(
themeType,
getString(PrefKeys.PREFERRED_LANGUAGE, null)?.ifBlank { null },
automaticallyShowImages,
automaticallyStartPlayback,
automaticallyShowUrlPreview,
automaticallyHideNavigationBars,
automaticallyShowProfilePictures
)
}
}
suspend fun loadCurrentAccountFromEncryptedStorage(npub: String?): Account? = withContext(Dispatchers.IO) {
checkNotInMainThread()
return@withContext with(encryptedPreferences(npub)) {
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf()
@@ -407,34 +473,7 @@ object LocalPreferences {
mapOf()
}
val settings = Settings()
encryptedPreferences().apply {
settings.automaticallyShowImages = if (contains(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)) {
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false))
} else {
ConnectivityType.ALWAYS
}
settings.automaticallyStartPlayback = if (contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) {
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false))
} else {
ConnectivityType.ALWAYS
}
settings.automaticallyShowUrlPreview = if (contains(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)) {
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false))
} else {
ConnectivityType.ALWAYS
}
settings.automaticallyHideNavigationBars = if (contains(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)) {
parseBooleanType(getBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, false))
} else {
BooleanType.ALWAYS
}
settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "")
}
val a = Account(
return@with Account(
keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
followingChannels = followingChannels,
followingCommunities = followingCommunities,
@@ -462,11 +501,8 @@ object LocalPreferences {
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,
settings = settings,
loginWithExternalSigner = loginWithExternalSigner
)
return a
}
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.os.Build
import android.util.Log
import coil.Coil
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
@@ -33,6 +32,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 {
@@ -40,14 +43,14 @@ object ServiceManager {
private var isStarted: Boolean = false // to not open amber in a loop trying to use auth relays and registering for notifications
private var account: Account? = null
fun start(account: Account, context: Context) {
fun start(account: Account) {
this.account = account
ExternalSignerUtils.account = account
start(context)
start()
}
@Synchronized
fun start(context: Context) {
fun start() {
if (isStarted && account != null) {
return
}
@@ -59,7 +62,7 @@ object ServiceManager {
HttpClient.start(account)
OptOutFromFilters.start(account?.warnAboutPostsWithReports ?: true, account?.filterSpamFromStrangers ?: true)
Coil.setImageLoader {
ImageLoader.Builder(context).components {
Amethyst.instance.imageLoaderBuilder().components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
@@ -67,7 +70,6 @@ object ServiceManager {
}
add(SvgDecoder.Factory())
}.logger(DebugLogger())
.diskCache { SingletonDiskCache.get(context.applicationContext) }
.okHttpClient { HttpClient.getHttpClient() }
.respectCacheHeaders(false)
.build()
@@ -87,9 +89,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()
@@ -131,10 +136,14 @@ object ServiceManager {
fun trimMemory() {
LocalCache.cleanObservers()
val accounts = LocalPreferences.allLocalAccountNPubs().mapNotNull { decodePublicKeyAsHexOrNull(it) }.toSet()
val accounts = LocalPreferences.allSavedAccounts().mapNotNull {
decodePublicKeyAsHexOrNull(it.npub)
}.toSet()
account?.let {
LocalCache.pruneOldAndHiddenMessages(it)
NostrChatroomDataSource.clearEOSEs(it)
LocalCache.pruneHiddenMessages(it)
LocalCache.pruneContactLists(accounts)
LocalCache.pruneRepliesAndReactions(accounts)
@@ -143,10 +152,10 @@ object ServiceManager {
}
}
fun restartIfDifferentAccount(account: Account, context: Context) {
fun restartIfDifferentAccount(account: Account) {
if (this.account != account) {
pause()
start(account, context)
start(account)
}
}
}

View File

@@ -25,7 +25,47 @@ import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.Contact
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.EmojiUrl
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GeneralListEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.Gossip
import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent
import com.vitorpamplona.quartz.events.IdentityClaim
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.NIP24Factory
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RelayAuthEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.Response
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WrappedEvent
import com.vitorpamplona.quartz.events.ZapSplitSetup
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
@@ -33,6 +73,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.net.Proxy
import java.util.Locale
@@ -90,7 +131,6 @@ class Account(
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true,
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
var settings: Settings = Settings(),
var loginWithExternalSigner: Boolean = false
) {
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
@@ -98,7 +138,6 @@ 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
@@ -169,38 +208,6 @@ class Account(
var userProfileCache: User? = null
fun updateAutomaticallyStartPlayback(
automaticallyStartPlayback: ConnectivityType
) {
settings.automaticallyStartPlayback = automaticallyStartPlayback
live.invalidateData()
saveable.invalidateData()
}
fun updateAutomaticallyShowUrlPreview(
automaticallyShowUrlPreview: ConnectivityType
) {
settings.automaticallyShowUrlPreview = automaticallyShowUrlPreview
live.invalidateData()
saveable.invalidateData()
}
fun updateAutomaticallyHideHavBars(
automaticallyHideHavBars: BooleanType
) {
settings.automaticallyHideNavigationBars = automaticallyHideHavBars
live.invalidateData()
saveable.invalidateData()
}
fun updateAutomaticallyShowImages(
automaticallyShowImages: ConnectivityType
) {
settings.automaticallyShowImages = automaticallyShowImages
live.invalidateData()
saveable.invalidateData()
}
fun updateOptOutOptions(warnReports: Boolean, filterSpam: Boolean) {
warnAboutPostsWithReports = warnReports
filterSpamFromStrangers = filterSpam
@@ -359,7 +366,7 @@ class Account(
)
}
broadcastPrivately(giftWraps)
broadcastPrivately(NIP24Factory.Result(senderReaction, giftWraps))
} else {
val giftWraps = NIP24Factory().createReactionWithinGroup(
emojiUrl = emojiUrl,
@@ -412,7 +419,7 @@ class Account(
recipientPubKey = it
)
broadcastPrivately(listOf(giftWraps))
broadcastPrivately(NIP24Factory.Result(senderReaction, listOf(giftWraps)))
}
} else {
val giftWraps = NIP24Factory().createReactionWithinGroup(
@@ -888,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)
@@ -1057,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)
@@ -1615,7 +1622,7 @@ class Account(
event = sealedEvent,
recipientPubKey = it
)
broadcastPrivately(listOf(giftWraps))
broadcastPrivately(NIP24Factory.Result(chatMessageEvent, listOf(giftWraps)))
}
}
} else {
@@ -1636,42 +1643,54 @@ class Account(
}
}
fun broadcastPrivately(signedEvents: List<GiftWrapEvent>) {
signedEvents.forEach {
Client.send(it)
fun broadcastPrivately(signedEvents: NIP24Factory.Result) {
val mine = signedEvents.wraps.filter {
(it.recipientPubKey() == keyPair.pubKey.toHexKey())
}
mine.forEach {
// Only keep in cache the GiftWrap for the account.
if (it.recipientPubKey() == keyPair.pubKey.toHexKey()) {
if (loginWithExternalSigner) {
ExternalSignerUtils.decrypt(it.content, it.pubKey, it.id, SignerType.NIP44_DECRYPT)
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[it.id] ?: ""
if (decryptedContent.isEmpty()) return
it.cachedGift(keyPair.pubKey, decryptedContent)?.let { cached ->
if (cached is SealedGossipEvent) {
ExternalSignerUtils.decrypt(cached.content, cached.pubKey, cached.id, SignerType.NIP44_DECRYPT)
val localDecryptedContent = ExternalSignerUtils.cachedDecryptedContent[cached.id] ?: ""
if (localDecryptedContent.isEmpty()) return
cached.cachedGossip(keyPair.pubKey, localDecryptedContent)?.let { gossip ->
LocalCache.justConsume(gossip, null)
}
} else {
LocalCache.justConsume(it, null)
}
}
} else {
it.cachedGift(keyPair.privKey!!)?.let {
if (it is SealedGossipEvent) {
it.cachedGossip(keyPair.privKey!!)?.let {
LocalCache.justConsume(it, null)
}
} else {
LocalCache.justConsume(it, null)
if (loginWithExternalSigner) {
ExternalSignerUtils.decrypt(it.content, it.pubKey, it.id, SignerType.NIP44_DECRYPT)
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[it.id] ?: ""
if (decryptedContent.isEmpty()) return
it.cachedGift(keyPair.pubKey, decryptedContent)?.let { cached ->
if (cached is SealedGossipEvent) {
ExternalSignerUtils.decrypt(cached.content, cached.pubKey, cached.id, SignerType.NIP44_DECRYPT)
val localDecryptedContent = ExternalSignerUtils.cachedDecryptedContent[cached.id] ?: ""
if (localDecryptedContent.isEmpty()) return
cached.cachedGossip(keyPair.pubKey, localDecryptedContent)?.let { gossip ->
LocalCache.justConsume(gossip, null)
}
} else {
LocalCache.justConsume(it, null)
}
}
} else {
it.cachedGift(keyPair.privKey!!)?.let {
if (it is SealedGossipEvent) {
it.cachedGossip(keyPair.privKey!!)?.let {
LocalCache.justConsume(it, null)
}
} else {
LocalCache.justConsume(it, null)
}
}
LocalCache.consume(it, null)
}
LocalCache.consume(it, null)
}
val id = mine.firstOrNull()?.id
val mineNote = if (id == null) null else LocalCache.getNoteIfExists(id)
signedEvents.wraps.forEach {
// Creates an alias
if (mineNote != null && it.recipientPubKey() != keyPair.pubKey.toHexKey()) {
LocalCache.getOrAddAliasNote(it.id, mineNote)
}
Client.send(it)
}
}
@@ -2947,7 +2966,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
@@ -3102,12 +3124,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
}
}
@@ -3115,7 +3139,7 @@ class Account(
return lastReadPerRoute[route] ?: 0
}
fun registerObservers() {
suspend fun registerObservers() = withContext(Dispatchers.Main) {
// Observes relays to restart connections
userProfile().live().relays.observeForever {
GlobalScope.launch(Dispatchers.IO) {

View File

@@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.R
fun checkForHashtagWithIcon(tag: String, primary: Color): HashtagIcon? {
return when (tag.lowercase()) {
"bitcoin", "btc", "timechain", "bitcoiner", "bitcoiners" -> HashtagIcon(R.drawable.ht_btc, "Bitcoin", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 0.dp))
"nostr", "nostrich", "nostriches" -> HashtagIcon(R.drawable.ht_nostr, "Nostr", Color.Unspecified, Modifier.padding(1.dp, 2.dp, 0.dp, 0.dp))
"nostr", "nostrich", "nostriches", "thenostr" -> HashtagIcon(R.drawable.ht_nostr, "Nostr", Color.Unspecified, Modifier.padding(1.dp, 2.dp, 0.dp, 0.dp))
"lightning", "lightningnetwork" -> HashtagIcon(R.drawable.ht_lightning, "Lightning", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
"zap", "zaps", "zapper", "zappers", "zapping", "zapped", "zapathon", "zapraiser", "zaplife", "zapchain" -> HashtagIcon(R.drawable.zap, "Zap", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
"amethyst" -> HashtagIcon(R.drawable.amethyst, "Amethyst", Color.Unspecified, Modifier.padding(3.dp, 2.dp, 0.dp, 0.dp))

View File

@@ -3,8 +3,6 @@ package com.vitorpamplona.amethyst.model
import android.util.Log
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledInsert
@@ -13,15 +11,71 @@ import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.HexValidator
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AppRecommendationEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BaseAddressableEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.CalendarDateSlotEvent
import com.vitorpamplona.quartz.events.CalendarEvent
import com.vitorpamplona.quartz.events.CalendarRSVPEvent
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelHideMessageEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChannelMuteUserEvent
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.NNSEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RecommendRelayEvent
import com.vitorpamplona.quartz.events.RelaySetEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import java.io.File
@@ -93,7 +147,15 @@ object LocalCache {
val noteEvent = note.event
return if (noteEvent is AddressableEvent) {
// upgrade to the latest
checkGetOrCreateAddressableNote(noteEvent.address().toTag())
val newNote = checkGetOrCreateAddressableNote(noteEvent.address().toTag())
if (newNote != null && noteEvent is Event && newNote.event == null) {
val author = note.author ?: getOrCreateUser(noteEvent.pubKey)
newNote.loadEvent(noteEvent as Event, author, emptyList())
note.moveAllReferencesTo(newNote)
}
newNote
} else {
note
}
@@ -101,6 +163,18 @@ object LocalCache {
return null
}
fun getOrAddAliasNote(idHex: String, note: Note): Note {
checkNotInMainThread()
return notes.get(idHex) ?: run {
require(isValidHex(idHex)) {
"$idHex is not a valid hex"
}
notes.putIfAbsent(idHex, note) ?: note
}
}
fun getOrCreateNote(idHex: String): Note {
checkNotInMainThread()
@@ -209,14 +283,6 @@ object LocalCache {
val user = getOrCreateUser(event.pubKey)
if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) {
if (event.dTag() == "bookmark") {
val loggedInUser = LocalPreferences.currentAccount()
val hexKey = loggedInUser?.bechToBytes()
if (hexKey != null) {
val pubKey = Hex.encode(hexKey)
if (pubKey == event.pubKey) {
ExternalSignerUtils.content.remove(event.id)
}
}
user.updateBookmark(event)
}
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
@@ -348,7 +414,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)
}
@@ -1447,7 +1518,7 @@ object LocalCache {
try {
event.checkSignature()
} catch (e: Exception) {
Log.w("Event failed retest ${event.kind}", e.message ?: "")
Log.w("Event failed retest ${event.kind}", (e.message ?: "") + event.toJson())
}
false
} else {
@@ -1548,7 +1619,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.

View File

@@ -20,7 +20,25 @@ import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PayInvoiceSuccessResponse
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.WrappedEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -399,9 +417,16 @@ open class Note(val idHex: String) {
}
}
@Synchronized
fun addRelaySync(url: String) {
if (url !in relays) {
relays = relays + url
}
}
fun addRelay(relay: Relay) {
if (relay.url !in relays) {
relays = relays + relay.url
addRelaySync(relay.url)
liveSet?.innerRelays?.invalidateData()
}
}

View File

@@ -4,15 +4,34 @@ import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.R
@Stable
class Settings(
var preferredLanguage: String? = null,
var automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
var automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS,
var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS,
var automaticallyHideNavigationBars: BooleanType = BooleanType.ALWAYS
data class Settings(
val theme: ThemeType = ThemeType.SYSTEM,
val preferredLanguage: String? = null,
val automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
val automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS,
val automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS,
val automaticallyHideNavigationBars: BooleanType = BooleanType.ALWAYS,
val automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS
)
enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) {
enum class ThemeType(val screenCode: Int, val resourceId: Int) {
SYSTEM(0, R.string.system),
LIGHT(1, R.string.light),
DARK(2, R.string.dark)
}
fun parseThemeType(code: Int?): ThemeType {
return when (code) {
ThemeType.SYSTEM.screenCode -> ThemeType.SYSTEM
ThemeType.LIGHT.screenCode -> ThemeType.LIGHT
ThemeType.DARK.screenCode -> ThemeType.DARK
else -> {
ThemeType.SYSTEM
}
}
}
enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val resourceId: Int) {
ALWAYS(null, 0, R.string.connectivity_type_always),
WIFI_ONLY(true, 1, R.string.connectivity_type_wifi_only),
NEVER(false, 2, R.string.connectivity_type_never)

View File

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

View File

@@ -156,40 +156,39 @@ object ExternalSignerUtils {
checkNotInMainThread()
val result = getDataFromResolver(SignerType.SIGN_EVENT, arrayOf(event.toJson(), event.pubKey()), columnName)
if (result !== null) {
if (result == null) {
ServiceManager.shouldPauseService = false
isActivityRunning = true
openSigner(
event.toJson(),
SignerType.SIGN_EVENT,
activityResultLauncher,
"",
event.id()
)
while (isActivityRunning) {
Thread.sleep(100)
}
} else {
content.put(event.id(), result)
return
}
ServiceManager.shouldPauseService = false
isActivityRunning = true
openSigner(
event.toJson(),
SignerType.SIGN_EVENT,
activityResultLauncher,
"",
event.id()
)
while (isActivityRunning) {
Thread.sleep(100)
}
}
fun decryptBlockList(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
if (result == null) {
isActivityRunning = true
openSigner(
encryptedContent,
signerType,
blockListResultLauncher,
pubKey,
id
)
} else {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
isActivityRunning = true
openSigner(
encryptedContent,
signerType,
blockListResultLauncher,
pubKey,
id
)
}
fun getDataFromResolver(signerType: SignerType, data: Array<out String>, columnName: String = "signature"): String? {
@@ -206,15 +205,16 @@ object ExternalSignerUtils {
null,
null
).use {
if (it !== null) {
if (it.moveToFirst()) {
val index = it.getColumnIndex(columnName)
if (index < 0) {
Log.d("getDataFromResolver", "column '$columnName' not found")
return null
}
return it.getString(index)
if (it == null) {
return null
}
if (it.moveToFirst()) {
val index = it.getColumnIndex(columnName)
if (index < 0) {
Log.d("getDataFromResolver", "column '$columnName' not found")
return null
}
return it.getString(index)
}
}
return null
@@ -222,90 +222,90 @@ object ExternalSignerUtils {
fun decrypt(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
if (result == null) {
isActivityRunning = true
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
while (isActivityRunning) {
// do nothing
}
} else {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
isActivityRunning = true
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
while (isActivityRunning) {
// do nothing
}
}
fun decryptDM(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
if (result == null) {
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
} else {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
}
fun decryptBookmark(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
if (result !== null) {
if (result == null) {
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
} else {
content.put(id, result)
cachedDecryptedContent[id] = result
return
}
openSigner(
encryptedContent,
signerType,
decryptResultLauncher,
pubKey,
id
)
}
fun encrypt(decryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_ENCRYPT) {
content.remove(id)
cachedDecryptedContent.remove(id)
val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey))
if (result !== null) {
if (result == null) {
isActivityRunning = true
openSigner(
decryptedContent,
signerType,
activityResultLauncher,
pubKey,
id
)
while (isActivityRunning) {
Thread.sleep(100)
}
} else {
content.put(id, result)
return
}
isActivityRunning = true
openSigner(
decryptedContent,
signerType,
activityResultLauncher,
pubKey,
id
)
while (isActivityRunning) {
Thread.sleep(100)
}
}
fun decryptZapEvent(event: LnZapRequestEvent) {
val result = getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey))
if (result !== null) {
if (result == null) {
openSigner(
event.toJson(),
SignerType.DECRYPT_ZAP_EVENT,
decryptResultLauncher,
event.pubKey,
event.id
)
} else {
content.put(event.id, result)
cachedDecryptedContent[event.id] = result
return
}
openSigner(
event.toJson(),
SignerType.DECRYPT_ZAP_EVENT,
decryptResultLauncher,
event.pubKey,
event.id
)
}
}

View File

@@ -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
@@ -16,6 +17,7 @@ import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LnZapEvent
@@ -34,6 +36,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
fun createAccountContactListFilter(): TypedFilter {
return TypedFilter(
@@ -141,7 +144,13 @@ 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
invalidateFilters()
}
}
override fun consume(event: Event, relay: Relay) {
@@ -200,19 +209,51 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
}
}
override fun markAsSeenOnRelay(eventId: String, relay: Relay) {
super.markAsSeenOnRelay(eventId, relay)
val note = LocalCache.getNoteIfExists(eventId) ?: return
val privKey = account.keyPair.privKey ?: return
val noteEvent = note.event ?: return
markInnerAsSeenOnRelay(noteEvent, privKey, relay)
}
private fun markInnerAsSeenOnRelay(noteEvent: EventInterface, privKey: ByteArray, relay: Relay) {
LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay)
if (noteEvent is GiftWrapEvent) {
val gift = noteEvent.cachedGift(privKey) ?: return
markInnerAsSeenOnRelay(gift, privKey, relay)
} else if (noteEvent is SealedGossipEvent) {
val rumor = noteEvent.cachedGossip(privKey) ?: return
markInnerAsSeenOnRelay(rumor, privKey, relay)
}
}
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) {

View File

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

View File

@@ -15,7 +15,6 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.Error
abstract class NostrDataSource(val debugName: String) {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

View File

@@ -7,7 +7,24 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.NNSEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
private var searchString: String? = null

View File

@@ -6,7 +6,15 @@ import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
private var eventsToWatch = setOf<Note>()

View File

@@ -4,7 +4,23 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.AppRecommendationEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
var user: User? = null

View File

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

View File

@@ -1,15 +0,0 @@
package com.vitorpamplona.amethyst.service.connectivitystatus
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
object ConnectivityStatus {
private val onMobileData = mutableStateOf(false)
val isOnMobileData: MutableState<Boolean> = onMobileData
fun updateConnectivityStatus(isOnMobileData: Boolean, isOnWifi: Boolean) {
if (onMobileData.value != isOnMobileData) {
onMobileData.value = isOnMobileData
}
}
}

View File

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

View File

@@ -33,9 +33,10 @@ class EventNotificationConsumer(private val applicationContext: Context) {
// PushNotification Wraps don't include a receiver.
// Test with all logged in accounts
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null && (acc.keyPair.privKey != null || acc.loginWithExternalSigner)) {
consumeIfMatchesAccount(event, acc)
if (it.hasPrivKey || it.loggedInWithExternalSigner) {
LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc ->
consumeIfMatchesAccount(event, acc)
}
}
}
}
@@ -43,6 +44,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(
@@ -71,9 +73,6 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
} else if (key != null) {
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
LocalCache.justConsume(notificationEvent, null)
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->

View File

@@ -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
@@ -17,13 +18,16 @@ class RegisterAccounts(
) {
// creates proof that it controls all accounts
private fun signEventsToProveControlOfAccounts(
private suspend fun signEventsToProveControlOfAccounts(
accounts: List<AccountInfo>,
notificationToken: String
): List<RelayAuthEvent> {
return accounts.mapNotNull {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null && acc.isWriteable()) {
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
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
}
}

View File

@@ -1,4 +1,4 @@
package com.vitorpamplona.amethyst
package com.vitorpamplona.amethyst.service.playback
import android.app.PendingIntent
import android.content.Context
@@ -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)
}
}
}
}

View File

@@ -1,4 +1,4 @@
package com.vitorpamplona.amethyst
package com.vitorpamplona.amethyst.service.playback
import android.content.ComponentName
import android.content.Context

View File

@@ -1,4 +1,4 @@
package com.vitorpamplona.amethyst
package com.vitorpamplona.amethyst.service.playback
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
@@ -8,6 +8,7 @@ import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.service.HttpClient
@UnstableApi // Extend MediaSessionService
@@ -23,7 +24,9 @@ class PlaybackService : MediaSessionService() {
}
fun newProgressiveDataSource(): MediaSource.Factory {
return ProgressiveMediaSource.Factory(VideoCache.get(Amethyst.instance, HttpClient.getHttpClient()))
return ProgressiveMediaSource.Factory(
(applicationContext as Amethyst).videoCache.get(HttpClient.getHttpClient())
)
}
fun lazyHlsDS(): MultiPlayerPlaybackManager {

View File

@@ -1,4 +1,4 @@
package com.vitorpamplona.amethyst
package com.vitorpamplona.amethyst.service.playback
import android.content.Context
import androidx.media3.common.util.UnstableApi
@@ -10,9 +10,9 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
import okhttp3.OkHttpClient
import java.io.File
@UnstableApi object VideoCache {
@UnstableApi class VideoCache {
var exoPlayerCacheSize: Long = 90 * 1024 * 1024 // 90MB
var exoPlayerCacheSize: Long = 150 * 1024 * 1024 // 90MB
var leastRecentlyUsedCacheEvictor = LeastRecentlyUsedCacheEvictor(exoPlayerCacheSize)
@@ -42,11 +42,7 @@ import java.io.File
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
}
fun get(context: Context, client: OkHttpClient): CacheDataSource.Factory {
if (!this::simpleCache.isInitialized) {
initFileCache(context)
}
fun get(client: OkHttpClient): CacheDataSource.Factory {
// Renews the factory because OkHttpMight have changed.
renewCacheFactory(client)

View File

@@ -1,4 +1,4 @@
package com.vitorpamplona.amethyst
package com.vitorpamplona.amethyst.service.playback
import android.util.LruCache

View File

@@ -1,8 +1,9 @@
package com.vitorpamplona.amethyst.service.previews
import android.net.Uri
import kotlinx.coroutines.*
import java.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Locale
class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
private val imageExtensionArray = arrayOf(".gif", ".png", ".jpg", ".jpeg", ".bmp", ".webp")

View File

@@ -6,7 +6,6 @@ import kotlinx.coroutines.withContext
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.lang.IllegalArgumentException
private const val ELEMENT_TAG_META = "meta"
private const val ATTRIBUTE_VALUE_PROPERTY = "property"

View File

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

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.relays
import android.util.Log
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.RelayAuthEvent
@@ -60,6 +61,8 @@ class Relay(
var afterEOSE = false
val authResponse = mutableMapOf<HexKey, Boolean>()
fun register(listener: Listener) {
listeners = listeners.plus(listener)
}
@@ -77,9 +80,7 @@ class Relay(
checkNotInMainThread()
// Sends everything.
Client.allSubscriptions().forEach {
sendFilter(requestId = it)
}
renewFilters()
}
}
@@ -224,8 +225,20 @@ class Relay(
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
}
"OK" -> listeners.forEach {
Log.w("Relay", "Relay on OK $url, ${msgArray[1].asText()}, ${msgArray[2].asBoolean()}, ${msgArray[3].asText()}")
it.onSendResponse(this@Relay, msgArray[1].asText(), msgArray[2].asBoolean(), msgArray[3].asText())
val eventId = msgArray[1].asText()
val sucess = msgArray[2].asBoolean()
val message = msgArray[3].asText()
if (authResponse.containsKey(eventId)) {
val wasAlreadyAuthenticated = authResponse.get(eventId)
authResponse.put(eventId, sucess)
if (wasAlreadyAuthenticated != true && sucess) {
renewFilters()
}
}
Log.w("Relay", "Relay on OK $url, $eventId, $sucess, $message")
it.onSendResponse(this@Relay, eventId, sucess, message)
}
"AUTH" -> listeners.forEach {
// Log.w("Relay", "Relay$url, ${msg[1].asString}")
@@ -290,10 +303,18 @@ class Relay(
}
}
fun renewFilters() {
// Force update all filters after AUTH.
Client.allSubscriptions().forEach {
sendFilter(requestId = it)
}
}
fun send(signedEvent: EventInterface) {
checkNotInMainThread()
if (signedEvent is RelayAuthEvent) {
authResponse.put(signedEvent.id, false)
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)

View File

@@ -14,17 +14,16 @@ import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
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.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting
import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex
@@ -33,7 +32,7 @@ import com.vitorpamplona.amethyst.ui.navigation.debugState
import com.vitorpamplona.amethyst.ui.note.Nip47
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.events.ChannelCreateEvent
@@ -50,46 +49,38 @@ import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class MainActivity : AppCompatActivity() {
private val isOnMobileDataState = mutableStateOf(false)
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
ExternalSignerUtils.start(this)
super.onCreate(savedInstanceState)
val language = LocalPreferences.getPreferredLanguage()
if (language.isNotBlank()) {
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(language)
AppCompatDelegate.setApplicationLocales(appLocale)
}
setContent {
val themeViewModel: ThemeViewModel = viewModel()
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
themeViewModel.onChange(LocalPreferences.getTheme())
AmethystTheme(themeViewModel) {
LaunchedEffect(key1 = sharedPreferencesViewModel, isOnMobileDataState) {
sharedPreferencesViewModel.init()
sharedPreferencesViewModel.updateConnectivityStatusState(isOnMobileDataState)
}
AmethystTheme(sharedPreferencesViewModel) {
// 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)
val accountStateViewModel: AccountStateViewModel = viewModel()
LaunchedEffect(key1 = Unit) {
accountStateViewModel.tryLoginExistingAccountAsync()
}
AccountScreen(accountStateViewModel, themeViewModel)
AccountScreen(accountStateViewModel, sharedPreferencesViewModel)
}
}
}
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
val connectivityManager =
getSystemService(ConnectivityManager::class.java) as ConnectivityManager
connectivityManager.requestNetwork(networkRequest, networkCallback)
}
@OptIn(DelicateCoroutinesApi::class)
@@ -102,13 +93,22 @@ class MainActivity : AppCompatActivity() {
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
if (ServiceManager.shouldPauseService) {
ServiceManager.start(this@MainActivity)
ServiceManager.start()
}
}
GlobalScope.launch(Dispatchers.IO) {
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
}
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
.registerNetworkCallback(networkRequest, networkCallback)
}
override fun onPause() {
@@ -120,6 +120,10 @@ class MainActivity : AppCompatActivity() {
if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
}
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
.unregisterNetworkCallback(networkCallback)
super.onPause()
}
@@ -153,7 +157,7 @@ class MainActivity : AppCompatActivity() {
GlobalScope.launch(Dispatchers.IO) {
if (ServiceManager.shouldPauseService) {
ServiceManager.pause()
ServiceManager.start(this@MainActivity)
ServiceManager.start()
}
}
}
@@ -166,15 +170,10 @@ class MainActivity : AppCompatActivity() {
super.onCapabilitiesChanged(network, networkCapabilities)
GlobalScope.launch(Dispatchers.IO) {
val hasMobileData =
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
ConnectivityStatus.updateConnectivityStatus(
hasMobileData,
hasWifi
)
val isOnMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $isOnMobileData")
isOnMobileDataState.value = isOnMobileData
}
}

View File

@@ -11,7 +11,10 @@ import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClient
import okhttp3.*
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import okio.BufferedSource
import okio.IOException
import okio.buffer

View File

@@ -10,8 +10,13 @@ import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.ServersAvailable
import com.vitorpamplona.amethyst.service.HttpClient
import okhttp3.*
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import okio.BufferedSink
import okio.IOException
import okio.source

View File

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

View File

@@ -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
@@ -67,8 +67,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchBarViewModel
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.ChatroomKey
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@@ -133,7 +131,7 @@ fun JoinUserOrChannelView(searchBarViewModel: SearchBarViewModel, onClose: () ->
Text(
text = "",
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
fontWeight = FontWeight.Bold
)
}
@@ -270,7 +268,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 = {
@@ -302,7 +300,10 @@ private fun RenderSearchResults(
if (searchBarViewModel.isSearching) {
val users by searchBarViewModel.searchResultsUsers.collectAsState()
val channels by searchBarViewModel.searchResultsChannels.collectAsState()
val scope = rememberCoroutineScope()
val automaticallyShowProfilePicture = remember {
accountViewModel.settings.showProfilePictures.value
}
Row(
modifier = Modifier
@@ -323,10 +324,8 @@ private fun RenderSearchResults(
key = { _, item -> "u" + item.pubkeyHex }
) { _, item ->
UserComposeForChat(item, accountViewModel) {
scope.launch(Dispatchers.IO) {
val withKey = ChatroomKey(persistentSetOf(item.pubkeyHex))
accountViewModel.userProfile().createChatroom(withKey)
nav("Room/${withKey.hashCode()}")
accountViewModel.createChatRoomFor(item) {
nav("Room/$it")
}
searchBarViewModel.clear()
@@ -337,7 +336,7 @@ private fun RenderSearchResults(
channels,
key = { _, item -> "c" + item.idHex }
) { _, item ->
RenderChannel(item) {
RenderChannel(item, automaticallyShowProfilePicture) {
nav("Channel/${item.idHex}")
searchBarViewModel.clear()
}
@@ -350,6 +349,7 @@ private fun RenderSearchResults(
@Composable
private fun RenderChannel(
item: com.vitorpamplona.amethyst.model.Channel,
loadProfilePicture: Boolean,
onClick: () -> Unit
) {
val hasNewMessages = remember {
@@ -368,7 +368,8 @@ private fun RenderChannel(
channelLastTime = null,
channelLastContent = item.summary(),
hasNewMessages,
onClick = onClick
onClick = onClick,
loadProfilePicture = loadProfilePicture
)
}
@@ -426,7 +427,7 @@ private fun DisplayUserAboutInfo(baseUser: User) {
Text(
text = about,
color = MaterialTheme.colors.placeholderText,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)

View File

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

View File

@@ -9,11 +9,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.ServersAvailable
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,7 +24,7 @@ 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)

View File

@@ -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
)
}
}
@@ -292,7 +292,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(

View File

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

View File

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

View File

@@ -1,10 +1,14 @@
package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Row
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.Icon
import androidx.compose.material3.IconButton
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
@@ -36,13 +40,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(

View File

@@ -3,8 +3,13 @@ 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.runtime.*
import androidx.compose.material3.LocalTextStyle
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.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -33,13 +38,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 +55,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 +73,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)
)
}

View File

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

View File

@@ -7,13 +7,11 @@ 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
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
@@ -26,13 +24,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 +46,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 +68,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 +86,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)
)
}

View File

@@ -9,9 +9,26 @@ import android.util.Size
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
@@ -19,7 +36,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 +48,24 @@ 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.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.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -46,7 +80,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
@@ -83,7 +116,15 @@ 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.components.BechLink
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.components.UrlPreview
import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.components.ZapRaiserRequest
import com.vitorpamplona.amethyst.ui.components.imageExtensions
import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.amethyst.ui.components.videoExtensions
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.CancelIcon
import com.vitorpamplona.amethyst.ui.note.CloseIcon
@@ -102,6 +143,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@@ -120,6 +162,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewPostView(
onClose: () -> Unit,
@@ -188,9 +231,7 @@ fun NewPostView(
TopAppBar(
title = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(end = 10.dp),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -207,28 +248,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 ->
@@ -249,7 +299,6 @@ fun NewPostView(
) {
Column(
modifier = Modifier
.padding(top = 10.dp)
.imePadding()
.weight(1f)
) {
@@ -270,7 +319,7 @@ fun NewPostView(
makeItShort = true,
unPackReply = false,
isQuotedNote = true,
modifier = MaterialTheme.colors.replyModifier,
modifier = MaterialTheme.colorScheme.replyModifier,
accountViewModel = accountViewModel,
nav = nav
)
@@ -374,6 +423,9 @@ fun NewPostView(
},
onClose = {
postViewModel.wantsInvoice = false
},
onError = { title, message ->
accountViewModel.toast(title, message)
}
)
}
@@ -406,7 +458,7 @@ fun NewPostView(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
)
)
@@ -416,7 +468,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)
}
@@ -463,7 +515,7 @@ fun NewPostView(
) {
UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
tint = MaterialTheme.colors.onBackground,
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
) {
postViewModel.selectImage(it)
@@ -527,22 +579,22 @@ 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(
painterResource(id = android.R.drawable.ic_input_add),
contentDescription = "Add poll option button",
modifier = Modifier.size(18.dp)
modifier = Size18Modifier
)
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun MessageField(
postViewModel: NewPostViewModel
@@ -569,7 +621,7 @@ private fun MessageField(
.fillMaxWidth()
.border(
width = 1.dp,
color = MaterialTheme.colors.surface,
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(8.dp)
)
.focusRequester(focusRequester)
@@ -581,15 +633,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)
)
}
@@ -640,7 +691,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)
)
}
@@ -670,18 +721,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
)
)
}
@@ -707,18 +756,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
)
)
}
@@ -772,7 +819,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)
)
@@ -817,12 +864,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)
)
@@ -870,7 +917,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
imageVector = Icons.Default.LocationOn,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.primary
)
}
@@ -890,7 +937,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)
)
}
@@ -951,7 +998,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)
)
@@ -965,7 +1012,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)
@@ -1024,7 +1071,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,
@@ -1032,7 +1079,7 @@ private fun AddZapraiserButton(
modifier = Modifier
.size(13.dp)
.align(Alignment.BottomEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1068,14 +1115,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
)
}
}
@@ -1096,7 +1143,7 @@ private fun AddLnInvoiceButton(
imageVector = Icons.Default.CurrencyBitcoin,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1131,7 +1178,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,
@@ -1139,7 +1186,7 @@ private fun ForwardZapTo(
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1185,7 +1232,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,
@@ -1193,7 +1240,7 @@ private fun MarkAsSensitive(
modifier = Modifier
.size(10.dp)
.align(Alignment.TopEnd),
tint = MaterialTheme.colors.onBackground
tint = MaterialTheme.colorScheme.onBackground
)
} else {
Icon(
@@ -1219,15 +1266,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()
}
@@ -1237,17 +1278,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)
}
@@ -1256,19 +1291,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))
}
}
@@ -1282,10 +1309,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)
}
@@ -1333,7 +1359,7 @@ fun ImageVideoDescription(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder
)
) {
@@ -1474,7 +1500,7 @@ fun ImageVideoDescription(
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -1493,7 +1519,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)
@@ -1527,21 +1553,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
)
}

View File

@@ -14,7 +14,11 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ServersAvailable
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.LocationUtil
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
@@ -33,6 +37,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 +63,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

View File

@@ -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
@@ -81,6 +83,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 +138,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 +214,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 +224,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 +234,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 +244,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 +262,7 @@ fun ServerConfigHeader() {
@Composable
fun ServerConfigPreview() {
ServerConfigClickableLine(
loadProfilePicture = true,
item = RelaySetupInfo(
url = "nostr.mom",
read = true,
@@ -297,7 +302,6 @@ fun ServerConfig(
nav: (String) -> Unit
) {
var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) }
val scope = rememberCoroutineScope()
val context = LocalContext.current
relayInfo?.let {
@@ -310,8 +314,13 @@ fun ServerConfig(
)
}
val automaticallyShowProfilePicture = remember {
accountViewModel.settings.showProfilePictures.value
}
ServerConfigClickableLine(
item = item,
loadProfilePicture = automaticallyShowProfilePicture,
onToggleDownload = onToggleDownload,
onToggleUpload = onToggleUpload,
onToggleFollows = onToggleFollows,
@@ -334,15 +343,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
)
}
)
}
@@ -352,6 +356,7 @@ fun ServerConfig(
@Composable
fun ServerConfigClickableLine(
item: RelaySetupInfo,
loadProfilePicture: Boolean,
onToggleDownload: (RelaySetupInfo) -> Unit,
onToggleUpload: (RelaySetupInfo) -> Unit,
onToggleFollows: (RelaySetupInfo) -> Unit,
@@ -368,7 +373,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 +446,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 +459,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -477,9 +482,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 +495,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -512,7 +517,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 +525,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
Icon(
@@ -542,7 +547,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 +555,7 @@ private fun RenderStatusRow(
maxLines = 1,
fontSize = 12.sp,
modifier = modifier,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
}
@@ -571,7 +576,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 +606,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 +639,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 +672,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 +705,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 +739,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 +772,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 +808,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 +822,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 +833,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 +851,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)

View File

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

View File

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

View File

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

View File

@@ -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
@@ -47,6 +48,10 @@ fun RelayInformationDialog(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val automaticallyShowProfilePicture = remember {
accountViewModel.settings.showProfilePictures.value
}
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
@@ -77,6 +82,7 @@ fun RelayInformationDialog(
Column() {
RenderRelayIcon(
relayBriefInfo.favIcon,
automaticallyShowProfilePicture,
Size55dp
)
}
@@ -111,7 +117,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))

View File

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

View File

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

View File

@@ -9,12 +9,22 @@ import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
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.runtime.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
@@ -144,7 +154,7 @@ fun LoadingAnimation(
),
progress = 1f,
strokeWidth = 1.dp,
color = MaterialTheme.colors.background // Set background color
color = MaterialTheme.colorScheme.background // Set background color
)
}

View File

@@ -1,15 +1,21 @@
package com.vitorpamplona.amethyst.ui.buttons
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
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.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.runtime.*
import androidx.compose.material3.FloatingActionButton
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
@@ -20,6 +26,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 +53,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 +69,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 +86,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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,16 +6,24 @@ 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.runtime.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.coerceIn
import androidx.compose.ui.unit.dp
@@ -55,7 +63,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) }

View File

@@ -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,8 +9,20 @@ 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.runtime.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
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
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
@@ -76,7 +87,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 +144,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 +182,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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,22 @@
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.runtime.*
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
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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 +46,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)
)
}

View File

@@ -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
@@ -97,7 +97,7 @@ fun ExpandableRichTextViewer(
.fillMaxWidth()
.background(getGradient(backgroundColor))
) {
ShowMoreButton() {
ShowMoreButton {
showFullText = !showFullText
}
}
@@ -112,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)
) {

View File

@@ -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,8 +7,19 @@ 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.runtime.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
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
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -23,8 +31,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 +76,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 +138,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)

View File

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

View File

@@ -1,20 +1,31 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
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.runtime.*
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.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -23,16 +34,19 @@ import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
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
@@ -57,6 +71,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
@@ -137,7 +152,7 @@ private fun RenderRegular(
val textStyle = currentTextStyle.copy(
lineHeight = 1.4.em,
color = currentTextStyle.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
LocalContentColor.current
}
)
@@ -333,8 +348,8 @@ private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<
}
ProvideTextStyle(MarkdownTextStyle) {
MaterialRichText(
style = MaterialTheme.colors.markdownStyle
Material3RichText(
style = MaterialTheme.colorScheme.markdownStyle
) {
RefreshableContent(content, tags, accountViewModel) {
Markdown(
@@ -404,7 +419,7 @@ private fun ObserveNIP19Event(
if (baseNote == null) {
LaunchedEffect(key1 = it.hex) {
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
accountViewModel.checkGetOrCreateNote(it.hex)?.let { note ->
accountViewModel.checkGetOrCreateNote(it.hex) { note ->
launch(Dispatchers.Main) { baseNote = note }
}
}
@@ -461,30 +476,14 @@ private fun ObserveUser(user: User, onRefresh: () -> Unit) {
}
}
@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
}
}
}
@@ -504,7 +503,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 {
@@ -527,7 +526,7 @@ private fun DisplayFullNote(
NoteCompose(
baseNote = it,
accountViewModel = accountViewModel,
modifier = MaterialTheme.colors.replyModifier,
modifier = MaterialTheme.colorScheme.replyModifier,
parentBackgroundColor = backgroundColor,
isQuotedNote = true,
nav = nav
@@ -556,12 +555,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}")
}
@@ -573,7 +572,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]")
@@ -672,7 +671,7 @@ private fun DisplayNoteFromTag(
NoteCompose(
baseNote = baseNote,
accountViewModel = accountViewModel,
modifier = MaterialTheme.colors.innerPostModifier,
modifier = MaterialTheme.colorScheme.innerPostModifier,
parentBackgroundColor = backgroundColor,
isQuotedNote = true,
nav = nav

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,19 +8,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) {
val automaticallyShowUrlPreview = remember {
when (accountViewModel.account.settings.automaticallyShowUrlPreview) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
accountViewModel.settings.showUrlPreview.value
}
if (!automaticallyShowUrlPreview) {

View File

@@ -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,13 @@ 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
@@ -31,11 +31,7 @@ fun UrlPreviewCard(
accountViewModel: AccountViewModel
) {
val automaticallyShowUrlPreview = remember {
when (accountViewModel.account.settings.automaticallyShowUrlPreview) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
accountViewModel.settings.showUrlPreview.value
}
if (!automaticallyShowUrlPreview) {
@@ -44,7 +40,7 @@ fun UrlPreviewCard(
val uri = LocalUriHandler.current
Row(
modifier = MaterialTheme.colors.innerPostModifier
modifier = MaterialTheme.colorScheme.innerPostModifier
.clickable {
runCatching { uri.openUri(url) }
}
@@ -57,12 +53,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 +66,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)
}
}
}

View File

@@ -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,13 +21,14 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -43,11 +44,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isFinite
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -58,12 +63,8 @@ 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
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.playback.PlaybackClientController
import com.vitorpamplona.amethyst.ui.note.LyricsIcon
import com.vitorpamplona.amethyst.ui.note.LyricsOffIcon
import com.vitorpamplona.amethyst.ui.note.MuteIcon
@@ -97,24 +98,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) {
@@ -152,29 +152,33 @@ fun VideoView(
title: String? = null,
thumb: VideoThumb? = null,
roundedCorner: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified,
waveform: ImmutableList<Int>? = null,
artworkUri: String? = null,
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,
topPaddingForControllers = topPaddingForControllers,
waveform = waveform,
artworkUri = artworkUri,
authorName = authorName,
nostrUriCallback = nostrUriCallback,
alwaysShowVideo = alwaysShowVideo,
accountViewModel = accountViewModel,
onControllerVisibilityChanged = onControllerVisibilityChanged,
onDialog = onDialog
)
}
@@ -186,54 +190,52 @@ fun VideoViewInner(
title: String? = null,
thumb: VideoThumb? = null,
roundedCorner: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified,
waveform: ImmutableList<Int>? = null,
artworkUri: String? = null,
authorName: String? = null,
nostrUriCallback: String? = null,
alwaysShowVideo: Boolean = false,
accountViewModel: AccountViewModel,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
onDialog: ((Boolean) -> Unit)? = null
) {
val automaticallyStartPlayback = remember {
mutableStateOf(
if (alwaysShowVideo) { true } else {
when (accountViewModel.account.settings.automaticallyStartPlayback) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
}
}
mutableStateOf<Boolean>(
if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback.value
)
}
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 {
null
}
} catch (e: Exception) {
null
}
)
.build()
)
.build()
}
if (!automaticallyStartPlayback.value) {
ImageUrlWithDownloadButton(url = videoUri, showImage = automaticallyStartPlayback)
} else {
VideoPlayerActiveMutex(videoUri) { activeOnScreen ->
val mediaItem = remember(videoUri) {
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
}
)
.build()
)
.build()
)
}
GetVideoController(
mediaItem = mediaItem,
videoUri = videoUri,
@@ -244,10 +246,12 @@ fun VideoViewInner(
controller = controller,
thumbData = thumb,
roundedCorner = roundedCorner,
topPaddingForControllers = topPaddingForControllers,
waveform = waveform,
keepPlaying = keepPlaying,
automaticallyStartPlayback = automaticallyStartPlayback,
activeOnScreen = activeOnScreen,
onControllerVisibilityChanged = onControllerVisibilityChanged,
onDialog = onDialog
)
}
@@ -258,7 +262,7 @@ fun VideoViewInner(
@Composable
@OptIn(UnstableApi::class)
fun GetVideoController(
mediaItem: MediaItem,
mediaItem: MutableState<MediaItem>,
videoUri: String,
defaultToStart: Boolean = false,
nostrUriCallback: String? = null,
@@ -288,14 +292,16 @@ fun GetVideoController(
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.
scope.launch(Dispatchers.IO) {
if (controller.value == null) {
if (controller.value == null) {
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
@@ -311,7 +317,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
@@ -329,27 +335,27 @@ fun GetVideoController(
it.volume = if (defaultToStart) 0f else 1f
}
it.setMediaItem(mediaItem)
it.setMediaItem(mediaItem.value)
it.prepare()
}
}
}
}
} else {
controller.value?.let {
if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) {
if (it.isPlaying) {
// There is a video playing, start this one on mute.
it.volume = 0f
} else {
// There is no other video playing. Use the default mute state to
// decide if sound is on or not.
it.volume = if (defaultToStart) 0f else 1f
}
it.setMediaItem(mediaItem)
it.prepare()
}
} else {
controller.value?.let {
if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) {
if (it.isPlaying) {
// There is a video playing, start this one on mute.
it.volume = 0f
} else {
// There is no other video playing. Use the default mute state to
// decide if sound is on or not.
it.volume = if (defaultToStart) 0f else 1f
}
it.setMediaItem(mediaItem.value)
it.prepare()
}
}
}
@@ -379,6 +385,8 @@ fun GetVideoController(
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
@@ -394,7 +402,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
@@ -460,7 +468,7 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Bo
}
}
Box(
val myModifier = remember(videoUri) {
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 70.dp)
@@ -489,7 +497,9 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Bo
}
}
}
) {
}
Box(modifier = myModifier) {
inner(active)
}
}
@@ -505,96 +515,148 @@ private fun RenderVideoPlayer(
controller: MediaController,
thumbData: VideoThumb?,
roundedCorner: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified,
waveform: ImmutableList<Int>? = null,
keepPlaying: MutableState<Boolean>,
automaticallyStartPlayback: MutableState<Boolean>,
automaticallyStartPlayback: State<Boolean>,
activeOnScreen: MutableState<Boolean>,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
onDialog: ((Boolean) -> Unit)?
) {
val context = LocalContext.current
ControlWhenPlayerIsActive(controller, keepPlaying, automaticallyStartPlayback, activeOnScreen)
val controllerVisible = remember(controller) {
mutableStateOf(false)
}
BoxWithConstraints() {
AndroidView(
modifier = if (roundedCorner) {
MaterialTheme.colors.imageModifier
.defaultMinSize(minHeight = 100.dp)
.align(Alignment.Center)
} else {
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 100.dp)
.align(Alignment.Center)
},
factory = {
PlayerView(context).apply {
player = controller
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
controllerAutoShow = false
thumbData?.thumb?.let { defaultArtwork = it }
hideController()
resizeMode =
if (maxHeight.isFinite) AspectRatioFrameLayout.RESIZE_MODE_FIT else AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
onDialog?.let { innerOnDialog ->
setFullscreenButtonClickListener {
controller.pause()
innerOnDialog(it)
}
}
setControllerVisibilityListener(
PlayerView.ControllerVisibilityListener {
controllerVisible.value = it == View.VISIBLE
}
)
val parentVideoPlaybackSize = remember {
mutableStateOf<IntSize>(IntSize.Zero)
}
val videoPlaybackSize = remember {
mutableStateOf<IntSize>(IntSize.Zero)
}
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize().onSizeChanged {
parentVideoPlaybackSize.value = it
}
) {
BoxWithConstraints(
modifier = Modifier.onSizeChanged {
videoPlaybackSize.value = it
}
) {
val borders = MaterialTheme.colorScheme.imageModifier
val myModifier = remember {
if (roundedCorner) {
borders
.defaultMinSize(minHeight = 100.dp)
.align(Alignment.Center)
} else {
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 100.dp)
.align(Alignment.Center)
}
}
)
waveform?.let {
Waveform(it, controller, Modifier.align(Alignment.Center))
}
val startingMuteState = remember(controller) {
controller.volume < 0.001
}
MuteButton(controllerVisible, startingMuteState) { mute: Boolean ->
// makes the new setting the default for new creations.
DefaultMutedSetting.value = mute
// if the user unmutes a video and it's not the current playing, switches to that one.
if (!mute && keepPlayingMutex != null && keepPlayingMutex != controller) {
keepPlayingMutex?.stop()
keepPlayingMutex?.release()
keepPlayingMutex = null
val factory = remember(controller) {
{ context: Context ->
PlayerView(context).apply {
player = controller
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
controllerAutoShow = false
thumbData?.thumb?.let { defaultArtwork = it }
hideController()
resizeMode =
if (maxHeight.isFinite) AspectRatioFrameLayout.RESIZE_MODE_FIT else AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
onDialog?.let { innerOnDialog ->
setFullscreenButtonClickListener {
controller.pause()
innerOnDialog(it)
}
}
setControllerVisibilityListener(
PlayerView.ControllerVisibilityListener { visible ->
controllerVisible.value = visible == View.VISIBLE
onControllerVisibilityChanged?.let { callback ->
callback(visible == View.VISIBLE)
}
}
)
}
}
}
controller.volume = if (mute) 0f else 1f
}
AndroidView(
modifier = myModifier,
factory = factory
)
KeepPlayingButton(keepPlaying, controllerVisible, 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) {
waveform?.let {
Waveform(it, controller, remember { Modifier.align(Alignment.Center) })
}
val startingMuteState = remember(controller) {
controller.volume < 0.001
}
val spaceModifier =
if (topPaddingForControllers.isSpecified && videoPlaybackSize.value.height > 0) {
val space = (abs(parentVideoPlaybackSize.value.height - videoPlaybackSize.value.height) / 2).dp
if (space > topPaddingForControllers) {
Modifier
} else {
Modifier.padding(top = topPaddingForControllers - space)
}
} else {
Modifier
}
MuteButton(
controllerVisible,
startingMuteState,
spaceModifier
) { mute: Boolean ->
// makes the new setting the default for new creations.
DefaultMutedSetting.value = mute
// if the user unmutes a video and it's not the current playing, switches to that one.
if (!mute && keepPlayingMutex != null && keepPlayingMutex != controller) {
keepPlayingMutex?.stop()
keepPlayingMutex?.release()
}
keepPlayingMutex = controller
} else {
if (keepPlayingMutex == controller) {
keepPlayingMutex = null
}
controller.volume = if (mute) 0f else 1f
}
keepPlaying.value = newKeepPlaying
KeepPlayingButton(
keepPlaying,
controllerVisible,
spaceModifier.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) {
keepPlayingMutex?.stop()
keepPlayingMutex?.release()
}
keepPlayingMutex = controller
} else {
if (keepPlayingMutex == controller) {
keepPlayingMutex = null
}
}
keepPlaying.value = newKeepPlaying
}
}
}
}
@@ -666,32 +728,34 @@ fun DrawWaveform(waveform: ImmutableList<Int>, waveformProgress: MutableState<Fl
fun ControlWhenPlayerIsActive(
controller: Player,
keepPlaying: MutableState<Boolean>,
automaticallyStartPlayback: MutableState<Boolean>,
automaticallyStartPlayback: State<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()
}
}
@@ -752,6 +816,7 @@ fun LayoutCoordinates.getDistanceToVertCenterIfVisible(view: View): Float? {
private fun MuteButton(
controllerVisible: MutableState<Boolean>,
startingMuteState: Boolean,
modifier: Modifier,
toggle: (Boolean) -> Unit
) {
val holdOn = remember {
@@ -771,6 +836,7 @@ private fun MuteButton(
AnimatedVisibility(
visible = holdOn.value || controllerVisible.value,
modifier = modifier,
enter = fadeIn(),
exit = fadeOut()
) {
@@ -780,7 +846,7 @@ private fun MuteButton(
.clip(CircleShape)
.fillMaxSize(0.6f)
.align(Alignment.Center)
.background(MaterialTheme.colors.background)
.background(MaterialTheme.colorScheme.background)
)
IconButton(
@@ -821,7 +887,7 @@ private fun KeepPlayingButton(
.clip(CircleShape)
.fillMaxSize(0.6f)
.align(Alignment.Center)
.background(MaterialTheme.colors.background)
.background(MaterialTheme.colorScheme.background)
)
IconButton(
@@ -832,9 +898,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)
}
}
}

View File

@@ -5,14 +5,12 @@ 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
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -60,7 +58,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 +82,7 @@ fun ZapRaiserRequest(
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colorScheme.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(

View File

@@ -6,16 +6,20 @@ 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
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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 +33,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 +52,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
@@ -64,8 +65,10 @@ import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isFinite
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
@@ -75,10 +78,9 @@ import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.imageLoader
import com.vitorpamplona.amethyst.R
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 +88,15 @@ 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.Size55dp
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 +214,7 @@ fun ZoomableContentView(
}
var mainImageModifier = if (roundedCorner) {
MaterialTheme.colors.imageModifier
MaterialTheme.colorScheme.imageModifier
} else {
Modifier.fillMaxWidth()
}
@@ -234,7 +236,7 @@ fun ZoomableContentView(
}
when (content) {
is ZoomableUrlImage -> UrlImageView(content, mainImageModifier, accountViewModel)
is ZoomableUrlImage -> UrlImageView(content, mainImageModifier, accountViewModel = accountViewModel)
is ZoomableUrlVideo -> VideoView(
videoUri = content.url,
title = content.description,
@@ -246,7 +248,7 @@ fun ZoomableContentView(
accountViewModel = accountViewModel
)
is ZoomableLocalImage -> LocalImageView(content, mainImageModifier, accountViewModel)
is ZoomableLocalImage -> LocalImageView(content, mainImageModifier, accountViewModel = accountViewModel)
is ZoomableLocalVideo ->
content.localFile?.let {
VideoView(
@@ -271,21 +273,15 @@ fun ZoomableContentView(
private fun LocalImageView(
content: ZoomableLocalImage,
mainImageModifier: Modifier,
accountViewModel: AccountViewModel?,
topPaddingForControllers: Dp = Dp.Unspecified,
accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false
) {
if (content.localFile != null && content.localFile.exists()) {
BoxWithConstraints(contentAlignment = Alignment.Center) {
val showImage = remember {
mutableStateOf(
if (alwayShowImage) { true } else {
when (accountViewModel?.account?.settings?.automaticallyShowImages) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
else -> true
}
}
if (alwayShowImage) true else accountViewModel.settings.showImages.value
)
}
@@ -304,7 +300,11 @@ private fun LocalImageView(
if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
}
val verifierModifier = Modifier.align(Alignment.TopEnd)
val verifierModifier = if (topPaddingForControllers.isSpecified) {
Modifier.padding(top = topPaddingForControllers).align(Alignment.TopEnd)
} else {
Modifier.align(Alignment.TopEnd)
}
val painterState = remember {
mutableStateOf<AsyncImagePainter.State?>(null)
@@ -340,20 +340,14 @@ private fun LocalImageView(
private fun UrlImageView(
content: ZoomableUrlImage,
mainImageModifier: Modifier,
accountViewModel: AccountViewModel?,
topPaddingForControllers: Dp = Dp.Unspecified,
accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false
) {
BoxWithConstraints(contentAlignment = Alignment.Center) {
val showImage = remember {
mutableStateOf(
if (alwayShowImage) { true } else {
when (accountViewModel?.account?.settings?.automaticallyShowImages) {
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
ConnectivityType.NEVER -> false
ConnectivityType.ALWAYS -> true
else -> true
}
}
mutableStateOf<Boolean>(
if (alwayShowImage) true else accountViewModel.settings.showImages.value
)
}
@@ -361,18 +355,24 @@ private fun UrlImageView(
mainImageModifier
.widthIn(max = maxWidth)
.heightIn(max = maxHeight)
/* Is this necessary? It makes images bleed into other pages
.run {
aspectRatio(content.dim)?.let { ratio ->
this.aspectRatio(ratio, false)
} ?: this
}
*/
}
val contentScale = remember {
if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
}
val verifierModifier = Modifier.align(Alignment.TopEnd)
val verifierModifier = if (topPaddingForControllers.isSpecified) {
Modifier.padding(top = topPaddingForControllers).align(Alignment.TopEnd)
} else {
Modifier.align(Alignment.TopEnd)
}
val painterState = remember {
mutableStateOf<AsyncImagePainter.State?>(null)
@@ -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]")
@@ -637,49 +637,97 @@ 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,
topPaddingForControllers = Size55dp,
onControllerVisibilityChanged = {
controllerVisible.value = it
},
onToggleControllerVisibility = {
controllerVisible.value = !controllerVisible.value
},
accountViewModel = accountViewModel
)
}
} else {
RenderImageOrVideo(
content = imageUrl,
roundedCorner = false,
topPaddingForControllers = Size55dp,
onControllerVisibilityChanged = {
controllerVisible.value = it
},
onToggleControllerVisibility = {
controllerVisible.value = !controllerVisible.value
},
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 +739,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,32 +767,68 @@ 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) {
val mainModifier = Modifier
.fillMaxSize()
.zoomable(rememberZoomState())
private fun RenderImageOrVideo(
content: ZoomableContent,
roundedCorner: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
onToggleControllerVisibility: (() -> Unit)? = null,
accountViewModel: AccountViewModel
) {
if (content is ZoomableUrlImage) {
UrlImageView(content = content, mainImageModifier = mainModifier, accountViewModel, alwayShowImage = true)
val mainModifier = Modifier
.fillMaxSize()
.zoomable(rememberZoomState())
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null // Avoid flashing on click
) {
if (onToggleControllerVisibility != null) {
onToggleControllerVisibility()
}
}
UrlImageView(
content = content,
mainImageModifier = mainModifier,
topPaddingForControllers = topPaddingForControllers,
accountViewModel,
alwayShowImage = true
)
} else if (content is ZoomableUrlVideo) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
VideoView(
@@ -759,12 +837,32 @@ private fun RenderImageOrVideo(content: ZoomableContent, roundedCorner: Boolean,
artworkUri = content.artworkUri,
authorName = content.authorName,
roundedCorner = roundedCorner,
topPaddingForControllers = topPaddingForControllers,
onControllerVisibilityChanged = onControllerVisibilityChanged,
accountViewModel = accountViewModel,
alwaysShowVideo = true
)
}
} else if (content is ZoomableLocalImage) {
LocalImageView(content = content, mainImageModifier = mainModifier, accountViewModel, alwayShowImage = true)
val mainModifier = Modifier
.fillMaxSize()
.zoomable(rememberZoomState())
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null // Avoid flashing on click
) {
if (onToggleControllerVisibility != null) {
onToggleControllerVisibility()
}
}
LocalImageView(
content = content,
mainImageModifier = mainModifier,
topPaddingForControllers = topPaddingForControllers,
accountViewModel,
alwayShowImage = true
)
} else if (content is ZoomableLocalVideo) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
content.localFile?.let {
@@ -774,6 +872,8 @@ private fun RenderImageOrVideo(content: ZoomableContent, roundedCorner: Boolean,
artworkUri = content.artworkUri,
authorName = content.authorName,
roundedCorner = roundedCorner,
topPaddingForControllers = topPaddingForControllers,
onControllerVisibilityChanged = onControllerVisibilityChanged,
accountViewModel = accountViewModel,
alwaysShowVideo = true
)
@@ -800,7 +900,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 +921,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 +929,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)

View File

@@ -5,7 +5,9 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.IsInPublicChatChannel
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
@@ -14,7 +16,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
}
override fun showHiddenKey(): Boolean {
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
return account.defaultDiscoveryFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
}
override fun feed(): List<Note> {

View File

@@ -5,7 +5,9 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
@@ -14,7 +16,7 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
}
override fun showHiddenKey(): Boolean {
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
return account.defaultDiscoveryFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
}
override fun feed(): List<Note> {

View File

@@ -5,10 +5,11 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.quartz.events.*
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.PeopleListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
open class DiscoverLiveFeedFilter(
@@ -23,7 +24,7 @@ open class DiscoverLiveFeedFilter(
}
override fun showHiddenKey(): Boolean {
return followList() == PeopleListEvent.blockList
return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
}
override fun feed(): List<Note> {

View File

@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
class DiscoverLiveNowFeedFilter(

View File

@@ -18,7 +18,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
}
override fun showHiddenKey(): Boolean {
return account.defaultHomeFollowList == PeopleListEvent.blockList
return account.defaultHomeFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
}
override fun feed(): List<Note> {

View File

@@ -17,12 +17,13 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils
class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList
}
override fun showHiddenKey(): Boolean {
return account.defaultHomeFollowList == PeopleListEvent.blockList
return account.defaultHomeFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
}
override fun feed(): List<Note> {

View File

@@ -5,7 +5,18 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RepostEvent
class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
@@ -13,7 +24,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
}
override fun showHiddenKey(): Boolean {
return account.defaultNotificationFollowList == PeopleListEvent.blockList
return account.defaultNotificationFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
}
override fun feed(): List<Note> {

View File

@@ -4,7 +4,9 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
@@ -13,7 +15,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
}
override fun showHiddenKey(): Boolean {
return account.defaultStoriesFollowList == PeopleListEvent.blockList
return account.defaultStoriesFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
}
override fun feed(): List<Note> {
@@ -29,7 +31,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = TimeUtils.now()
val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS
val isHiddenList = account.defaultStoriesFollowList == PeopleListEvent.blockList
val isHiddenList = account.defaultStoriesFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet()

View File

@@ -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
@@ -60,6 +61,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 +113,9 @@ fun AccountSwitchBottomSheet(
ArrowBackIcon()
}
},
backgroundColor = Color.Transparent,
elevation = 0.dp
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
}
@@ -155,7 +158,7 @@ fun DisplayAccount(
modifier = Modifier
.fillMaxWidth()
.clickable {
accountStateViewModel.switchUser(acc.npub)
accountStateViewModel.switchUser(acc)
}
.padding(16.dp, 16.dp),
verticalAlignment = Alignment.CenterVertically
@@ -173,7 +176,11 @@ fun DisplayAccount(
.width(55.dp)
.padding(0.dp)
) {
AccountPicture(it)
val automaticallyShowProfilePicture = remember {
accountViewModel.settings.showProfilePictures.value
}
AccountPicture(it, automaticallyShowProfilePicture)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
@@ -202,22 +209,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
)
}
@@ -269,7 +275,7 @@ private fun LogoutButton(
TextButton(
onClick = {
logoutDialog = false
accountStateViewModel.logOff(acc.npub)
accountStateViewModel.logOff(acc)
}
) {
Text(text = stringResource(R.string.log_out))
@@ -293,7 +299,7 @@ private fun LogoutButton(
Icon(
imageVector = Icons.Default.Logout,
contentDescription = stringResource(R.string.log_out),
tint = MaterialTheme.colors.onSurface
tint = MaterialTheme.colorScheme.onSurface
)
}
}

View File

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

View File

@@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.BookmarkListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen
@@ -69,7 +69,7 @@ fun AppNavigation(
navController: NavHostController,
accountViewModel: AccountViewModel,
themeViewModel: ThemeViewModel
sharedPreferencesViewModel: SharedPreferencesViewModel
) {
val scope = rememberCoroutineScope()
val nav = remember {
@@ -263,8 +263,7 @@ fun AppNavigation(
Route.Settings.let { route ->
composable(route.route, route.arguments, content = {
SettingsScreen(
accountViewModel = accountViewModel,
themeViewModel
sharedPreferencesViewModel
)
})
}

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