mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 07:48:27 +00:00
Compare commits
249 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
227aac5b76 | ||
|
|
82ea72d2d1 | ||
|
|
f1637c3d20 | ||
|
|
07be841246 | ||
|
|
3ce5d92556 | ||
|
|
8f11396e00 | ||
|
|
2259523387 | ||
|
|
d67711b123 | ||
|
|
c89ca51a1b | ||
|
|
c5c951dd59 | ||
|
|
9f68f0227e | ||
|
|
7faed2ddd3 | ||
|
|
7dceb701b2 | ||
|
|
e454ccc326 | ||
|
|
bac50668e8 | ||
|
|
ff9e1556d5 | ||
|
|
dffb49b071 | ||
|
|
7387d9060f | ||
|
|
52e1842174 | ||
|
|
d738e5cbaf | ||
|
|
386b6db5ff | ||
|
|
47d57e5a44 | ||
|
|
92068ca64f | ||
|
|
349510e900 | ||
|
|
948d0f4bea | ||
|
|
b9a5586167 | ||
|
|
54edddbd2d | ||
|
|
b9c83426dc | ||
|
|
40dae875b8 | ||
|
|
e762ad5e18 | ||
|
|
9b1bd2127f | ||
|
|
88701a97c7 | ||
|
|
600c927594 | ||
|
|
1387a0f62b | ||
|
|
e3df94338f | ||
|
|
ee466aa1e4 | ||
|
|
9704d61a7a | ||
|
|
69d7e82b71 | ||
|
|
288d80d163 | ||
|
|
b1edf4e934 | ||
|
|
af9b0b444c | ||
|
|
a3865e68bd | ||
|
|
ed70585e90 | ||
|
|
f707fd007b | ||
|
|
d92e30dba3 | ||
|
|
4afa5c1789 | ||
|
|
e89e8e5d01 | ||
|
|
52f600485b | ||
|
|
b5a0d65f3c | ||
|
|
fc4433e7ae | ||
|
|
f2b6a9bedb | ||
|
|
35f0b1291f | ||
|
|
045699ecd9 | ||
|
|
74a2b46508 | ||
|
|
feaa594e52 | ||
|
|
d966541ea7 | ||
|
|
ecfc287871 | ||
|
|
c10461126a | ||
|
|
20903cefc8 | ||
|
|
e1caf40c3a | ||
|
|
6e70034758 | ||
|
|
ee30ff28f6 | ||
|
|
03654b5ebd | ||
|
|
7071174aed | ||
|
|
2c27531056 | ||
|
|
57d5556728 | ||
|
|
f4da2ae6be | ||
|
|
57dfe3af8c | ||
|
|
618d14a965 | ||
|
|
ee95942289 | ||
|
|
c6c15c3ec7 | ||
|
|
ed4c51b664 | ||
|
|
b3f39434ee | ||
|
|
30ded17581 | ||
|
|
19557bf358 | ||
|
|
bdc2cbbb99 | ||
|
|
82e2d15aaf | ||
|
|
6aff31886e | ||
|
|
7cc089f0ee | ||
|
|
6c025f7274 | ||
|
|
d5220324c4 | ||
|
|
a0308bd3a1 | ||
|
|
856b4ca2c1 | ||
|
|
8abfd10f0c | ||
|
|
1bc4d33bb7 | ||
|
|
35c67f692a | ||
|
|
a319902cf7 | ||
|
|
2bc8d26b4f | ||
|
|
402da58391 | ||
|
|
b533afe27f | ||
|
|
6a4afb45dc | ||
|
|
73a718e6c7 | ||
|
|
140f6e3318 | ||
|
|
073add9e71 | ||
|
|
35ecf1f7a7 | ||
|
|
71db62e5a5 | ||
|
|
ff82b52a5b | ||
|
|
18e81de26d | ||
|
|
27f37fc158 | ||
|
|
6638ae1d7b | ||
|
|
42094c2a70 | ||
|
|
932abdd412 | ||
|
|
b4bb6f2b15 | ||
|
|
4c7b523de1 | ||
|
|
59608e9acc | ||
|
|
77afedb2d0 | ||
|
|
ccb2c626a7 | ||
|
|
9b6eec5fdd | ||
|
|
02786fb7d3 | ||
|
|
384594ba5c | ||
|
|
dab02f2cbd | ||
|
|
978f13fbf3 | ||
|
|
831386a656 | ||
|
|
8b7406e25a | ||
|
|
9e7fed6646 | ||
|
|
89d88326e8 | ||
|
|
f18dd02161 | ||
|
|
e00db19485 | ||
|
|
a706b71d7c | ||
|
|
a83b26cc6f | ||
|
|
dc73843447 | ||
|
|
3e6aa2b943 | ||
|
|
909d3f4fbe | ||
|
|
fb0748f4a7 | ||
|
|
1ec4823122 | ||
|
|
cce5e0bf21 | ||
|
|
0006595786 | ||
|
|
6b884f645e | ||
|
|
946723a80e | ||
|
|
791debbaca | ||
|
|
3bb5a4e50d | ||
|
|
3efa3a68fe | ||
|
|
dc314a94ff | ||
|
|
11b062c41f | ||
|
|
faeb2a3894 | ||
|
|
b1f45a346d | ||
|
|
0b16de6a85 | ||
|
|
66d4b75373 | ||
|
|
3b376a615c | ||
|
|
c8da570c89 | ||
|
|
38f4a5b7fc | ||
|
|
b03ff36cf3 | ||
|
|
d145938652 | ||
|
|
1993388d9f | ||
|
|
15e7540b26 | ||
|
|
c2beaf5f80 | ||
|
|
5d395419ff | ||
|
|
f672a51470 | ||
|
|
f741239e23 | ||
|
|
3442194b01 | ||
|
|
0b025d7679 | ||
|
|
227de74317 | ||
|
|
0b9fced8bc | ||
|
|
274e4f7498 | ||
|
|
f6496b5e7e | ||
|
|
ee5ff1845f | ||
|
|
c84e835ce5 | ||
|
|
cdb5be2fe9 | ||
|
|
d726b3b5b5 | ||
|
|
2fa4281b6f | ||
|
|
c42944e15b | ||
|
|
9f9f158709 | ||
|
|
3a753f0d5d | ||
|
|
13c94112d4 | ||
|
|
dd46f12a05 | ||
|
|
9a648d7e84 | ||
|
|
614dbab1fd | ||
|
|
3d80ee1984 | ||
|
|
6876a7f0d2 | ||
|
|
87dbf86129 | ||
|
|
175d1ae33b | ||
|
|
bf9d318209 | ||
|
|
21560d098e | ||
|
|
33f7c5c82e | ||
|
|
52f17e9f46 | ||
|
|
1e7ffa365b | ||
|
|
e00fc83e5c | ||
|
|
a2fcb3539e | ||
|
|
8c4af4ac85 | ||
|
|
58cf0d9f5d | ||
|
|
de5c0d4681 | ||
|
|
780832d785 | ||
|
|
b8808a17b5 | ||
|
|
5001e58c2b | ||
|
|
95514b66ea | ||
|
|
90c6d6c8f8 | ||
|
|
c3ddc4f7ad | ||
|
|
ff4d704bde | ||
|
|
a773be12c4 | ||
|
|
7ff6f16df0 | ||
|
|
36a3c4123c | ||
|
|
6303573dd9 | ||
|
|
a7aa3705ef | ||
|
|
a64c941f36 | ||
|
|
59cde5699e | ||
|
|
bd836a9580 | ||
|
|
90aade338c | ||
|
|
6d691c4741 | ||
|
|
bbba27752d | ||
|
|
d9cf090cc9 | ||
|
|
46effa572c | ||
|
|
f76ab01c11 | ||
|
|
d1cb699027 | ||
|
|
9ee1250f3a | ||
|
|
7a68cf867e | ||
|
|
bc8fa00608 | ||
|
|
329d806ffd | ||
|
|
e040d0f971 | ||
|
|
c223c1372b | ||
|
|
a450dcbac1 | ||
|
|
ee5fbce2ce | ||
|
|
559b88be18 | ||
|
|
dfbc5fa556 | ||
|
|
df1a017eb3 | ||
|
|
b2b7799a0f | ||
|
|
e817f94045 | ||
|
|
46571a6029 | ||
|
|
4f4d689f5a | ||
|
|
a0308938de | ||
|
|
b10e875941 | ||
|
|
632400f52c | ||
|
|
b63479c45f | ||
|
|
7ea6495f28 | ||
|
|
453e63025d | ||
|
|
17ecc98e1e | ||
|
|
f9272f5cfc | ||
|
|
348e07a307 | ||
|
|
2009f5bd7b | ||
|
|
7ea92bc068 | ||
|
|
3b07b5d8dd | ||
|
|
f06e79cfd3 | ||
|
|
584ceb1f0f | ||
|
|
8053119d42 | ||
|
|
b8e97525eb | ||
|
|
66b1e4b15d | ||
|
|
e6f6f64512 | ||
|
|
d2eea9f0ae | ||
|
|
012fdc8cc6 | ||
|
|
74054aab5a | ||
|
|
11908b9e0c | ||
|
|
570cccfc62 | ||
|
|
de9d6877e1 | ||
|
|
c31fa116ca | ||
|
|
10b15226a1 | ||
|
|
7a46376bc1 | ||
|
|
b761aaf970 | ||
|
|
3a08db2dfb | ||
|
|
41fd2e087c | ||
|
|
ba476bcdca |
32
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
32
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 296
|
||||
versionName "0.76.2"
|
||||
versionCode 311
|
||||
versionName "0.79.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -102,7 +102,7 @@ dependencies {
|
||||
// Observe Live data as State
|
||||
implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version"
|
||||
|
||||
implementation "androidx.compose.material:material:$compose_ui_version"
|
||||
implementation "androidx.compose.material3:material3:${material3_version}"
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose_ui_version"
|
||||
|
||||
// Lifecycle
|
||||
@@ -155,7 +155,7 @@ dependencies {
|
||||
|
||||
// Markdown (With fix for full-image bleeds)
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material3:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:a0954aba63')
|
||||
|
||||
// Language picker and Theme chooser
|
||||
@@ -171,7 +171,7 @@ dependencies {
|
||||
playImplementation 'com.google.mlkit:translate:17.0.1'
|
||||
|
||||
// PushNotifications
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.3.1')
|
||||
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||
|
||||
// Charts
|
||||
@@ -192,7 +192,7 @@ dependencies {
|
||||
implementation 'id.zelory:compressor:3.0.1'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'io.mockk:mockk:1.13.7'
|
||||
testImplementation 'io.mockk:mockk:1.13.8'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha01'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.service.RichTextParser
|
||||
import com.vitorpamplona.amethyst.service.RichTextViewerState
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -666,7 +666,7 @@ class RichTextParserTest {
|
||||
|
||||
@Test
|
||||
fun testTextToParse() {
|
||||
val state = RichTextParser().parseText(textToParse, ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText(textToParse, EmptyTagList)
|
||||
Assert.assertEquals(
|
||||
"relay.shitforce.one, relayable.org, universe.nostrich.land, nos.lol, universe.nostrich.land?lang=zh, universe.nostrich.land?lang=en, relay.damus.io, relay.nostr.wirednet.jp, offchain.pub, nostr.rocks, relay.wellorder.net, nostr.oxtr.dev, universe.nostrich.land?lang=ja, relay.mostr.pub, nostr.bitcoiner.social, Nostr-Check.com, MR.Rabbit, Ancap.su, zapper.lol, smies.me, baller.hodl",
|
||||
state.urlSet.joinToString(", ")
|
||||
@@ -3989,7 +3989,7 @@ class RichTextParserTest {
|
||||
|
||||
@Test
|
||||
fun testShortTextToParse() {
|
||||
val state = RichTextParser().parseText("Hi, how are you doing? ", ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText("Hi, how are you doing? ", EmptyTagList)
|
||||
Assert.assertTrue(state.urlSet.isEmpty())
|
||||
Assert.assertTrue(state.imagesForPager.isEmpty())
|
||||
Assert.assertTrue(state.imageList.isEmpty())
|
||||
@@ -3999,7 +3999,7 @@ class RichTextParserTest {
|
||||
|
||||
@Test
|
||||
fun testShortNewLinesTextToParse() {
|
||||
val state = RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList)
|
||||
Assert.assertTrue(state.urlSet.isEmpty())
|
||||
Assert.assertTrue(state.imagesForPager.isEmpty())
|
||||
Assert.assertTrue(state.imageList.isEmpty())
|
||||
@@ -4019,7 +4019,7 @@ See how it can be done here: https://lnshort.it/live-stream-embeds/
|
||||
https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg
|
||||
""".trimIndent()
|
||||
|
||||
val state = RichTextParser().parseText(text, ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText(text, EmptyTagList)
|
||||
Assert.assertEquals("https://lnshort.it/live-stream-embeds/", state.urlSet.firstOrNull())
|
||||
Assert.assertEquals("https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", state.imagesForPager.keys.firstOrNull())
|
||||
Assert.assertEquals("https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", state.imageList.firstOrNull()?.url)
|
||||
@@ -4080,7 +4080,7 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
|
||||
fun testNewLineAfterImage() {
|
||||
val text = "That’s it ! That’s the #note https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg "
|
||||
|
||||
val state = RichTextParser().parseText(text, ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText(text, EmptyTagList)
|
||||
|
||||
printStateForDebug(state)
|
||||
|
||||
@@ -4106,7 +4106,7 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
|
||||
fun testSapceAfterImage() {
|
||||
val text = "That’s it! https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg That’s the #note"
|
||||
|
||||
val state = RichTextParser().parseText(text, ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText(text, EmptyTagList)
|
||||
|
||||
printStateForDebug(state)
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.vitorpamplona.amethyst.service.notifications
|
||||
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
|
||||
class PushNotificationUtils {
|
||||
fun init(accounts: List<AccountInfo>) {
|
||||
object PushNotificationUtils {
|
||||
var hasInit: Boolean = true
|
||||
suspend fun init(accounts: List<AccountInfo>) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<queries>
|
||||
<package android:name="org.torproject.android"/>
|
||||
<package android:name="com.greenart7c3.nostrsigner"/>
|
||||
<package android:name="com.greenart7c3.nostrsigner.debug"/>
|
||||
</queries>
|
||||
|
||||
<!-- Doesn't require a camera -->
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material3.pullrefresh
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.platform.inspectable
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
|
||||
/**
|
||||
* A nested scroll modifier that provides scroll events to [state].
|
||||
*
|
||||
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
|
||||
* order to receive scroll events. For example:
|
||||
*
|
||||
* @sample androidx.compose.material.samples.PullRefreshSample
|
||||
*
|
||||
* @param state The [PullRefreshState] associated with this pull-to-refresh component.
|
||||
* The state will be updated by this modifier.
|
||||
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored.
|
||||
*/
|
||||
// TODO(b/244423199): Move pullRefresh into its own material library similar to material-ripple.
|
||||
fun Modifier.pullRefresh(
|
||||
state: PullRefreshState,
|
||||
enabled: Boolean = true
|
||||
) = inspectable(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "pullRefresh"
|
||||
properties["state"] = state
|
||||
properties["enabled"] = enabled
|
||||
}
|
||||
) {
|
||||
Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* A nested scroll modifier that provides [onPull] and [onRelease] callbacks to aid building custom
|
||||
* pull refresh components.
|
||||
*
|
||||
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
|
||||
* order to receive scroll events. For example:
|
||||
*
|
||||
* @sample androidx.compose.material.samples.CustomPullRefreshSample
|
||||
*
|
||||
* @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument.
|
||||
* Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling
|
||||
* down despite being at the top of a scrollable component), whereas negative delta (swiping up) is
|
||||
* dispatched first (in case it is needed to push the indicator back up), and then the unconsumed
|
||||
* delta is passed on to the child. The callback returns how much delta was consumed.
|
||||
* @param onRelease Callback for when drag is released, takes float flingVelocity as argument.
|
||||
* The callback returns how much velocity was consumed - in most cases this should only consume
|
||||
* velocity if pull refresh has been dragged already and the velocity is positive (the fling is
|
||||
* downwards), as an upwards fling should typically still scroll a scrollable component beneath the
|
||||
* pullRefresh. This is invoked before any remaining velocity is passed to the child.
|
||||
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither
|
||||
* [onPull] nor [onRelease] will be invoked.
|
||||
*/
|
||||
fun Modifier.pullRefresh(
|
||||
onPull: (pullDelta: Float) -> Float,
|
||||
onRelease: suspend (flingVelocity: Float) -> Float,
|
||||
enabled: Boolean = true
|
||||
) = inspectable(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "pullRefresh"
|
||||
properties["onPull"] = onPull
|
||||
properties["onRelease"] = onRelease
|
||||
properties["enabled"] = enabled
|
||||
}
|
||||
) {
|
||||
Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
|
||||
}
|
||||
|
||||
private class PullRefreshNestedScrollConnection(
|
||||
private val onPull: (pullDelta: Float) -> Float,
|
||||
private val onRelease: suspend (flingVelocity: Float) -> Float,
|
||||
private val enabled: Boolean
|
||||
) : NestedScrollConnection {
|
||||
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset = when {
|
||||
!enabled -> Offset.Zero
|
||||
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset = when {
|
||||
!enabled -> Offset.Zero
|
||||
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
return Velocity(0f, onRelease(available.y))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material3.pullrefresh
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathFillType
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* The default indicator for Compose pull-to-refresh, based on Android's SwipeRefreshLayout.
|
||||
*
|
||||
* @sample androidx.compose.material.samples.PullRefreshSample
|
||||
*
|
||||
* @param refreshing A boolean representing whether a refresh is occurring.
|
||||
* @param state The [PullRefreshState] which controls where and how the indicator will be drawn.
|
||||
* @param modifier Modifiers for the indicator.
|
||||
* @param backgroundColor The color of the indicator's background.
|
||||
* @param contentColor The color of the indicator's arc and arrow.
|
||||
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
|
||||
*/
|
||||
// TODO(b/244423199): Consider whether the state parameter should be replaced with lambdas to
|
||||
// enable people to use this indicator with custom pull-to-refresh components.
|
||||
@Composable
|
||||
fun PullRefreshIndicator(
|
||||
refreshing: Boolean,
|
||||
state: PullRefreshState,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||
contentColor: Color = contentColorFor(backgroundColor),
|
||||
scale: Boolean = false
|
||||
) {
|
||||
val showElevation by remember(refreshing, state) {
|
||||
derivedStateOf { refreshing || state.position > 0.5f }
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.size(IndicatorSize)
|
||||
.pullRefreshIndicatorTransform(state, scale),
|
||||
shape = SpinnerShape,
|
||||
color = backgroundColor,
|
||||
shadowElevation = if (showElevation) Elevation else 0.dp
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = refreshing,
|
||||
animationSpec = tween(durationMillis = CrossfadeDurationMs)
|
||||
) { refreshing ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
|
||||
|
||||
if (refreshing) {
|
||||
CircularProgressIndicator(
|
||||
color = contentColor,
|
||||
strokeWidth = StrokeWidth,
|
||||
modifier = Modifier.size(spinnerSize)
|
||||
)
|
||||
} else {
|
||||
CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier.size MUST be specified.
|
||||
*/
|
||||
@Composable
|
||||
private fun CircularArrowIndicator(
|
||||
state: PullRefreshState,
|
||||
color: Color,
|
||||
modifier: Modifier
|
||||
) {
|
||||
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
|
||||
|
||||
val targetAlpha by remember(state) {
|
||||
derivedStateOf {
|
||||
if (state.progress >= 1f) MaxAlpha else MinAlpha
|
||||
}
|
||||
}
|
||||
|
||||
val alphaState = animateFloatAsState(targetValue = targetAlpha, animationSpec = AlphaTween)
|
||||
|
||||
// Empty semantics for tests
|
||||
Canvas(modifier.semantics {}) {
|
||||
val values = ArrowValues(state.progress)
|
||||
val alpha = alphaState.value
|
||||
|
||||
rotate(degrees = values.rotation) {
|
||||
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f
|
||||
val arcBounds = Rect(
|
||||
size.center.x - arcRadius,
|
||||
size.center.y - arcRadius,
|
||||
size.center.x + arcRadius,
|
||||
size.center.y + arcRadius
|
||||
)
|
||||
drawArc(
|
||||
color = color,
|
||||
alpha = alpha,
|
||||
startAngle = values.startAngle,
|
||||
sweepAngle = values.endAngle - values.startAngle,
|
||||
useCenter = false,
|
||||
topLeft = arcBounds.topLeft,
|
||||
size = arcBounds.size,
|
||||
style = Stroke(
|
||||
width = StrokeWidth.toPx(),
|
||||
cap = StrokeCap.Square
|
||||
)
|
||||
)
|
||||
drawArrow(path, arcBounds, color, alpha, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private class ArrowValues(
|
||||
val rotation: Float,
|
||||
val startAngle: Float,
|
||||
val endAngle: Float,
|
||||
val scale: Float
|
||||
)
|
||||
|
||||
private fun ArrowValues(progress: Float): ArrowValues {
|
||||
// Discard first 40% of progress. Scale remaining progress to full range between 0 and 100%.
|
||||
val adjustedPercent = max(min(1f, progress) - 0.4f, 0f) * 5 / 3
|
||||
// How far beyond the threshold pull has gone, as a percentage of the threshold.
|
||||
val overshootPercent = abs(progress) - 1.0f
|
||||
// Limit the overshoot to 200%. Linear between 0 and 200.
|
||||
val linearTension = overshootPercent.coerceIn(0f, 2f)
|
||||
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
|
||||
val tensionPercent = linearTension - linearTension.pow(2) / 4
|
||||
|
||||
// Calculations based on SwipeRefreshLayout specification.
|
||||
val endTrim = adjustedPercent * MaxProgressArc
|
||||
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
|
||||
val startAngle = rotation * 360
|
||||
val endAngle = (rotation + endTrim) * 360
|
||||
val scale = min(1f, adjustedPercent)
|
||||
|
||||
return ArrowValues(rotation, startAngle, endAngle, scale)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawArrow(
|
||||
arrow: Path,
|
||||
bounds: Rect,
|
||||
color: Color,
|
||||
alpha: Float,
|
||||
values: ArrowValues
|
||||
) {
|
||||
arrow.reset()
|
||||
arrow.moveTo(0f, 0f) // Move to left corner
|
||||
arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
|
||||
|
||||
// Line to tip of arrow
|
||||
arrow.lineTo(
|
||||
x = ArrowWidth.toPx() * values.scale / 2,
|
||||
y = ArrowHeight.toPx() * values.scale
|
||||
)
|
||||
|
||||
val radius = min(bounds.width, bounds.height) / 2f
|
||||
val inset = ArrowWidth.toPx() * values.scale / 2f
|
||||
arrow.translate(
|
||||
Offset(
|
||||
x = radius + bounds.center.x - inset,
|
||||
y = bounds.center.y + StrokeWidth.toPx() / 2f
|
||||
)
|
||||
)
|
||||
arrow.close()
|
||||
rotate(degrees = values.endAngle) {
|
||||
drawPath(path = arrow, color = color, alpha = alpha)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CrossfadeDurationMs = 100
|
||||
private const val MaxProgressArc = 0.8f
|
||||
|
||||
private val IndicatorSize = 40.dp
|
||||
private val SpinnerShape = CircleShape
|
||||
private val ArcRadius = 7.5.dp
|
||||
private val StrokeWidth = 2.5.dp
|
||||
private val ArrowWidth = 10.dp
|
||||
private val ArrowHeight = 5.dp
|
||||
private val Elevation = 6.dp
|
||||
|
||||
// Values taken from SwipeRefreshLayout
|
||||
private const val MinAlpha = 0.3f
|
||||
private const val MaxAlpha = 1f
|
||||
private val AlphaTween = tween<Float>(300, easing = LinearEasing)
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material3.pullrefresh
|
||||
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.platform.inspectable
|
||||
|
||||
/**
|
||||
* A modifier for translating the position and scaling the size of a pull-to-refresh indicator
|
||||
* based on the given [PullRefreshState].
|
||||
*
|
||||
* @sample androidx.compose.material.samples.PullRefreshIndicatorTransformSample
|
||||
*
|
||||
* @param state The [PullRefreshState] which determines the position of the indicator.
|
||||
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
|
||||
*/
|
||||
// TODO: Consider whether the state parameter should be replaced with lambdas.
|
||||
fun Modifier.pullRefreshIndicatorTransform(
|
||||
state: PullRefreshState,
|
||||
scale: Boolean = false
|
||||
) = inspectable(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "pullRefreshIndicatorTransform"
|
||||
properties["state"] = state
|
||||
properties["scale"] = scale
|
||||
}
|
||||
) {
|
||||
Modifier
|
||||
// Essentially we only want to clip the at the top, so the indicator will not appear when
|
||||
// the position is 0. It is preferable to clip the indicator as opposed to the layout that
|
||||
// contains the indicator, as this would also end up clipping shadows drawn by items in a
|
||||
// list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE
|
||||
// for the other dimensions to allow for more room for elevation / arbitrary indicators - we
|
||||
// only ever really want to clip at the top edge.
|
||||
.drawWithContent {
|
||||
clipRect(
|
||||
top = 0f,
|
||||
left = -Float.MAX_VALUE,
|
||||
right = Float.MAX_VALUE,
|
||||
bottom = Float.MAX_VALUE
|
||||
) {
|
||||
this@drawWithContent.drawContent()
|
||||
}
|
||||
}
|
||||
.graphicsLayer {
|
||||
translationY = state.position - size.height
|
||||
|
||||
if (scale && !state.refreshing) {
|
||||
val scaleFraction = LinearOutSlowInEasing
|
||||
.transform(state.position / state.threshold)
|
||||
.coerceIn(0f, 1f)
|
||||
scaleX = scaleFraction
|
||||
scaleY = scaleFraction
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material3.pullrefresh
|
||||
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.foundation.MutatorMutex
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Creates a [PullRefreshState] that is remembered across compositions.
|
||||
*
|
||||
* Changes to [refreshing] will result in [PullRefreshState] being updated.
|
||||
*
|
||||
* @sample androidx.compose.material.samples.PullRefreshSample
|
||||
*
|
||||
* @param refreshing A boolean representing whether a refresh is currently occurring.
|
||||
* @param onRefresh The function to be called to trigger a refresh.
|
||||
* @param refreshThreshold The threshold below which, if a release
|
||||
* occurs, [onRefresh] will be called.
|
||||
* @param refreshingOffset The offset at which the indicator will be drawn while refreshing. This
|
||||
* offset corresponds to the position of the bottom of the indicator.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberPullRefreshState(
|
||||
refreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
|
||||
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset
|
||||
): PullRefreshState {
|
||||
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val onRefreshState = rememberUpdatedState(onRefresh)
|
||||
val thresholdPx: Float
|
||||
val refreshingOffsetPx: Float
|
||||
|
||||
with(LocalDensity.current) {
|
||||
thresholdPx = refreshThreshold.toPx()
|
||||
refreshingOffsetPx = refreshingOffset.toPx()
|
||||
}
|
||||
|
||||
val state = remember(scope) {
|
||||
PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx)
|
||||
}
|
||||
|
||||
SideEffect {
|
||||
state.setRefreshing(refreshing)
|
||||
state.setThreshold(thresholdPx)
|
||||
state.setRefreshingOffset(refreshingOffsetPx)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* A state object that can be used in conjunction with [pullRefresh] to add pull-to-refresh
|
||||
* behaviour to a scroll component. Based on Android's SwipeRefreshLayout.
|
||||
*
|
||||
* Provides [progress], a float representing how far the user has pulled as a percentage of the
|
||||
* refreshThreshold. Values of one or less indicate that the user has not yet pulled past the
|
||||
* threshold. Values greater than one indicate how far past the threshold the user has pulled.
|
||||
*
|
||||
* Can be used in conjunction with [pullRefreshIndicatorTransform] to implement Android-like
|
||||
* pull-to-refresh behaviour with a custom indicator.
|
||||
*
|
||||
* Should be created using [rememberPullRefreshState].
|
||||
*/
|
||||
class PullRefreshState internal constructor(
|
||||
private val animationScope: CoroutineScope,
|
||||
private val onRefreshState: State<() -> Unit>,
|
||||
refreshingOffset: Float,
|
||||
threshold: Float
|
||||
) {
|
||||
/**
|
||||
* A float representing how far the user has pulled as a percentage of the refreshThreshold.
|
||||
*
|
||||
* If the component has not been pulled at all, progress is zero. If the pull has reached
|
||||
* halfway to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has
|
||||
* gone beyond the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to
|
||||
* two times the refreshThreshold.
|
||||
*/
|
||||
val progress get() = adjustedDistancePulled / threshold
|
||||
|
||||
internal val refreshing get() = _refreshing
|
||||
internal val position get() = _position
|
||||
internal val threshold get() = _threshold
|
||||
|
||||
private val adjustedDistancePulled by derivedStateOf { distancePulled * DragMultiplier }
|
||||
|
||||
private var _refreshing by mutableStateOf(false)
|
||||
private var _position by mutableStateOf(0f)
|
||||
private var distancePulled by mutableStateOf(0f)
|
||||
private var _threshold by mutableStateOf(threshold)
|
||||
private var _refreshingOffset by mutableStateOf(refreshingOffset)
|
||||
|
||||
internal fun onPull(pullDelta: Float): Float {
|
||||
if (_refreshing) return 0f // Already refreshing, do nothing.
|
||||
|
||||
val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f)
|
||||
val dragConsumed = newOffset - distancePulled
|
||||
distancePulled = newOffset
|
||||
_position = calculateIndicatorPosition()
|
||||
return dragConsumed
|
||||
}
|
||||
|
||||
internal fun onRelease(velocity: Float): Float {
|
||||
if (refreshing) return 0f // Already refreshing, do nothing
|
||||
|
||||
if (adjustedDistancePulled > threshold) {
|
||||
onRefreshState.value()
|
||||
}
|
||||
animateIndicatorTo(0f)
|
||||
val consumed = when {
|
||||
// We are flinging without having dragged the pull refresh (for example a fling inside
|
||||
// a list) - don't consume
|
||||
distancePulled == 0f -> 0f
|
||||
// If the velocity is negative, the fling is upwards, and we don't want to prevent the
|
||||
// the list from scrolling
|
||||
velocity < 0f -> 0f
|
||||
// We are showing the indicator, and the fling is downwards - consume everything
|
||||
else -> velocity
|
||||
}
|
||||
distancePulled = 0f
|
||||
return consumed
|
||||
}
|
||||
|
||||
internal fun setRefreshing(refreshing: Boolean) {
|
||||
if (_refreshing != refreshing) {
|
||||
_refreshing = refreshing
|
||||
distancePulled = 0f
|
||||
animateIndicatorTo(if (refreshing) _refreshingOffset else 0f)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setThreshold(threshold: Float) {
|
||||
_threshold = threshold
|
||||
}
|
||||
|
||||
internal fun setRefreshingOffset(refreshingOffset: Float) {
|
||||
if (_refreshingOffset != refreshingOffset) {
|
||||
_refreshingOffset = refreshingOffset
|
||||
if (refreshing) animateIndicatorTo(refreshingOffset)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to cancel any existing animations when we launch a new one. We use this instead of
|
||||
// Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
|
||||
// overhead of running through the animation pipeline instead of directly mutating the state.
|
||||
private val mutatorMutex = MutatorMutex()
|
||||
|
||||
private fun animateIndicatorTo(offset: Float) = animationScope.launch {
|
||||
mutatorMutex.mutate {
|
||||
animate(initialValue = _position, targetValue = offset) { value, _ ->
|
||||
_position = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateIndicatorPosition(): Float = when {
|
||||
// If drag hasn't gone past the threshold, the position is the adjustedDistancePulled.
|
||||
adjustedDistancePulled <= threshold -> adjustedDistancePulled
|
||||
else -> {
|
||||
// How far beyond the threshold pull has gone, as a percentage of the threshold.
|
||||
val overshootPercent = abs(progress) - 1.0f
|
||||
// Limit the overshoot to 200%. Linear between 0 and 200.
|
||||
val linearTension = overshootPercent.coerceIn(0f, 2f)
|
||||
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
|
||||
val tensionPercent = linearTension - linearTension.pow(2) / 4
|
||||
// The additional offset beyond the threshold.
|
||||
val extraOffset = threshold * tensionPercent
|
||||
threshold + extraOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default parameter values for [rememberPullRefreshState].
|
||||
*/
|
||||
object PullRefreshDefaults {
|
||||
/**
|
||||
* If the indicator is below this threshold offset when it is released, a refresh
|
||||
* will be triggered.
|
||||
*/
|
||||
val RefreshThreshold = 80.dp
|
||||
|
||||
/**
|
||||
* The offset at which the indicator should be rendered whilst a refresh is occurring.
|
||||
*/
|
||||
val RefreshingOffset = 56.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* The distance pulled is multiplied by this value to give us the adjusted distance pulled, which
|
||||
* is used in calculating the indicator position (when the adjusted distance pulled is less than
|
||||
* the refresh threshold, it is the indicator position, otherwise the indicator position is
|
||||
* derived from the progress).
|
||||
*/
|
||||
private const val DragMultiplier = 0.5f
|
||||
@@ -3,23 +3,26 @@ package com.vitorpamplona.amethyst
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.BooleanType
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.DefaultReactions
|
||||
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.Nip47URI
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
@@ -35,7 +38,8 @@ private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
|
||||
@Immutable
|
||||
data class AccountInfo(
|
||||
val npub: String,
|
||||
val hasPrivKey: Boolean = false
|
||||
val hasPrivKey: Boolean,
|
||||
val loggedInWithExternalSigner: Boolean
|
||||
)
|
||||
|
||||
private object PrefKeys {
|
||||
@@ -74,7 +78,9 @@ private object PrefKeys {
|
||||
const val THEME = "theme"
|
||||
const val PREFERRED_LANGUAGE = "preferred_Language"
|
||||
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars"
|
||||
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
|
||||
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
|
||||
}
|
||||
|
||||
object LocalPreferences {
|
||||
@@ -82,7 +88,7 @@ object LocalPreferences {
|
||||
|
||||
private var _currentAccount: String? = null
|
||||
|
||||
private fun currentAccount(): String? {
|
||||
fun currentAccount(): String? {
|
||||
if (_currentAccount == null) {
|
||||
_currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
|
||||
}
|
||||
@@ -201,7 +207,11 @@ object LocalPreferences {
|
||||
|
||||
fun allSavedAccounts(): List<AccountInfo> {
|
||||
return savedAccounts().map { npub ->
|
||||
AccountInfo(npub = npub)
|
||||
AccountInfo(
|
||||
npub,
|
||||
hasPrivKey(npub),
|
||||
getLoggedInWithExternalSigner(npub)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +255,7 @@ object LocalPreferences {
|
||||
} else {
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
||||
}
|
||||
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.loginWithExternalSigner)
|
||||
}.apply()
|
||||
|
||||
val globalPrefs = encryptedPreferences()
|
||||
@@ -265,6 +276,17 @@ object LocalPreferences {
|
||||
} else {
|
||||
putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview.prefCode!!)
|
||||
}
|
||||
if (account.settings.automaticallyHideNavigationBars.prefCode == null) {
|
||||
remove(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)
|
||||
} else {
|
||||
putBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, account.settings.automaticallyHideNavigationBars.prefCode!!)
|
||||
}
|
||||
|
||||
if (account.settings.automaticallyShowProfilePictures.prefCode == null) {
|
||||
remove(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE)
|
||||
} else {
|
||||
putBoolean(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE, account.settings.automaticallyShowProfilePictures.prefCode!!)
|
||||
}
|
||||
putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "")
|
||||
}.apply()
|
||||
}
|
||||
@@ -276,17 +298,19 @@ object LocalPreferences {
|
||||
}
|
||||
|
||||
fun getTheme(): Int {
|
||||
encryptedPreferences().apply {
|
||||
return getInt(PrefKeys.THEME, 0)
|
||||
}
|
||||
return encryptedPreferences().getInt(PrefKeys.THEME, 0)
|
||||
}
|
||||
|
||||
fun getPreferredLanguage(): String {
|
||||
var language = ""
|
||||
encryptedPreferences().apply {
|
||||
language = getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
|
||||
}
|
||||
return language
|
||||
return encryptedPreferences().getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
|
||||
}
|
||||
|
||||
private fun getLoggedInWithExternalSigner(npub: String): Boolean {
|
||||
return encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
|
||||
}
|
||||
|
||||
private fun hasPrivKey(npub: String): Boolean {
|
||||
return (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank()
|
||||
}
|
||||
|
||||
fun loadFromEncryptedStorage(): Account? {
|
||||
@@ -316,11 +340,11 @@ object LocalPreferences {
|
||||
|
||||
val zapAmountChoices = getString(PrefKeys.ZAP_AMOUNTS, "[]")?.let {
|
||||
Event.mapper.readValue<List<Long>?>(it)
|
||||
}?.ifEmpty { listOf(500L, 1000L, 5000L) } ?: listOf(500L, 1000L, 5000L)
|
||||
}?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
|
||||
|
||||
val reactionChoices = getString(PrefKeys.REACTION_CHOICES, "[]")?.let {
|
||||
Event.mapper.readValue<List<String>?>(it)
|
||||
}?.ifEmpty { listOf("+") } ?: listOf("+")
|
||||
}?.ifEmpty { DefaultReactions } ?: DefaultReactions
|
||||
|
||||
val defaultZapType = getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
|
||||
LnZapEvent.ZapType.values().firstOrNull() { it.name == serverName }
|
||||
@@ -335,16 +359,22 @@ object LocalPreferences {
|
||||
Event.mapper.readValue<Nip47URI?>(it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("LocalPreferences", "Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}", e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
val latestContactList = try {
|
||||
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
|
||||
Event.fromJson(it) as ContactListEvent?
|
||||
println("Decoding Contact List: " + it)
|
||||
if (it != null) {
|
||||
Event.fromJson(it) as ContactListEvent?
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
Log.w("LocalPreferences", "Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", e)
|
||||
null
|
||||
}
|
||||
|
||||
@@ -353,6 +383,7 @@ object LocalPreferences {
|
||||
Event.mapper.readValue<Map<String, String>?>(it)
|
||||
} ?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
Log.w("LocalPreferences", "Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}", e)
|
||||
e.printStackTrace()
|
||||
mapOf()
|
||||
}
|
||||
@@ -363,6 +394,7 @@ object LocalPreferences {
|
||||
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
||||
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
val loginWithExternalSigner = getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
|
||||
|
||||
val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
||||
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
|
||||
@@ -377,6 +409,7 @@ object LocalPreferences {
|
||||
Event.mapper.readValue<Map<String, Long>?>(it)
|
||||
} ?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
Log.w("LocalPreferences", "Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}", e)
|
||||
e.printStackTrace()
|
||||
mapOf()
|
||||
}
|
||||
@@ -399,6 +432,17 @@ object LocalPreferences {
|
||||
} else {
|
||||
ConnectivityType.ALWAYS
|
||||
}
|
||||
settings.automaticallyHideNavigationBars = if (contains(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)) {
|
||||
parseBooleanType(getBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, false))
|
||||
} else {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
|
||||
settings.automaticallyShowProfilePictures = if (contains(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE)) {
|
||||
parseConnectivityType(getBoolean(PrefKeys.AUTOMATICALLY_SHOW_PROFILE_PICTURE, false))
|
||||
} else {
|
||||
ConnectivityType.ALWAYS
|
||||
}
|
||||
|
||||
settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "")
|
||||
}
|
||||
@@ -431,51 +475,11 @@ object LocalPreferences {
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = lastReadPerRoute,
|
||||
settings = settings
|
||||
settings = settings,
|
||||
loginWithExternalSigner = loginWithExternalSigner
|
||||
)
|
||||
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
fun migrateSingleUserPrefs() {
|
||||
if (currentAccount() != null) return
|
||||
|
||||
val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return
|
||||
val npub = Hex.decode(pubkey).toNpub()
|
||||
|
||||
val stringPrefs = listOf(
|
||||
PrefKeys.NOSTR_PRIVKEY,
|
||||
PrefKeys.NOSTR_PUBKEY,
|
||||
PrefKeys.RELAYS,
|
||||
PrefKeys.LANGUAGE_PREFS,
|
||||
PrefKeys.TRANSLATE_TO,
|
||||
PrefKeys.ZAP_AMOUNTS,
|
||||
PrefKeys.LATEST_CONTACT_LIST
|
||||
)
|
||||
|
||||
val stringSetPrefs = listOf(
|
||||
PrefKeys.FOLLOWING_CHANNELS,
|
||||
PrefKeys.HIDDEN_USERS,
|
||||
PrefKeys.DONT_TRANSLATE_FROM
|
||||
)
|
||||
|
||||
encryptedPreferences().apply {
|
||||
val appPrefs = this
|
||||
encryptedPreferences(npub).edit().apply {
|
||||
val userPrefs = this
|
||||
|
||||
stringPrefs.forEach { userPrefs.putString(it, appPrefs.getString(it, null)) }
|
||||
stringSetPrefs.forEach { userPrefs.putStringSet(it, appPrefs.getStringSet(it, null)) }
|
||||
userPrefs.putBoolean(
|
||||
PrefKeys.HIDE_DELETE_REQUEST_DIALOG,
|
||||
appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
||||
)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
encryptedPreferences().edit().clear().apply()
|
||||
addAccount(npub)
|
||||
updateCurrentAccount(npub)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import coil.disk.DiskCache
|
||||
import coil.util.DebugLogger
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
@@ -32,18 +33,28 @@ 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 {
|
||||
var shouldPauseService: Boolean = true // to not open amber in a loop trying to use auth relays and registering for notifications
|
||||
private var isStarted: Boolean = false // to not open amber in a loop trying to use auth relays and registering for notifications
|
||||
private var account: Account? = null
|
||||
|
||||
fun start(account: Account, context: Context) {
|
||||
this.account = account
|
||||
ExternalSignerUtils.account = account
|
||||
start(context)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun start(context: Context) {
|
||||
if (isStarted && account != null) {
|
||||
return
|
||||
}
|
||||
Log.d("ServiceManager", "Starting Relay Services")
|
||||
|
||||
val myAccount = account
|
||||
@@ -80,14 +91,18 @@ 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()
|
||||
NostrSingleChannelDataSource.start()
|
||||
NostrSingleUserDataSource.start()
|
||||
isStarted = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +128,7 @@ object ServiceManager {
|
||||
NostrVideoDataSource.stop()
|
||||
|
||||
Client.disconnect()
|
||||
isStarted = false
|
||||
}
|
||||
|
||||
fun cleanObservers() {
|
||||
@@ -133,6 +149,13 @@ object ServiceManager {
|
||||
LocalCache.pruneExpiredEvents()
|
||||
}
|
||||
}
|
||||
|
||||
fun restartIfDifferentAccount(account: Account, context: Context) {
|
||||
if (this.account != account) {
|
||||
pause()
|
||||
start(account, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SingletonDiskCache {
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.io.File
|
||||
|
||||
@UnstableApi object VideoCache {
|
||||
|
||||
var exoPlayerCacheSize: Long = 90 * 1024 * 1024 // 90MB
|
||||
var exoPlayerCacheSize: Long = 150 * 1024 * 1024 // 90MB
|
||||
|
||||
var leastRecentlyUsedCacheEvictor = LeastRecentlyUsedCacheEvictor(exoPlayerCacheSize)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||
@@ -11,6 +13,7 @@ import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.HexValidator
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
@@ -19,6 +22,7 @@ import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import java.io.File
|
||||
@@ -206,6 +210,14 @@ object LocalCache {
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) {
|
||||
if (event.dTag() == "bookmark") {
|
||||
val loggedInUser = LocalPreferences.currentAccount()
|
||||
val hexKey = loggedInUser?.bechToBytes()
|
||||
if (hexKey != null) {
|
||||
val pubKey = Hex.encode(hexKey)
|
||||
if (pubKey == event.pubKey) {
|
||||
ExternalSignerUtils.content.remove(event.id)
|
||||
}
|
||||
}
|
||||
user.updateBookmark(event)
|
||||
}
|
||||
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
|
||||
@@ -337,7 +349,12 @@ object LocalCache {
|
||||
val channel = getOrCreateChannel(note.idHex) {
|
||||
LiveActivitiesChannel(note.address)
|
||||
} as? LiveActivitiesChannel
|
||||
channel?.updateChannelInfo(author, event, event.createdAt)
|
||||
|
||||
val creator = event.host()?.ifBlank { null }?.let {
|
||||
checkGetOrCreateUser(it)
|
||||
} ?: author
|
||||
|
||||
channel?.updateChannelInfo(creator, event, event.createdAt)
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
@@ -1537,7 +1554,7 @@ object LocalCache {
|
||||
|
||||
@Stable
|
||||
class LocalCacheLiveData {
|
||||
private val _newEventBundles = MutableSharedFlow<Set<Note>>()
|
||||
private val _newEventBundles = MutableSharedFlow<Set<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
|
||||
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
|
||||
|
||||
// Refreshes observers in batches.
|
||||
|
||||
@@ -43,6 +43,10 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
||||
|
||||
return minOf(publishedAt, lastCreatedAt)
|
||||
}
|
||||
|
||||
fun dTag(): String? {
|
||||
return (event as? AddressableEvent)?.dTag()
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
@@ -669,16 +673,24 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
||||
if (event == null) return false
|
||||
val thisEvent = event ?: return false
|
||||
|
||||
val isBoostedNoteHidden = if (event is GenericRepostEvent || event is RepostEvent || event is CommunityPostApprovalEvent) {
|
||||
val isBoostedNoteHidden = if (thisEvent is GenericRepostEvent || thisEvent is RepostEvent || thisEvent is CommunityPostApprovalEvent) {
|
||||
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isSensitive = event?.isSensitive() ?: false
|
||||
return isBoostedNoteHidden ||
|
||||
val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) {
|
||||
accountChoices.hiddenWords.any {
|
||||
thisEvent.content.contains(it, true)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isSensitive = thisEvent.isSensitive()
|
||||
return isBoostedNoteHidden || isHiddenByWord ||
|
||||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
|
||||
accountChoices.spammers.contains(author?.pubkeyHex) ||
|
||||
(isSensitive && accountChoices.showSensitiveContent == false)
|
||||
|
||||
@@ -8,7 +8,9 @@ class Settings(
|
||||
var preferredLanguage: String? = null,
|
||||
var automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
var automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS
|
||||
var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
var automaticallyHideNavigationBars: BooleanType = BooleanType.ALWAYS,
|
||||
var automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS
|
||||
)
|
||||
|
||||
enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) {
|
||||
@@ -38,3 +40,28 @@ fun parseConnectivityType(screenCode: Int): ConnectivityType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class BooleanType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) {
|
||||
ALWAYS(null, 0, R.string.connectivity_type_always),
|
||||
NEVER(false, 1, R.string.connectivity_type_never)
|
||||
}
|
||||
|
||||
fun parseBooleanType(code: Boolean?): BooleanType {
|
||||
return when (code) {
|
||||
BooleanType.ALWAYS.prefCode -> BooleanType.ALWAYS
|
||||
BooleanType.NEVER.prefCode -> BooleanType.NEVER
|
||||
else -> {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseBooleanType(screenCode: Int): BooleanType {
|
||||
return when (screenCode) {
|
||||
BooleanType.ALWAYS.screenCode -> BooleanType.ALWAYS
|
||||
BooleanType.NEVER.screenCode -> BooleanType.NEVER
|
||||
else -> {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ class User(val pubkeyHex: String) {
|
||||
return latestContactList?.verifiedFollowKeySet ?: emptySet()
|
||||
}
|
||||
|
||||
fun cachedFollowingTagSet(): Set<HexKey> {
|
||||
fun cachedFollowingTagSet(): Set<String> {
|
||||
return latestContactList?.verifiedFollowTagSet ?: emptySet()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
|
||||
import com.vitorpamplona.amethyst.ui.components.hashTagsPattern
|
||||
import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
||||
import com.vitorpamplona.amethyst.ui.components.startsWithNIP19Scheme
|
||||
import com.vitorpamplona.amethyst.ui.components.tagIndex
|
||||
import com.vitorpamplona.amethyst.ui.components.videoExtensions
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
@@ -290,3 +289,9 @@ class SchemelessUrlSegment(segment: String, val url: String, val extras: String?
|
||||
|
||||
@Immutable
|
||||
class RegularTextSegment(segment: String) : Segment(segment)
|
||||
|
||||
fun startsWithNIP19Scheme(word: String): Boolean {
|
||||
val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@")
|
||||
|
||||
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class SignerType {
|
||||
SIGN_EVENT,
|
||||
NIP04_ENCRYPT,
|
||||
NIP04_DECRYPT,
|
||||
NIP44_ENCRYPT,
|
||||
NIP44_DECRYPT,
|
||||
GET_PUBLIC_KEY,
|
||||
DECRYPT_ZAP_EVENT
|
||||
}
|
||||
|
||||
object ExternalSignerUtils {
|
||||
val content = LruCache<String, String>(10)
|
||||
var isActivityRunning: Boolean = false
|
||||
val cachedDecryptedContent = mutableMapOf<HexKey, String>()
|
||||
lateinit var account: Account
|
||||
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var decryptResultLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var blockListResultLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun requestRejectedToast() {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
Amethyst.instance.getString(R.string.sign_request_rejected),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun default() {
|
||||
isActivityRunning = false
|
||||
ServiceManager.shouldPauseService = true
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
isActivityRunning = false
|
||||
ServiceManager.shouldPauseService = true
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun start(activity: MainActivity) {
|
||||
activityResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val event = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
content.put(id, event)
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
|
||||
decryptResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val event = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
content.put(id, event)
|
||||
cachedDecryptedContent[id] = event
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
|
||||
blockListResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val decryptedContent = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
cachedDecryptedContent[id] = decryptedContent
|
||||
account.live.invalidateData()
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun openSigner(
|
||||
data: String,
|
||||
type: SignerType,
|
||||
intentResult: ActivityResultLauncher<Intent>,
|
||||
pubKey: HexKey,
|
||||
id: String
|
||||
) {
|
||||
try {
|
||||
ServiceManager.shouldPauseService = false
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data"))
|
||||
val signerType = when (type) {
|
||||
SignerType.SIGN_EVENT -> "sign_event"
|
||||
SignerType.NIP04_ENCRYPT -> "nip04_encrypt"
|
||||
SignerType.NIP04_DECRYPT -> "nip04_decrypt"
|
||||
SignerType.NIP44_ENCRYPT -> "nip44_encrypt"
|
||||
SignerType.NIP44_DECRYPT -> "nip44_decrypt"
|
||||
SignerType.GET_PUBLIC_KEY -> "get_public_key"
|
||||
SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event"
|
||||
}
|
||||
intent.putExtra("type", signerType)
|
||||
intent.putExtra("pubKey", pubKey)
|
||||
intent.putExtra("id", id)
|
||||
if (type !== SignerType.GET_PUBLIC_KEY) {
|
||||
intent.putExtra("current_user", account.keyPair.pubKey.toNpub())
|
||||
}
|
||||
intent.`package` = "com.greenart7c3.nostrsigner"
|
||||
intentResult.launch(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
Amethyst.instance.getString(R.string.error_opening_external_signer),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openSigner(event: EventInterface, columnName: String = "signature") {
|
||||
checkNotInMainThread()
|
||||
|
||||
val result = getDataFromResolver(SignerType.SIGN_EVENT, arrayOf(event.toJson(), event.pubKey()), columnName)
|
||||
if (result !== null) {
|
||||
content.put(event.id(), result)
|
||||
return
|
||||
}
|
||||
|
||||
ServiceManager.shouldPauseService = false
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
event.toJson(),
|
||||
SignerType.SIGN_EVENT,
|
||||
activityResultLauncher,
|
||||
"",
|
||||
event.id()
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptBlockList(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
return
|
||||
}
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
blockListResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
fun getDataFromResolver(signerType: SignerType, data: Array<out String>, columnName: String = "signature"): String? {
|
||||
val localData = if (signerType !== SignerType.GET_PUBLIC_KEY) {
|
||||
data.toList().plus(account.keyPair.pubKey.toNpub()).toTypedArray()
|
||||
} else {
|
||||
data
|
||||
}
|
||||
|
||||
Amethyst.instance.contentResolver.query(
|
||||
Uri.parse("content://com.greenart7c3.nostrsigner.$signerType"),
|
||||
localData,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use {
|
||||
if (it !== null) {
|
||||
if (it.moveToFirst()) {
|
||||
val index = it.getColumnIndex(columnName)
|
||||
if (index < 0) {
|
||||
Log.d("getDataFromResolver", "column '$columnName' not found")
|
||||
return null
|
||||
}
|
||||
return it.getString(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun decrypt(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
return
|
||||
}
|
||||
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptDM(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
return
|
||||
}
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
fun decryptBookmark(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
return
|
||||
}
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
fun encrypt(decryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_ENCRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey))
|
||||
if (result !== null) {
|
||||
content.put(id, result)
|
||||
return
|
||||
}
|
||||
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
decryptedContent,
|
||||
signerType,
|
||||
activityResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptZapEvent(event: LnZapRequestEvent) {
|
||||
val result = getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey))
|
||||
if (result !== null) {
|
||||
content.put(event.id, result)
|
||||
cachedDecryptedContent[event.id] = result
|
||||
return
|
||||
}
|
||||
openSigner(
|
||||
event.toJson(),
|
||||
SignerType.DECRYPT_ZAP_EVENT,
|
||||
decryptResultLauncher,
|
||||
event.pubKey,
|
||||
event.id
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,16 @@ class FileHeader(
|
||||
val size: Int,
|
||||
val dim: String?,
|
||||
val blurHash: String?,
|
||||
val description: String? = null,
|
||||
val alt: String? = null,
|
||||
val sensitiveContent: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
suspend fun prepare(fileUrl: String, mimeType: String?, description: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||
suspend fun prepare(fileUrl: String, mimeType: String?, alt: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||
try {
|
||||
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
||||
|
||||
if (imageData != null) {
|
||||
prepare(imageData, fileUrl, mimeType, description, sensitiveContent, onReady, onError)
|
||||
prepare(imageData, fileUrl, mimeType, alt, sensitiveContent, onReady, onError)
|
||||
} else {
|
||||
onError()
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class FileHeader(
|
||||
data: ByteArray,
|
||||
fileUrl: String,
|
||||
mimeType: String?,
|
||||
description: String?,
|
||||
alt: String?,
|
||||
sensitiveContent: Boolean,
|
||||
onReady: (FileHeader) -> Unit,
|
||||
onError: () -> Unit
|
||||
@@ -79,7 +79,7 @@ class FileHeader(
|
||||
Pair(null, null)
|
||||
}
|
||||
|
||||
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, description, sensitiveContent))
|
||||
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, alt, sensitiveContent))
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||
onError()
|
||||
|
||||
@@ -2,29 +2,40 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
|
||||
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.events.StatusEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
|
||||
object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
lateinit var account: Account
|
||||
|
||||
val latestEOSEs = EOSEAccount()
|
||||
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
|
||||
|
||||
fun createAccountContactListFilter(): TypedFilter {
|
||||
return TypedFilter(
|
||||
@@ -132,10 +143,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
)
|
||||
|
||||
val accountChannel = requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
|
||||
if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
|
||||
} else {
|
||||
hasLoadedTheBasics[account.userProfile()] = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun consume(event: Event, relay: Relay) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (LocalCache.justVerify(event)) {
|
||||
if (event is GiftWrapEvent) {
|
||||
val privateKey = account.keyPair.privKey
|
||||
@@ -143,6 +160,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
event.cachedGift(privateKey)?.let {
|
||||
this.consume(it, relay)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGift(account.keyPair.pubKey, cached)?.let {
|
||||
this.consume(it, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +183,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
event.cachedGossip(privateKey)?.let {
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't store sealed gossips to avoid rebroadcasting by mistake.
|
||||
@@ -162,18 +207,28 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
// gets everthing about the user logged in
|
||||
accountChannel.typedFilters = listOf(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createNotificationFilter(),
|
||||
createGiftWrapsToMeFilter(),
|
||||
createAccountReportsFilter(),
|
||||
createAccountAcceptedAwardsFilter(),
|
||||
createAccountBookmarkListFilter(),
|
||||
createAccountLastPostsListFilter()
|
||||
).ifEmpty { null }
|
||||
return if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
// gets everthing about the user logged in
|
||||
accountChannel.typedFilters = listOf(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createNotificationFilter(),
|
||||
createGiftWrapsToMeFilter(),
|
||||
createAccountReportsFilter(),
|
||||
createAccountAcceptedAwardsFilter(),
|
||||
createAccountBookmarkListFilter(),
|
||||
createAccountLastPostsListFilter()
|
||||
).ifEmpty { null }
|
||||
} else {
|
||||
// just the basics.
|
||||
accountChannel.typedFilters = listOf(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createAccountBookmarkListFilter()
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
|
||||
override fun auth(relay: Relay, challenge: String) {
|
||||
@@ -181,7 +236,6 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
|
||||
if (this::account.isInitialized) {
|
||||
val event = account.createAuthEvent(relay, challenge)
|
||||
|
||||
if (event != null) {
|
||||
Client.send(
|
||||
event,
|
||||
|
||||
@@ -44,7 +44,7 @@ class NostrLnZapPaymentResponseDataSource(
|
||||
override fun auth(relay: Relay, challenge: String) {
|
||||
super.auth(relay, challenge)
|
||||
|
||||
val event = RelayAuthEvent.create(relay.url, challenge, authSigningKey)
|
||||
val event = RelayAuthEvent.create(relay.url, challenge, "", authSigningKey)
|
||||
Client.send(
|
||||
event,
|
||||
relay.url
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object PackageUtils {
|
||||
private fun isPackageInstalled(context: Context, target: String): Boolean {
|
||||
return context.packageManager.getInstalledApplications(0).find { info -> info.packageName == target } != null
|
||||
}
|
||||
|
||||
fun isOrbotInstalled(context: Context): Boolean {
|
||||
return isPackageInstalled(context, "org.torproject.android")
|
||||
}
|
||||
|
||||
fun isAmberInstalled(context: Context): Boolean {
|
||||
return isPackageInstalled(context, "com.greenart7c3.nostrsigner")
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class ZapPaymentHandler(val account: Account) {
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
context: Context,
|
||||
onError: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
|
||||
zapType: LnZapEvent.ZapType
|
||||
@@ -48,7 +48,10 @@ class ZapPaymentHandler(val account: Account) {
|
||||
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||
|
||||
if (lud16.isNullOrBlank()) {
|
||||
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
|
||||
onError(
|
||||
context.getString(R.string.missing_lud16),
|
||||
context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats)
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
@@ -121,6 +124,9 @@ class ZapPaymentHandler(val account: Account) {
|
||||
)
|
||||
} else {
|
||||
onError(
|
||||
context.getString(
|
||||
R.string.missing_lud16
|
||||
),
|
||||
context.getString(
|
||||
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
|
||||
@@ -149,7 +155,7 @@ class ZapPaymentHandler(val account: Account) {
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
context: Context,
|
||||
onError: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayInvoiceThroughIntent: (String) -> Unit,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
@@ -181,9 +187,13 @@ class ZapPaymentHandler(val account: Account) {
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: "Error parsing error message"
|
||||
context.getString(R.string.error_dialog_pay_invoice_error),
|
||||
context.getString(
|
||||
R.string.wallet_connect_pay_invoice_error_error,
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: "Error parsing error message"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onProgress(1f)
|
||||
@@ -192,16 +202,13 @@ class ZapPaymentHandler(val account: Account) {
|
||||
)
|
||||
onProgress(0.8f)
|
||||
} else {
|
||||
try {
|
||||
onPayInvoiceThroughIntent(it)
|
||||
} catch (e: Exception) {
|
||||
onError(context.getString(R.string.lightning_wallets_not_found2))
|
||||
}
|
||||
onPayInvoiceThroughIntent(it)
|
||||
onProgress(0f)
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = onProgress
|
||||
onProgress = onProgress,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.vitorpamplona.amethyst.service.lnurl
|
||||
|
||||
import android.content.Context
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
@@ -31,13 +33,24 @@ class LightningAddressResolver() {
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun fetchLightningAddressJson(lnaddress: String, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
|
||||
private suspend fun fetchLightningAddressJson(
|
||||
lnaddress: String,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
context: Context
|
||||
) = withContext(Dispatchers.IO) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val url = assembleUrl(lnaddress)
|
||||
|
||||
if (url == null) {
|
||||
onError("Could not assemble LNUrl from Lightning Address \"${lnaddress}\". Check the user's setup")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup,
|
||||
lnaddress
|
||||
)
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
@@ -51,16 +64,39 @@ class LightningAddressResolver() {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server is up and if the lightning address is correct")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string.the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct,
|
||||
url,
|
||||
lnaddress,
|
||||
it.code.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onError("Could not resolve $url. Check if the server is up and if the lightning address $lnaddress is correct")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct,
|
||||
url,
|
||||
lnaddress
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
|
||||
suspend fun fetchLightningInvoice(
|
||||
lnCallback: String,
|
||||
milliSats: Long,
|
||||
message: String,
|
||||
nostrRequest: String? = null,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
context: Context
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val encodedMessage = URLEncoder.encode(message, "utf-8")
|
||||
|
||||
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
|
||||
@@ -80,18 +116,22 @@ class LightningAddressResolver() {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("Could not fetch invoice from $lnCallback")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.could_not_fetch_invoice_from, lnCallback)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) {
|
||||
fetchLightningAddressJson(
|
||||
lnaddress,
|
||||
onSuccess = {
|
||||
onSuccess(it.toByteArray().toLnUrl())
|
||||
},
|
||||
onError = onError
|
||||
onError = onError,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
@@ -101,8 +141,9 @@ class LightningAddressResolver() {
|
||||
message: String,
|
||||
nostrRequest: String? = null,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit
|
||||
onError: (String, String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
context: Context
|
||||
) {
|
||||
val mapper = jacksonObjectMapper()
|
||||
|
||||
@@ -114,14 +155,20 @@ class LightningAddressResolver() {
|
||||
val lnurlp = try {
|
||||
mapper.readTree(lnAddressJson)
|
||||
} catch (t: Throwable) {
|
||||
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup)
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
val callback = lnurlp?.get("callback")?.asText()
|
||||
|
||||
if (callback == null) {
|
||||
onError("Callback URL not found in the User's lightning address server configuration")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration)
|
||||
)
|
||||
}
|
||||
|
||||
val allowsNostr = lnurlp?.get("allowsNostr")?.asBoolean() ?: false
|
||||
@@ -138,7 +185,10 @@ class LightningAddressResolver() {
|
||||
val lnInvoice = try {
|
||||
mapper.readTree(it)
|
||||
} catch (t: Throwable) {
|
||||
onError("Error Parsing JSON from Lightning Address's invoice fetch. Check the user's lightning setup")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup)
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
@@ -151,21 +201,40 @@ class LightningAddressResolver() {
|
||||
onSuccess(pr)
|
||||
} else {
|
||||
onProgress(0.0f)
|
||||
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from $lnaddress. It should have been $expectedAmountInSats")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string.incorrect_invoice_amount_sats_from_it_should_have_been,
|
||||
invoiceAmount.toLong().toString(),
|
||||
lnaddress,
|
||||
expectedAmountInSats.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
} ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason ->
|
||||
onProgress(0.0f)
|
||||
onError("Unable to create a lightning invoice before sending the zap. The receiver's lightning wallet sent the following error: $reason")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error,
|
||||
reason
|
||||
)
|
||||
)
|
||||
} ?: run {
|
||||
onProgress(0.0f)
|
||||
onError("nable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON.")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = onError
|
||||
onError = onError,
|
||||
context
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = onError
|
||||
onError = onError,
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.SignerType
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
@@ -32,27 +34,57 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
// Test with all logged in accounts
|
||||
LocalPreferences.allSavedAccounts().forEach {
|
||||
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||
if (acc != null && acc.keyPair.privKey != null) {
|
||||
if (acc != null && (acc.keyPair.privKey != null || acc.loginWithExternalSigner)) {
|
||||
consumeIfMatchesAccount(event, acc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun consumeIfMatchesAccount(pushWrappedEvent: GiftWrapEvent, account: Account) {
|
||||
val key = account.keyPair.privKey ?: return
|
||||
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
|
||||
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
|
||||
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
|
||||
val key = account.keyPair.privKey
|
||||
if (account.loginWithExternalSigner) {
|
||||
ExternalSignerUtils.account = account
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
pushWrappedEvent.content,
|
||||
pushWrappedEvent.pubKey,
|
||||
pushWrappedEvent.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id] ?: ""
|
||||
}
|
||||
pushWrappedEvent.unwrap(cached)?.let { notificationEvent ->
|
||||
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
|
||||
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
|
||||
|
||||
LocalCache.justConsume(notificationEvent, null)
|
||||
LocalCache.justConsume(notificationEvent, null)
|
||||
|
||||
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
notify(innerEvent, account)
|
||||
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (key != null) {
|
||||
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
|
||||
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
|
||||
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
|
||||
|
||||
LocalCache.justConsume(notificationEvent, null)
|
||||
|
||||
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,17 +95,54 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
|
||||
return when (event) {
|
||||
is GiftWrapEvent -> {
|
||||
val key = account.keyPair.privKey ?: return null
|
||||
event.cachedGift(key)?.let {
|
||||
unwrapAndConsume(it, account)
|
||||
val key = account.keyPair.privKey
|
||||
if (key != null) {
|
||||
event.cachedGift(key)?.let {
|
||||
unwrapAndConsume(it, account)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGift(account.keyPair.pubKey, cached)?.let {
|
||||
unwrapAndConsume(it, account)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is SealedGossipEvent -> {
|
||||
val key = account.keyPair.privKey ?: return null
|
||||
event.cachedGossip(key)?.let {
|
||||
// this is not verifiable
|
||||
LocalCache.justConsume(it, null)
|
||||
it
|
||||
val key = account.keyPair.privKey
|
||||
if (key != null) {
|
||||
event.cachedGossip(key)?.let {
|
||||
// this is not verifiable
|
||||
LocalCache.justConsume(it, null)
|
||||
it
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
|
||||
LocalCache.justConsume(it, null)
|
||||
it
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Log
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -23,7 +24,10 @@ class RegisterAccounts(
|
||||
): List<RelayAuthEvent> {
|
||||
return accounts.mapNotNull {
|
||||
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||
if (acc != null && acc.isWriteable()) {
|
||||
if (acc != null && (acc.isWriteable() || acc.loginWithExternalSigner)) {
|
||||
if (acc.loginWithExternalSigner) {
|
||||
ExternalSignerUtils.account = acc
|
||||
}
|
||||
val relayToUse = acc.activeRelays()?.firstOrNull { it.read }
|
||||
if (relayToUse != null) {
|
||||
acc.createAuthEvent(relayToUse, notificationToken)
|
||||
@@ -36,7 +40,7 @@ class RegisterAccounts(
|
||||
}
|
||||
}
|
||||
|
||||
private fun postRegistrationEvent(events: List<RelayAuthEvent>) {
|
||||
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
|
||||
try {
|
||||
val jsonObject = """{
|
||||
"events": [ ${events.joinToString(", ") { it.toJson() }} ]
|
||||
@@ -66,5 +70,6 @@ class RegisterAccounts(
|
||||
postRegistrationEvent(
|
||||
signEventsToProveControlOfAccounts(accounts, notificationToken)
|
||||
)
|
||||
PushNotificationUtils.hasInit = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
private const val ELEMENT_TAG_META = "meta"
|
||||
private const val ATTRIBUTE_VALUE_PROPERTY = "property"
|
||||
@@ -64,7 +65,11 @@ suspend fun getDocument(url: String, timeOut: Int = 30000): Document =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request: Request = Request.Builder().url(url).get().build()
|
||||
val html = HttpClient.getHttpClient().newCall(request).execute().use {
|
||||
it.body.string()
|
||||
if (it.isSuccessful) {
|
||||
it.body.string()
|
||||
} else {
|
||||
throw IllegalArgumentException("Website returned: " + it.code)
|
||||
}
|
||||
}
|
||||
|
||||
Jsoup.parse(html)
|
||||
|
||||
@@ -16,13 +16,14 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
||||
import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting
|
||||
@@ -51,9 +52,10 @@ import java.nio.charset.StandardCharsets
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ExternalSignerUtils.start(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
LocalPreferences.migrateSingleUserPrefs()
|
||||
val language = LocalPreferences.getPreferredLanguage()
|
||||
if (language.isNotBlank()) {
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(language)
|
||||
@@ -68,7 +70,7 @@ class MainActivity : AppCompatActivity() {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel {
|
||||
AccountStateViewModel(this@MainActivity)
|
||||
@@ -99,11 +101,13 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
// Only starts after login
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.start(this@MainActivity)
|
||||
if (ServiceManager.shouldPauseService) {
|
||||
ServiceManager.start(this@MainActivity)
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
PushNotificationUtils().init(LocalPreferences.allSavedAccounts())
|
||||
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +117,9 @@ class MainActivity : AppCompatActivity() {
|
||||
debugState(this)
|
||||
// }
|
||||
|
||||
ServiceManager.pause()
|
||||
if (ServiceManager.shouldPauseService) {
|
||||
ServiceManager.pause()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -145,8 +151,10 @@ class MainActivity : AppCompatActivity() {
|
||||
Log.d("NETWORKCALLBACK", "onAvailable: Disconnecting and connecting again")
|
||||
// Only starts after login
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.pause()
|
||||
ServiceManager.start(this@MainActivity)
|
||||
if (ServiceManager.shouldPauseService) {
|
||||
ServiceManager.pause()
|
||||
ServiceManager.start(this@MainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +184,9 @@ class MainActivity : AppCompatActivity() {
|
||||
Log.d("NETWORKCALLBACK", "onLost: Disconnecting and pausing relay's connection")
|
||||
// Only starts after login
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.pause()
|
||||
if (ServiceManager.shouldPauseService) {
|
||||
ServiceManager.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
|
||||
@Composable
|
||||
fun InformationDialog(
|
||||
title: String,
|
||||
textContent: String,
|
||||
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
SelectionContainer {
|
||||
Text(textContent)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = onDismiss, colors = buttonColors, contentPadding = PaddingValues(horizontal = Size16dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Done,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(stringResource(R.string.error_dialog_button_ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -16,15 +16,15 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -54,10 +54,12 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.note.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
@@ -133,7 +135,7 @@ fun JoinUserOrChannelView(searchBarViewModel: SearchBarViewModel, onClose: () ->
|
||||
|
||||
Text(
|
||||
text = "",
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
@@ -194,7 +196,7 @@ private fun RenderSearch(
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
DisposableEffect(lifeCycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Join Start")
|
||||
@@ -270,7 +272,7 @@ private fun SearchEditTextForJoin(
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.channel_list_user_or_group_id_demo),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
@@ -303,6 +305,13 @@ private fun RenderSearchResults(
|
||||
val users by searchBarViewModel.searchResultsUsers.collectAsState()
|
||||
val channels by searchBarViewModel.searchResultsChannels.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -337,7 +346,7 @@ private fun RenderSearchResults(
|
||||
channels,
|
||||
key = { _, item -> "c" + item.idHex }
|
||||
) { _, item ->
|
||||
RenderChannel(item) {
|
||||
RenderChannel(item, automaticallyShowProfilePicture) {
|
||||
nav("Channel/${item.idHex}")
|
||||
searchBarViewModel.clear()
|
||||
}
|
||||
@@ -350,6 +359,7 @@ private fun RenderSearchResults(
|
||||
@Composable
|
||||
private fun RenderChannel(
|
||||
item: com.vitorpamplona.amethyst.model.Channel,
|
||||
loadProfilePicture: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val hasNewMessages = remember {
|
||||
@@ -368,7 +378,8 @@ private fun RenderChannel(
|
||||
channelLastTime = null,
|
||||
channelLastContent = item.summary(),
|
||||
hasNewMessages,
|
||||
onClick = onClick
|
||||
onClick = onClick,
|
||||
loadProfilePicture = loadProfilePicture
|
||||
)
|
||||
}
|
||||
|
||||
@@ -426,7 +437,7 @@ private fun DisplayUserAboutInfo(baseUser: User) {
|
||||
|
||||
Text(
|
||||
text = about,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
@@ -10,11 +10,11 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -75,7 +75,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.my_awesome_group),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -94,7 +94,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "http://mygroup.com/logo.jpg",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -111,7 +111,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.about_us),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -22,11 +23,11 @@ open class NewMediaModel : ViewModel() {
|
||||
var account: Account? = null
|
||||
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
val imageUploadingError = MutableSharedFlow<String?>()
|
||||
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
var mediaType by mutableStateOf<String?>(null)
|
||||
|
||||
var selectedServer by mutableStateOf<ServersAvailable?>(null)
|
||||
var description by mutableStateOf("")
|
||||
var alt by mutableStateOf("")
|
||||
var sensitiveContent by mutableStateOf(false)
|
||||
|
||||
// Images and Videos
|
||||
@@ -79,7 +80,7 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.2f
|
||||
uploadingDescription.value = "Loading"
|
||||
contentResolver.openInputStream(fileUri)?.use {
|
||||
createNIP95Record(it.readBytes(), contentType, description, sensitiveContent, relayList = relayList)
|
||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
||||
}
|
||||
?: run {
|
||||
viewModelScope.launch {
|
||||
@@ -92,24 +93,32 @@ open class NewMediaModel : ViewModel() {
|
||||
} else {
|
||||
uploadingPercentage.value = 0.2f
|
||||
uploadingDescription.value = "Uploading"
|
||||
ImageUploader.uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = serverToUse,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
createNIP94Record(imageUrl, mimeType, description, sensitiveContent, relayList = relayList)
|
||||
},
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader.uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = serverToUse,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
createNIP94Record(
|
||||
imageUrl,
|
||||
mimeType,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
relayList = relayList
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
@@ -131,7 +140,7 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingDescription.value = null
|
||||
uploadingPercentage.value = 0.0f
|
||||
|
||||
description = ""
|
||||
alt = ""
|
||||
selectedServer = account?.defaultFileServer
|
||||
}
|
||||
|
||||
@@ -139,7 +148,7 @@ open class NewMediaModel : ViewModel() {
|
||||
return !isUploadingImage && galleryUri != null && selectedServer != null
|
||||
}
|
||||
|
||||
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP94Record(imageUrl: String, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
uploadingPercentage.value = 0.40f
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
uploadingDescription.value = "Server Processing"
|
||||
@@ -158,7 +167,7 @@ open class NewMediaModel : ViewModel() {
|
||||
imageData,
|
||||
imageUrl,
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
uploadingPercentage.value = 0.90f
|
||||
@@ -192,7 +201,7 @@ open class NewMediaModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
uploadingPercentage.value = 0.30f
|
||||
uploadingDescription.value = "Hashing"
|
||||
|
||||
@@ -201,7 +210,7 @@ open class NewMediaModel : ViewModel() {
|
||||
bytes,
|
||||
"",
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
uploadingDescription.value = "Signing"
|
||||
|
||||
@@ -21,12 +21,12 @@ import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -134,7 +135,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
||||
painter = painterResource(R.drawable.relays),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(25.dp),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -186,8 +187,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
||||
)
|
||||
|
||||
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() }
|
||||
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
|
||||
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
||||
val resolver = LocalContext.current.contentResolver
|
||||
|
||||
Row(
|
||||
@@ -252,7 +252,6 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
label = stringResource(id = R.string.file_server),
|
||||
placeholder = fileServers.firstOrNull { it.first == accountViewModel.account.defaultFileServer }?.second ?: fileServers[0].second,
|
||||
options = fileServerOptions,
|
||||
explainers = fileServerExplainers,
|
||||
onSelect = {
|
||||
postViewModel.selectedServer = fileServers[it].first
|
||||
},
|
||||
@@ -288,12 +287,12 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
||||
value = postViewModel.description,
|
||||
onValueChange = { postViewModel.description = it },
|
||||
value = postViewModel.alt,
|
||||
onValueChange = { postViewModel.alt = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.content_description_example),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
@@ -13,7 +13,8 @@ class NewMessageTagger(
|
||||
var message: String,
|
||||
var mentions: List<User>? = null,
|
||||
var replyTos: List<Note>? = null,
|
||||
var channelHex: String? = null
|
||||
var channelHex: String? = null,
|
||||
var dao: Dao
|
||||
) {
|
||||
|
||||
val directMentions = mutableSetOf<HexKey>()
|
||||
@@ -40,20 +41,20 @@ class NewMessageTagger(
|
||||
return (if (channelHex != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
|
||||
}
|
||||
|
||||
fun run() {
|
||||
suspend fun run() {
|
||||
// adds all references to mentions and reply tos
|
||||
message.split('\n').forEach { paragraph: String ->
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
val results = parseDirtyWordForKey(word)
|
||||
|
||||
if (results?.key?.type == Nip19.Type.USER) {
|
||||
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
|
||||
addUserToMentions(dao.getOrCreateUser(results.key.hex))
|
||||
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
||||
addNoteToReplyTos(dao.getOrCreateNote(results.key.hex))
|
||||
} else if (results?.key?.type == Nip19.Type.EVENT) {
|
||||
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
||||
addNoteToReplyTos(dao.getOrCreateNote(results.key.hex))
|
||||
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
val note = dao.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
if (note != null) {
|
||||
addNoteToReplyTos(note)
|
||||
}
|
||||
@@ -66,19 +67,19 @@ class NewMessageTagger(
|
||||
paragraph.split(' ').map { word: String ->
|
||||
val results = parseDirtyWordForKey(word)
|
||||
if (results?.key?.type == Nip19.Type.USER) {
|
||||
val user = LocalCache.getOrCreateUser(results.key.hex)
|
||||
val user = dao.getOrCreateUser(results.key.hex)
|
||||
|
||||
"nostr:${user.pubkeyNpub()}${results.restOfWord}"
|
||||
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||
val note = dao.getOrCreateNote(results.key.hex)
|
||||
|
||||
"nostr:${note.toNEvent()}${results.restOfWord}"
|
||||
} else if (results?.key?.type == Nip19.Type.EVENT) {
|
||||
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||
val note = dao.getOrCreateNote(results.key.hex)
|
||||
|
||||
"nostr:${note.toNEvent()}${results.restOfWord}"
|
||||
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
val note = dao.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
if (note != null) {
|
||||
"nostr:${note.idNote()}${results.restOfWord}"
|
||||
} else {
|
||||
@@ -91,6 +92,7 @@ class NewMessageTagger(
|
||||
}.joinToString("\n")
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class DirtyKeyInfo(val key: Nip19.Return, val restOfWord: String)
|
||||
|
||||
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
|
||||
@@ -155,3 +157,9 @@ class NewMessageTagger(
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface Dao {
|
||||
suspend fun getOrCreateUser(hex: String): User
|
||||
suspend fun getOrCreateNote(hex: String): Note
|
||||
suspend fun checkGetOrCreateAddressableNote(hex: String): Note?
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -38,13 +38,13 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
|
||||
} catch (e: Exception) { pollViewModel.isValidClosedAt.value = false }
|
||||
}
|
||||
|
||||
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.error,
|
||||
val colorInValid = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.error,
|
||||
unfocusedBorderColor = Color.Red
|
||||
)
|
||||
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colors.placeholderText
|
||||
val colorValid = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -60,13 +60,13 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_closing_time),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_closing_time_days),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,10 +5,10 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -38,13 +38,13 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
|
||||
} catch (e: Exception) { pollViewModel.isValidConsensusThreshold.value = false }
|
||||
}
|
||||
|
||||
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.error,
|
||||
val colorInValid = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.error,
|
||||
unfocusedBorderColor = Color.Red
|
||||
)
|
||||
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colors.placeholderText
|
||||
val colorValid = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -60,13 +60,13 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_consensus_threshold),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_consensus_threshold_percent),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -36,13 +36,13 @@ fun NewPollOption(pollViewModel: NewPostViewModel, optionIndex: Int) {
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_option_index).format(optionIndex + 1),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_option_description),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -33,13 +33,13 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
|
||||
isInputValid = false
|
||||
}
|
||||
|
||||
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.error,
|
||||
val colorInValid = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.error,
|
||||
unfocusedBorderColor = Color.Red
|
||||
)
|
||||
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colors.placeholderText
|
||||
val colorValid = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
@@ -50,7 +50,7 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_primary_description),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -68,11 +68,11 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_primary_description),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
colors = if (isInputValid) colorValid else colorInValid,
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -30,13 +30,13 @@ fun NewPollRecipientsField(pollViewModel: NewPostViewModel, account: Account) {
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_zap_recipients),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_zap_recipients),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -26,13 +26,13 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
|
||||
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.error,
|
||||
val colorInValid = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.error,
|
||||
unfocusedBorderColor = Color.Red
|
||||
)
|
||||
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = MaterialTheme.colors.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colors.placeholderText
|
||||
val colorValid = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -48,13 +48,13 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_zap_value_min),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.sats),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -70,13 +70,13 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_zap_value_max),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.sats),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -88,7 +88,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_zap_value_min_max_explainer),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowForwardIos
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
@@ -32,6 +31,7 @@ import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -46,7 +46,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -82,6 +81,7 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
||||
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
|
||||
import com.vitorpamplona.amethyst.service.startsWithNIP19Scheme
|
||||
import com.vitorpamplona.amethyst.ui.components.*
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.CancelIcon
|
||||
@@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
@@ -118,6 +119,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Math.round
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewPostView(
|
||||
onClose: () -> Unit,
|
||||
@@ -142,7 +144,7 @@ fun NewPostView(
|
||||
var relayList = account.activeWriteRelays()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.load(account, baseReplyTo, quote)
|
||||
postViewModel.load(accountViewModel, baseReplyTo, quote)
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
postViewModel.imageUploadingError.collect { error ->
|
||||
@@ -186,9 +188,7 @@ fun NewPostView(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 10.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -205,14 +205,15 @@ fun NewPostView(
|
||||
painter = painterResource(R.drawable.relays),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(25.dp),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
PostButton(
|
||||
onPost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.sendPost(relayList = relayList)
|
||||
postViewModel.sendPost(relayList = relayList)
|
||||
scope.launch {
|
||||
delay(100)
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
@@ -221,14 +222,20 @@ fun NewPostView(
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
CloseButton(onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
})
|
||||
Row() {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
CloseButton(onPress = {
|
||||
postViewModel.cancel()
|
||||
scope.launch {
|
||||
delay(100)
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
backgroundColor = MaterialTheme.colors.surface,
|
||||
elevation = 0.dp
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { pad ->
|
||||
@@ -249,7 +256,6 @@ fun NewPostView(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(top = 10.dp)
|
||||
.imePadding()
|
||||
.weight(1f)
|
||||
) {
|
||||
@@ -270,7 +276,7 @@ fun NewPostView(
|
||||
makeItShort = true,
|
||||
unPackReply = false,
|
||||
isQuotedNote = true,
|
||||
modifier = MaterialTheme.colors.replyModifier,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@@ -339,8 +345,8 @@ fun NewPostView(
|
||||
ImageVideoDescription(
|
||||
url,
|
||||
account.defaultFileServer,
|
||||
onAdd = { description, server, sensitiveContent ->
|
||||
postViewModel.upload(url, description, sensitiveContent, server, context, relayList)
|
||||
onAdd = { alt, server, sensitiveContent ->
|
||||
postViewModel.upload(url, alt, sensitiveContent, server, context, relayList)
|
||||
account.changeDefaultFileServer(server)
|
||||
},
|
||||
onCancel = {
|
||||
@@ -374,6 +380,9 @@ fun NewPostView(
|
||||
},
|
||||
onClose = {
|
||||
postViewModel.wantsInvoice = false
|
||||
},
|
||||
onError = { title, message ->
|
||||
accountViewModel.toast(title, message)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -406,7 +415,7 @@ fun NewPostView(
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colors.subtleBorder,
|
||||
MaterialTheme.colorScheme.subtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
)
|
||||
@@ -416,7 +425,7 @@ fun NewPostView(
|
||||
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
|
||||
}
|
||||
} else if (startsWithNIP19Scheme(myUrlPreview)) {
|
||||
val bgColor = MaterialTheme.colors.background
|
||||
val bgColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember {
|
||||
mutableStateOf(bgColor)
|
||||
}
|
||||
@@ -463,7 +472,7 @@ fun NewPostView(
|
||||
) {
|
||||
UploadFromGallery(
|
||||
isUploading = postViewModel.isUploadingImage,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier
|
||||
) {
|
||||
postViewModel.selectImage(it)
|
||||
@@ -527,10 +536,10 @@ private fun PollField(postViewModel: NewPostViewModel) {
|
||||
},
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
MaterialTheme.colors.placeholderText
|
||||
MaterialTheme.colorScheme.placeholderText
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colors.placeholderText
|
||||
containerColor = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
) {
|
||||
Image(
|
||||
@@ -542,7 +551,7 @@ private fun PollField(postViewModel: NewPostViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MessageField(
|
||||
postViewModel: NewPostViewModel
|
||||
@@ -569,7 +578,7 @@ private fun MessageField(
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colors.surface,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.focusRequester(focusRequester)
|
||||
@@ -581,15 +590,14 @@ private fun MessageField(
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.what_s_on_your_mind),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults
|
||||
.outlinedTextFieldColors(
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
focusedBorderColor = Color.Transparent
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
}
|
||||
@@ -640,7 +648,7 @@ fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) {
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.add_sensitive_content_explainer),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
@@ -670,18 +678,16 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.messages_new_message_to_caption),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
visualTransformation = UrlUserTagTransformation(
|
||||
MaterialTheme.colors.primary
|
||||
MaterialTheme.colorScheme.primary
|
||||
),
|
||||
colors = TextFieldDefaults
|
||||
.outlinedTextFieldColors(
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
focusedBorderColor = Color.Transparent
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
focusedBorderColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -707,18 +713,16 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.messages_new_message_subject_caption),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
visualTransformation = UrlUserTagTransformation(
|
||||
MaterialTheme.colors.primary
|
||||
MaterialTheme.colorScheme.primary
|
||||
),
|
||||
colors = TextFieldDefaults
|
||||
.outlinedTextFieldColors(
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
focusedBorderColor = Color.Transparent
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
focusedBorderColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -772,7 +776,7 @@ fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewMo
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.zap_split_explainer),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
|
||||
@@ -817,12 +821,12 @@ fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewMo
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.zap_split_serarch_and_add_user_placeholder),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
visualTransformation = UrlUserTagTransformation(
|
||||
MaterialTheme.colors.primary
|
||||
MaterialTheme.colorScheme.primary
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
@@ -870,7 +874,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -890,7 +894,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.geohash_explainer),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
@@ -951,7 +955,7 @@ fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
|
||||
Text(
|
||||
stringResource(R.string.reply_notify),
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.align(CenterVertically)
|
||||
)
|
||||
|
||||
@@ -965,7 +969,7 @@ fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.mediumImportanceLink
|
||||
containerColor = MaterialTheme.colorScheme.mediumImportanceLink
|
||||
),
|
||||
onClick = {
|
||||
onClick(myUser)
|
||||
@@ -1024,7 +1028,7 @@ private fun AddZapraiserButton(
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.align(Alignment.TopStart),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
@@ -1032,7 +1036,7 @@ private fun AddZapraiserButton(
|
||||
modifier = Modifier
|
||||
.size(13.dp)
|
||||
.align(Alignment.BottomEnd),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
@@ -1068,14 +1072,14 @@ fun AddGeoHash(postViewModel: NewPostViewModel, onClick: () -> Unit) {
|
||||
imageVector = Icons.Default.LocationOff,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1096,7 +1100,7 @@ private fun AddLnInvoiceButton(
|
||||
imageVector = Icons.Default.CurrencyBitcoin,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
@@ -1131,7 +1135,7 @@ private fun ForwardZapTo(
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.align(Alignment.CenterStart),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowForwardIos,
|
||||
@@ -1139,7 +1143,7 @@ private fun ForwardZapTo(
|
||||
modifier = Modifier
|
||||
.size(13.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
@@ -1185,7 +1189,7 @@ private fun MarkAsSensitive(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
@@ -1193,7 +1197,7 @@ private fun MarkAsSensitive(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
@@ -1219,15 +1223,9 @@ private fun MarkAsSensitive(
|
||||
|
||||
@Composable
|
||||
fun CloseButton(onPress: () -> Unit) {
|
||||
Button(
|
||||
onClick = {
|
||||
onPress()
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = Color.Gray
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = onPress,
|
||||
contentPadding = PaddingValues(horizontal = Size5dp)
|
||||
) {
|
||||
CloseIcon()
|
||||
}
|
||||
@@ -1237,17 +1235,11 @@ fun CloseButton(onPress: () -> Unit) {
|
||||
fun PostButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
enabled = isActive,
|
||||
onClick = {
|
||||
if (isActive) {
|
||||
onPost()
|
||||
}
|
||||
onPost()
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
shape = ButtonBorder
|
||||
) {
|
||||
Text(text = stringResource(R.string.post), color = Color.White)
|
||||
}
|
||||
@@ -1256,19 +1248,11 @@ fun PostButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier =
|
||||
@Composable
|
||||
fun SaveButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) {
|
||||
Button(
|
||||
enabled = isActive,
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
if (isActive) {
|
||||
onPost()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
onClick = onPost
|
||||
) {
|
||||
Text(text = stringResource(R.string.save), color = Color.White)
|
||||
Text(text = stringResource(R.string.save))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1282,10 +1266,9 @@ fun CreateButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isActive) MaterialTheme.colorScheme.primary else Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.create), color = Color.White)
|
||||
}
|
||||
@@ -1320,8 +1303,7 @@ fun ImageVideoDescription(
|
||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
||||
)
|
||||
|
||||
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() }
|
||||
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
|
||||
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
||||
|
||||
var selectedServer by remember { mutableStateOf(defaultServer) }
|
||||
var message by remember { mutableStateOf("") }
|
||||
@@ -1334,7 +1316,7 @@ fun ImageVideoDescription(
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colors.subtleBorder,
|
||||
MaterialTheme.colorScheme.subtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
) {
|
||||
@@ -1435,7 +1417,6 @@ fun ImageVideoDescription(
|
||||
label = stringResource(id = R.string.file_server),
|
||||
placeholder = fileServers.filter { it.first == defaultServer }.firstOrNull()?.second ?: fileServers[0].second,
|
||||
options = fileServerOptions,
|
||||
explainers = fileServerExplainers,
|
||||
onSelect = {
|
||||
selectedServer = fileServers[it].first
|
||||
},
|
||||
@@ -1476,7 +1457,7 @@ fun ImageVideoDescription(
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.content_description_example),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -1495,7 +1476,7 @@ fun ImageVideoDescription(
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.add_content), color = Color.White, fontSize = 20.sp)
|
||||
@@ -1529,21 +1510,17 @@ fun SettingSwitchItem(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
val contentAlpha = if (enabled) ContentAlpha.high else ContentAlpha.disabled
|
||||
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(contentAlpha)
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = description),
|
||||
style = MaterialTheme.typography.caption,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(contentAlpha)
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.ui.components.Split
|
||||
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
@@ -32,6 +33,7 @@ import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
@@ -45,6 +47,7 @@ enum class UserSuggestionAnchor {
|
||||
|
||||
@Stable
|
||||
open class NewPostViewModel() : ViewModel() {
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
var requiresNIP24: Boolean = false
|
||||
|
||||
@@ -56,7 +59,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
var message by mutableStateOf(TextFieldValue(""))
|
||||
var urlPreview by mutableStateOf<String?>(null)
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
val imageUploadingError = MutableSharedFlow<String?>()
|
||||
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
var userSuggestions by mutableStateOf<List<User>>(emptyList())
|
||||
var userSuggestionAnchor: TextRange? = null
|
||||
@@ -115,7 +118,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
// NIP24 Wrapped DMs / Group messages
|
||||
var nip24 by mutableStateOf(false)
|
||||
|
||||
open fun load(account: Account, replyingTo: Note?, quote: Note?) {
|
||||
open fun load(accountViewModel: AccountViewModel, replyingTo: Note?, quote: Note?) {
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
if (replyNote.event is BaseTextNoteEvent) {
|
||||
@@ -147,8 +150,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
|
||||
canAddInvoice = account.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = account.userProfile().info?.lnAddress() != null
|
||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
|
||||
contentToAddUrl = null
|
||||
|
||||
@@ -160,14 +163,26 @@ open class NewPostViewModel() : ViewModel() {
|
||||
forwardZapTo = Split()
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
this.account = account
|
||||
this.accountViewModel = accountViewModel
|
||||
this.account = accountViewModel.account
|
||||
}
|
||||
|
||||
fun sendPost(relayList: List<Relay>? = null) {
|
||||
val tagger = NewMessageTagger(message.text, mentions, replyTos, originalNote?.channelHex())
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
innerSendPost(relayList)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun innerSendPost(relayList: List<Relay>? = null) {
|
||||
if (accountViewModel == null) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
val tagger = NewMessageTagger(message.text, mentions, replyTos, originalNote?.channelHex(), accountViewModel!!)
|
||||
tagger.run()
|
||||
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null)
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null, accountViewModel!!)
|
||||
toUsersTagger.run()
|
||||
val dmUsers = toUsersTagger.mentions
|
||||
|
||||
@@ -285,7 +300,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
cancel()
|
||||
}
|
||||
|
||||
fun upload(galleryUri: Uri, description: String, sensitiveContent: Boolean, server: ServersAvailable, context: Context, relayList: List<Relay>? = null) {
|
||||
fun upload(galleryUri: Uri, alt: String, sensitiveContent: Boolean, server: ServersAvailable, context: Context, relayList: List<Relay>? = null) {
|
||||
isUploadingImage = true
|
||||
contentToAddUrl = null
|
||||
|
||||
@@ -300,7 +315,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
onReady = { fileUri, contentType, size ->
|
||||
if (server == ServersAvailable.NIP95) {
|
||||
contentResolver.openInputStream(fileUri)?.use {
|
||||
createNIP95Record(it.readBytes(), contentType, description, sensitiveContent, relayList = relayList)
|
||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
||||
}
|
||||
} else {
|
||||
ImageUploader.uploadImage(
|
||||
@@ -311,7 +326,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
if (isNIP94Server(server)) {
|
||||
createNIP94Record(imageUrl, mimeType, description, sensitiveContent)
|
||||
createNIP94Record(imageUrl, mimeType, alt, sensitiveContent)
|
||||
} else {
|
||||
isUploadingImage = false
|
||||
message = TextFieldValue(message.text + "\n\n" + imageUrl)
|
||||
@@ -523,13 +538,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP94Record(imageUrl: String, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Images don't seem to be ready immediately after upload
|
||||
FileHeader.prepare(
|
||||
imageUrl,
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
val note = account?.sendHeader(it, relayList = relayList)
|
||||
@@ -554,13 +569,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
FileHeader.prepare(
|
||||
bytes,
|
||||
"",
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
val nip95 = account?.createNip95(bytes, headerInfo = it)
|
||||
|
||||
@@ -14,16 +14,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
@@ -34,6 +24,18 @@ import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.SyncProblem
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -56,9 +58,11 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.relays.Constants
|
||||
import com.vitorpamplona.amethyst.service.relays.Constants.defaultRelays
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
@@ -81,6 +85,7 @@ import com.vitorpamplona.amethyst.ui.theme.warningColor
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.Math.round
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) {
|
||||
val postViewModel: NewRelayListViewModel = viewModel()
|
||||
@@ -135,8 +140,9 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
|
||||
onClose()
|
||||
})
|
||||
},
|
||||
backgroundColor = MaterialTheme.colors.surface,
|
||||
elevation = 0.dp
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { pad ->
|
||||
@@ -210,7 +216,7 @@ fun ServerConfigHeader() {
|
||||
maxLines = 1,
|
||||
fontSize = Font14SP,
|
||||
modifier = Modifier.weight(1.2f),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(5.dp))
|
||||
@@ -220,7 +226,7 @@ fun ServerConfigHeader() {
|
||||
maxLines = 1,
|
||||
fontSize = Font14SP,
|
||||
modifier = Modifier.weight(1.2f),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(5.dp))
|
||||
@@ -230,7 +236,7 @@ fun ServerConfigHeader() {
|
||||
maxLines = 1,
|
||||
fontSize = Font14SP,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(5.dp))
|
||||
@@ -240,7 +246,7 @@ fun ServerConfigHeader() {
|
||||
maxLines = 1,
|
||||
fontSize = Font14SP,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(2.dp))
|
||||
@@ -258,6 +264,7 @@ fun ServerConfigHeader() {
|
||||
@Composable
|
||||
fun ServerConfigPreview() {
|
||||
ServerConfigClickableLine(
|
||||
loadProfilePicture = true,
|
||||
item = RelaySetupInfo(
|
||||
url = "nostr.mom",
|
||||
read = true,
|
||||
@@ -310,7 +317,16 @@ fun ServerConfig(
|
||||
)
|
||||
}
|
||||
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
ServerConfigClickableLine(
|
||||
loadProfilePicture = automaticallyShowProfilePicture,
|
||||
item = item,
|
||||
onToggleDownload = onToggleDownload,
|
||||
onToggleUpload = onToggleUpload,
|
||||
@@ -334,15 +350,10 @@ fun ServerConfig(
|
||||
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
msg,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.unable_to_download_relay_document),
|
||||
msg
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -351,6 +362,7 @@ fun ServerConfig(
|
||||
|
||||
@Composable
|
||||
fun ServerConfigClickableLine(
|
||||
loadProfilePicture: Boolean,
|
||||
item: RelaySetupInfo,
|
||||
onToggleDownload: (RelaySetupInfo) -> Unit,
|
||||
onToggleUpload: (RelaySetupInfo) -> Unit,
|
||||
@@ -368,7 +380,7 @@ fun ServerConfigClickableLine(
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
) {
|
||||
Column(Modifier.clickable(onClick = onClick)) {
|
||||
RenderRelayIcon(iconUrl = item.briefInfo.favIcon, Size55dp)
|
||||
RenderRelayIcon(iconUrl = item.briefInfo.favIcon, loadProfilePicture, Size55dp)
|
||||
}
|
||||
|
||||
Spacer(modifier = HalfHorzPadding)
|
||||
@@ -441,9 +453,9 @@ private fun RenderStatusRow(
|
||||
}
|
||||
),
|
||||
tint = if (item.read) {
|
||||
MaterialTheme.colors.allGoodColor
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface.copy(
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
}
|
||||
@@ -454,7 +466,7 @@ private fun RenderStatusRow(
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Icon(
|
||||
@@ -477,9 +489,9 @@ private fun RenderStatusRow(
|
||||
}
|
||||
),
|
||||
tint = if (item.write) {
|
||||
MaterialTheme.colors.allGoodColor
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface.copy(
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
}
|
||||
@@ -490,7 +502,7 @@ private fun RenderStatusRow(
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Icon(
|
||||
@@ -512,7 +524,7 @@ private fun RenderStatusRow(
|
||||
}
|
||||
}
|
||||
),
|
||||
tint = if (item.errorCount > 0) MaterialTheme.colors.warningColor else MaterialTheme.colors.allGoodColor
|
||||
tint = if (item.errorCount > 0) MaterialTheme.colorScheme.warningColor else MaterialTheme.colorScheme.allGoodColor
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -520,7 +532,7 @@ private fun RenderStatusRow(
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
Icon(
|
||||
@@ -542,7 +554,7 @@ private fun RenderStatusRow(
|
||||
}
|
||||
}
|
||||
),
|
||||
tint = if (item.spamCount > 0) MaterialTheme.colors.warningColor else MaterialTheme.colors.allGoodColor
|
||||
tint = if (item.spamCount > 0) MaterialTheme.colorScheme.warningColor else MaterialTheme.colorScheme.allGoodColor
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -550,7 +562,7 @@ private fun RenderStatusRow(
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
@@ -571,7 +583,7 @@ private fun RenderActiveToggles(
|
||||
text = stringResource(id = R.string.active_for),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(start = 2.dp, end = 5.dp),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
@@ -601,9 +613,9 @@ private fun RenderActiveToggles(
|
||||
}
|
||||
),
|
||||
tint = if (item.feedTypes.contains(FeedType.FOLLOWS)) {
|
||||
MaterialTheme.colors.allGoodColor
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface.copy(
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
}
|
||||
@@ -634,9 +646,9 @@ private fun RenderActiveToggles(
|
||||
}
|
||||
),
|
||||
tint = if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) {
|
||||
MaterialTheme.colors.allGoodColor
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface.copy(
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
}
|
||||
@@ -667,9 +679,9 @@ private fun RenderActiveToggles(
|
||||
}
|
||||
),
|
||||
tint = if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) {
|
||||
MaterialTheme.colors.allGoodColor
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface.copy(
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
}
|
||||
@@ -700,9 +712,9 @@ private fun RenderActiveToggles(
|
||||
}
|
||||
),
|
||||
tint = if (item.feedTypes.contains(FeedType.GLOBAL)) {
|
||||
MaterialTheme.colors.allGoodColor
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface.copy(
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
}
|
||||
@@ -734,9 +746,9 @@ private fun RenderActiveToggles(
|
||||
}
|
||||
),
|
||||
tint = if (item.feedTypes.contains(FeedType.SEARCH)) {
|
||||
MaterialTheme.colors.allGoodColor
|
||||
MaterialTheme.colorScheme.allGoodColor
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface.copy(
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
}
|
||||
@@ -767,7 +779,7 @@ private fun FirstLine(
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp, top = 1.dp)
|
||||
.size(14.dp),
|
||||
tint = MaterialTheme.colors.allGoodColor
|
||||
tint = MaterialTheme.colorScheme.allGoodColor
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -803,7 +815,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "server.com",
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
@@ -817,7 +829,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
|
||||
modifier = Modifier
|
||||
.size(Size35dp)
|
||||
.padding(horizontal = 5.dp),
|
||||
tint = if (read) MaterialTheme.colors.allGoodColor else MaterialTheme.colors.placeholderText
|
||||
tint = if (read) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
@@ -828,7 +840,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
|
||||
modifier = Modifier
|
||||
.size(Size35dp)
|
||||
.padding(horizontal = 5.dp),
|
||||
tint = if (write) MaterialTheme.colors.allGoodColor else MaterialTheme.colors.placeholderText
|
||||
tint = if (write) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
@@ -846,7 +858,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (url.isNotBlank()) MaterialTheme.colors.primary else MaterialTheme.colors.placeholderText
|
||||
containerColor = if (url.isNotBlank()) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.add), color = Color.White)
|
||||
|
||||
@@ -57,21 +57,30 @@ class NewRelayListViewModel : ViewModel() {
|
||||
_relays.update {
|
||||
var relayFile = account.userProfile().latestContactList?.relays()
|
||||
|
||||
// Ugly, but forces nostr.band as the only search-supporting relay today.
|
||||
// TODO: Remove when search becomes more available.
|
||||
if (relayFile?.none { it.key.removeSuffix("/") in Constants.forcedRelaysForSearchSet } == true) {
|
||||
relayFile = relayFile + Constants.forcedRelayForSearch.map {
|
||||
Pair(
|
||||
it.url,
|
||||
ContactListEvent.ReadWrite(it.read, it.write)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (relayFile != null) {
|
||||
// Ugly, but forces nostr.band as the only search-supporting relay today.
|
||||
// TODO: Remove when search becomes more available.
|
||||
|
||||
val needsSearchRelay = relayFile.none {
|
||||
it.key.removeSuffix("/") in Constants.forcedRelaysForSearchSet
|
||||
} && relayFile.none {
|
||||
account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes?.contains(FeedType.SEARCH) ?: false
|
||||
}
|
||||
|
||||
if (needsSearchRelay) {
|
||||
relayFile = relayFile + Constants.forcedRelayForSearch.map {
|
||||
Pair(
|
||||
it.url,
|
||||
ContactListEvent.ReadWrite(it.read, it.write)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
relayFile.map {
|
||||
val liveRelay = RelayPool.getRelay(it.key)
|
||||
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes ?: FeedType.values().toSet().toImmutableSet()
|
||||
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes
|
||||
?: Constants.defaultRelays.filter { defaultRelay -> defaultRelay.url == it.key }.firstOrNull()?.feedTypes
|
||||
?: FeedType.values().toSet().toImmutableSet()
|
||||
|
||||
val errorCounter = liveRelay?.errorCounter ?: 0
|
||||
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
|
||||
|
||||
@@ -11,10 +11,10 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -82,45 +82,26 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringResource(R.string.display_name)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = postViewModel.displayName.value,
|
||||
onValueChange = { postViewModel.displayName.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.my_display_name),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Text("@", Modifier.padding(5.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringResource(R.string.username)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = postViewModel.userName.value,
|
||||
onValueChange = { postViewModel.userName.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.my_username),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
},
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringResource(R.string.display_name)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.displayName.value,
|
||||
onValueChange = { postViewModel.displayName.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.my_display_name),
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
@@ -134,7 +115,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.about_me),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -153,13 +134,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "https://mywebsite.com/me.jpg",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
UploadFromGallery(
|
||||
isUploading = postViewModel.isUploadingImageForPicture,
|
||||
tint = MaterialTheme.colors.placeholderText,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(start = 5.dp)
|
||||
) {
|
||||
postViewModel.uploadForPicture(it, context)
|
||||
@@ -178,13 +159,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "https://mywebsite.com/mybanner.jpg",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
UploadFromGallery(
|
||||
isUploading = postViewModel.isUploadingImageForBanner,
|
||||
tint = MaterialTheme.colors.placeholderText,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(start = 5.dp)
|
||||
) {
|
||||
postViewModel.uploadForBanner(it, context)
|
||||
@@ -203,7 +184,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "https://mywebsite.com",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
singleLine = true
|
||||
@@ -219,7 +200,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "_@mywebsite.com",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
singleLine = true
|
||||
@@ -234,7 +215,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "me@mylightiningnode.com",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
singleLine = true
|
||||
@@ -250,7 +231,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.lnurl),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -265,7 +246,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.twitter_proof_url_template),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -280,7 +261,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.mastodon_proof_url_template),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -295,7 +276,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.github_proof_url_template),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
@@ -124,12 +125,11 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.sendNewUserMetadata(writer.buffer.toString(), newClaims)
|
||||
}
|
||||
|
||||
clear()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
userName.value = ""
|
||||
// userName.value = ""
|
||||
displayName.value = ""
|
||||
about.value = ""
|
||||
picture.value = ""
|
||||
@@ -183,23 +183,25 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
contentResolver.getType(galleryUri),
|
||||
context.applicationContext,
|
||||
onReady = { fileUri, contentType, size ->
|
||||
ImageUploader.uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = account.defaultFileServer,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
onUploading(false)
|
||||
onUploaded(imageUrl)
|
||||
},
|
||||
onError = {
|
||||
onUploading(false)
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader.uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = account.defaultFileServer,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
onUploading(false)
|
||||
onUploaded(imageUrl)
|
||||
},
|
||||
onError = {
|
||||
onUploading(false)
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
onUploading(false)
|
||||
|
||||
@@ -13,9 +13,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -25,8 +26,10 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
@@ -47,6 +50,14 @@ fun RelayInformationDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
properties = DialogProperties(
|
||||
@@ -77,6 +88,7 @@ fun RelayInformationDialog(
|
||||
Column() {
|
||||
RenderRelayIcon(
|
||||
relayBriefInfo.favIcon,
|
||||
automaticallyShowProfilePicture,
|
||||
Size55dp
|
||||
)
|
||||
}
|
||||
@@ -111,7 +123,15 @@ fun RelayInformationDialog(
|
||||
Section(stringResource(R.string.contact))
|
||||
|
||||
Box(modifier = Modifier.padding(start = 10.dp)) {
|
||||
ClickableEmail(relayInfo.contact ?: "")
|
||||
relayInfo.contact?.let {
|
||||
if (it.startsWith("https:")) {
|
||||
ClickableUrl(urlText = it, url = it)
|
||||
} else if (it.startsWith("mailto:") || it.contains('@')) {
|
||||
ClickableEmail(it)
|
||||
} else {
|
||||
SectionContent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(stringResource(R.string.supports))
|
||||
@@ -248,7 +268,7 @@ private fun DisplayOwnerInformation(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadUser(baseUserHex = userHex) {
|
||||
LoadUser(baseUserHex = userHex, accountViewModel) {
|
||||
Crossfade(it) {
|
||||
if (it != null) {
|
||||
UserCompose(baseUser = it, accountViewModel = accountViewModel, showDiviser = false, nav = nav)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -12,19 +11,18 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
@@ -34,7 +32,6 @@ import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class RelayList(
|
||||
val relay: Relay,
|
||||
@@ -55,7 +52,6 @@ fun RelaySelectionDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
var relays by remember {
|
||||
@@ -69,6 +65,13 @@ fun RelaySelectionDialog(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val hasSelectedRelay by remember {
|
||||
derivedStateOf {
|
||||
relays.any { it.isSelected }
|
||||
}
|
||||
}
|
||||
|
||||
var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) }
|
||||
|
||||
relayInfo?.let {
|
||||
@@ -121,21 +124,15 @@ fun RelaySelectionDialog(
|
||||
SaveButton(
|
||||
onPost = {
|
||||
val selectedRelays = relays.filter { it.isSelected }
|
||||
if (selectedRelays.isEmpty()) {
|
||||
scope.launch {
|
||||
Toast.makeText(context, context.getString(R.string.select_a_relay_to_continue), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@SaveButton
|
||||
}
|
||||
onPost(selectedRelays.map { it.relay })
|
||||
onClose()
|
||||
},
|
||||
isActive = true
|
||||
isActive = hasSelectedRelay
|
||||
)
|
||||
}
|
||||
|
||||
RelaySwitch(
|
||||
text = stringResource(R.string.select_deselect_all),
|
||||
text = context.getString(R.string.select_deselect_all),
|
||||
checked = selected,
|
||||
onClick = {
|
||||
selected = !selected
|
||||
@@ -181,15 +178,10 @@ fun RelaySelectionDialog(
|
||||
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
msg,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.unable_to_download_relay_document),
|
||||
msg
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@ package com.vitorpamplona.amethyst.ui.actions
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
@@ -66,21 +64,16 @@ fun SaveToGallery(url: String) {
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || writeStoragePermissionState.status.isGranted) {
|
||||
saveImage()
|
||||
} else {
|
||||
writeStoragePermissionState.launchPermissionRequest()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = Color.Gray
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.save), color = Color.White)
|
||||
Text(text = stringResource(id = R.string.save))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +119,7 @@ fun SaveToGallery(localFile: File, mimeType: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || writeStoragePermissionState.status.isGranted) {
|
||||
saveImage()
|
||||
@@ -134,12 +127,8 @@ fun SaveToGallery(localFile: File, mimeType: String?) {
|
||||
writeStoragePermissionState.launchPermissionRequest()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = Color.Gray
|
||||
)
|
||||
shape = ButtonBorder
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.save), color = Color.White)
|
||||
Text(text = stringResource(id = R.string.save))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddPhotoAlternate
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -144,7 +144,7 @@ fun LoadingAnimation(
|
||||
),
|
||||
progress = 1f,
|
||||
strokeWidth = 1.dp,
|
||||
color = MaterialTheme.colors.background // Set background color
|
||||
color = MaterialTheme.colorScheme.background // Set background color
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@ package com.vitorpamplona.amethyst.ui.buttons
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -20,6 +19,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
|
||||
@Composable
|
||||
fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
@@ -46,12 +46,11 @@ fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit)
|
||||
|
||||
Column() {
|
||||
if (isOpen) {
|
||||
OutlinedButton(
|
||||
FloatingActionButton(
|
||||
onClick = { wantsToSendNewMessage = true; isOpen = false },
|
||||
modifier = Modifier.size(55.dp),
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
contentPadding = PaddingValues(bottom = 3.dp)
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.messages_new_message),
|
||||
@@ -63,12 +62,11 @@ fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
OutlinedButton(
|
||||
FloatingActionButton(
|
||||
onClick = { wantsToCreateChannel = true; isOpen = false },
|
||||
modifier = Modifier.size(55.dp),
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
contentPadding = PaddingValues(bottom = 3.dp)
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.messages_create_public_chat),
|
||||
@@ -81,12 +79,11 @@ fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
FloatingActionButton(
|
||||
onClick = { isOpen = !isOpen },
|
||||
modifier = Modifier.size(55.dp),
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
|
||||
@@ -3,12 +3,12 @@ package com.vitorpamplona.amethyst.buttons
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -36,7 +36,7 @@ fun NewChannelButton(accountViewModel: AccountViewModel) {
|
||||
onClick = { wantsToPost = true },
|
||||
modifier = Modifier.size(55.dp),
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
colors = ButtonDefaults.outlinedButtonColors(containerColor = MaterialTheme.colorScheme.primary),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.buttons
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -21,10 +19,11 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
|
||||
@Composable
|
||||
fun NewCommunityNoteButton(communityIdHex: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LoadNote(baseNoteHex = communityIdHex) {
|
||||
LoadNote(baseNoteHex = communityIdHex, accountViewModel) {
|
||||
it?.let {
|
||||
NewCommunityNoteButton(it, accountViewModel, nav)
|
||||
}
|
||||
@@ -41,12 +40,11 @@ fun NewCommunityNoteButton(note: Note, accountViewModel: AccountViewModel, nav:
|
||||
NewPostView({ wantsToPost = false }, note, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
FloatingActionButton(
|
||||
onClick = { wantsToPost = true },
|
||||
modifier = Modifier.size(55.dp),
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
|
||||
@@ -6,16 +6,14 @@ import android.os.Build
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.ProgressIndicatorDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -41,6 +39,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -112,12 +111,11 @@ fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit, na
|
||||
if (postViewModel.isUploadingImage) {
|
||||
ShowProgress(postViewModel)
|
||||
} else {
|
||||
OutlinedButton(
|
||||
FloatingActionButton(
|
||||
onClick = { wantsToPost = true },
|
||||
modifier = Modifier.size(55.dp),
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
@@ -137,16 +135,15 @@ private fun ShowProgress(postViewModel: NewMediaModel) {
|
||||
targetValue = postViewModel.uploadingPercentage.value,
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
|
||||
).value,
|
||||
modifier = Modifier
|
||||
.size(55.dp)
|
||||
modifier = Size55Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colors.background),
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
strokeWidth = 5.dp
|
||||
)
|
||||
postViewModel.uploadingDescription.value?.let {
|
||||
Text(
|
||||
it,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = 10.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.buttons
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -19,6 +17,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
|
||||
@Composable
|
||||
fun NewNoteButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
@@ -30,12 +29,11 @@ fun NewNoteButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
NewPostView({ wantsToPost = false }, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
FloatingActionButton(
|
||||
onClick = { wantsToPost = true },
|
||||
modifier = Modifier.size(55.dp),
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -55,7 +55,7 @@ fun AudioWaveformReadOnly(
|
||||
amplitudes: List<Int>,
|
||||
onProgressChange: (Float) -> Unit
|
||||
) {
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
val _progress = remember(progress) { progress.coerceIn(MinProgress, MaxProgress) }
|
||||
val _spikeWidth = remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) }
|
||||
val _spikePadding = remember(spikePadding) { spikePadding.coerceIn(MinSpikePaddingDp, MaxSpikePaddingDp) }
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -10,7 +9,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -76,7 +75,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
|
||||
.fillMaxWidth()
|
||||
.padding(start = 30.dp, end = 30.dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
|
||||
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -133,31 +132,25 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
|
||||
CashuProcessor().melt(
|
||||
token,
|
||||
lud16,
|
||||
onSuccess = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
onSuccess = { title, message ->
|
||||
accountViewModel.toast(title, message)
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
onError = { title, message ->
|
||||
accountViewModel.toast(title, message)
|
||||
},
|
||||
context
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_lightning_address_set),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.no_lightning_address_set),
|
||||
context.getString(R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, accountViewModel.account.userProfile().toBestDisplayName())
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
@@ -177,16 +170,12 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
|
||||
startActivity(context, intent, null)
|
||||
} else {
|
||||
// Copying the token to clipboard for now
|
||||
var orignaltoken = token.token
|
||||
clipboardManager.setText(AnnotatedString("$orignaltoken"))
|
||||
scope.launch {
|
||||
Toast.makeText(context, context.getString(R.string.copied_token_to_clipboard), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
clipboardManager.setText(AnnotatedString(token.token))
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text("⎘", color = Color.White, fontSize = 18.sp)
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -13,12 +13,13 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
@Composable
|
||||
fun ClickableEmail(email: String) {
|
||||
val stripped = email.replaceFirst("mailto:", "")
|
||||
val context = LocalContext.current
|
||||
|
||||
ClickableText(
|
||||
text = remember { AnnotatedString(email) },
|
||||
onClick = { runCatching { context.sendMail(email) } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
text = remember { AnnotatedString(stripped) },
|
||||
onClick = { runCatching { context.sendMail(stripped) } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -16,6 +16,6 @@ fun ClickableNoteTag(
|
||||
ClickableText(
|
||||
text = AnnotatedString("@${baseNote.idNote().toShortenHex()}"),
|
||||
onClick = { nav("Note/${baseNote.idHex}") },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -18,7 +18,7 @@ fun ClickablePhone(phone: String) {
|
||||
ClickableText(
|
||||
text = remember { AnnotatedString(phone) },
|
||||
onClick = { runCatching { context.dial(phone) } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,10 @@ import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -40,12 +39,12 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NIP30Parser
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
@@ -61,20 +60,21 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun ClickableRoute(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (nip19.type) {
|
||||
Nip19.Type.USER -> {
|
||||
DisplayUser(nip19, nav)
|
||||
DisplayUser(nip19, accountViewModel, nav)
|
||||
}
|
||||
Nip19.Type.ADDRESS -> {
|
||||
DisplayAddress(nip19, nav)
|
||||
DisplayAddress(nip19, accountViewModel, nav)
|
||||
}
|
||||
Nip19.Type.NOTE -> {
|
||||
DisplayNote(nip19, nav)
|
||||
DisplayNote(nip19, accountViewModel, nav)
|
||||
}
|
||||
Nip19.Type.EVENT -> {
|
||||
DisplayEvent(nip19, nav)
|
||||
DisplayEvent(nip19, accountViewModel, nav)
|
||||
}
|
||||
else -> {
|
||||
Text(
|
||||
@@ -86,34 +86,15 @@ fun ClickableRoute(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadNote(
|
||||
hex: String,
|
||||
content: @Composable (Note) -> Unit
|
||||
) {
|
||||
var noteBase by remember(hex) { mutableStateOf(LocalCache.getNoteIfExists(hex)) }
|
||||
|
||||
if (noteBase == null) {
|
||||
LaunchedEffect(key1 = hex) {
|
||||
launch(Dispatchers.IO) {
|
||||
noteBase = LocalCache.checkGetOrCreateNote(hex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
noteBase?.let {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayEvent(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadNote(nip19.hex) {
|
||||
LoadNote(nip19.hex, accountViewModel) {
|
||||
if (it != null) {
|
||||
DisplayNoteLink(it, nip19, nav)
|
||||
DisplayNoteLink(it, nip19, accountViewModel, nav)
|
||||
} else {
|
||||
CreateClickableText(
|
||||
clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" },
|
||||
@@ -128,11 +109,12 @@ private fun DisplayEvent(
|
||||
@Composable
|
||||
private fun DisplayNote(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadNote(nip19.hex) {
|
||||
LoadNote(nip19.hex, accountViewModel = accountViewModel) {
|
||||
if (it != null) {
|
||||
DisplayNoteLink(it, nip19, nav)
|
||||
DisplayNoteLink(it, nip19, accountViewModel, nav)
|
||||
} else {
|
||||
CreateClickableText(
|
||||
clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" },
|
||||
@@ -148,6 +130,7 @@ private fun DisplayNote(
|
||||
private fun DisplayNoteLink(
|
||||
it: Note,
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteState by it.live().metadata.observeAsState()
|
||||
@@ -177,7 +160,7 @@ private fun DisplayNoteLink(
|
||||
nav = nav
|
||||
)
|
||||
} else if (channelHex != null) {
|
||||
LoadChannel(baseChannelHex = channelHex) { baseChannel ->
|
||||
LoadChannel(baseChannelHex = channelHex, accountViewModel) { baseChannel ->
|
||||
val channelState by baseChannel.live.observeAsState()
|
||||
val channelDisplayName by remember(channelState) {
|
||||
derivedStateOf {
|
||||
@@ -205,14 +188,15 @@ private fun DisplayNoteLink(
|
||||
@Composable
|
||||
private fun DisplayAddress(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var noteBase by remember(nip19) { mutableStateOf(LocalCache.getNoteIfExists(nip19.hex)) }
|
||||
var noteBase by remember(nip19) { mutableStateOf(accountViewModel.getNoteIfExists(nip19.hex)) }
|
||||
|
||||
if (noteBase == null) {
|
||||
LaunchedEffect(key1 = nip19.hex) {
|
||||
launch(Dispatchers.IO) {
|
||||
noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
|
||||
accountViewModel.checkGetOrCreateAddressableNote(nip19.hex) {
|
||||
noteBase = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,14 +228,19 @@ private fun DisplayAddress(
|
||||
@Composable
|
||||
private fun DisplayUser(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var userBase by remember(nip19) { mutableStateOf(LocalCache.getUserIfExists(nip19.hex)) }
|
||||
var userBase by remember(nip19) {
|
||||
mutableStateOf(
|
||||
accountViewModel.getUserIfExists(nip19.hex)
|
||||
)
|
||||
}
|
||||
|
||||
if (userBase == null) {
|
||||
LaunchedEffect(key1 = nip19.hex) {
|
||||
launch(Dispatchers.IO) {
|
||||
userBase = LocalCache.checkGetOrCreateUser(nip19.hex)
|
||||
accountViewModel.checkGetOrCreateUser(nip19.hex) {
|
||||
userBase = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,8 +306,8 @@ fun CreateClickableText(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val currentStyle = LocalTextStyle.current
|
||||
val primaryColor = MaterialTheme.colors.primary
|
||||
val onBackgroundColor = MaterialTheme.colors.onBackground
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
||||
|
||||
val clickablePartStyle = remember(primaryColor, overrideColor) {
|
||||
currentStyle.copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight).toSpanStyle()
|
||||
@@ -378,7 +367,7 @@ fun CreateTextWithEmoji(
|
||||
|
||||
val textColor = color.takeOrElse {
|
||||
LocalTextStyle.current.color.takeOrElse {
|
||||
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||
LocalContentColor.current
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +423,7 @@ fun CreateTextWithEmoji(
|
||||
|
||||
val textColor = color.takeOrElse {
|
||||
LocalTextStyle.current.color.takeOrElse {
|
||||
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||
LocalContentColor.current
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,14 +536,14 @@ fun CreateClickableTextWithEmoji(
|
||||
ClickableInLineIconRenderer(
|
||||
emojiLists!!.part1,
|
||||
maxLines,
|
||||
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()
|
||||
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colorScheme.primary, fontWeight = fontWeight).toSpanStyle()
|
||||
) {
|
||||
nav(route)
|
||||
}
|
||||
|
||||
InLineIconRenderer(
|
||||
emojiLists!!.part2,
|
||||
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle(),
|
||||
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colorScheme.onBackground, fontWeight = fontWeight).toSpanStyle(),
|
||||
maxLines = maxLines
|
||||
)
|
||||
}
|
||||
@@ -687,7 +676,7 @@ fun InLineIconRenderer(
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 1.dp)
|
||||
.padding(horizontal = 0.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
@@ -23,6 +23,6 @@ fun ClickableUrl(urlText: String, url: String) {
|
||||
ClickableText(
|
||||
text = text,
|
||||
onClick = { runCatching { uri.openUri(doubleCheckedUrl) } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -28,6 +28,6 @@ fun ClickableUserTag(
|
||||
ClickableText(
|
||||
text = userName,
|
||||
onClick = { nav(route) },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.quartz.encoders.LnWithdrawalUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -43,29 +41,28 @@ fun MayBeWithdrawal(lnurlWord: String) {
|
||||
@Composable
|
||||
fun ClickableWithdrawal(withdrawalString: String) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val withdraw = remember(withdrawalString) {
|
||||
AnnotatedString("$withdrawalString ")
|
||||
}
|
||||
|
||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
if (showErrorMessageDialog != null) {
|
||||
ErrorMessageDialog(
|
||||
title = context.getString(R.string.error_dialog_pay_withdraw_error),
|
||||
textContent = showErrorMessageDialog ?: "",
|
||||
onDismiss = { showErrorMessageDialog = null }
|
||||
)
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = withdraw,
|
||||
onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$withdrawalString"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.lightning_wallets_not_found),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
payViaIntent(withdrawalString, context) {
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
},
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
|
||||
const val SHORT_TEXT_LENGTH = 350
|
||||
const val SHORTEN_AFTER_LINES = 10
|
||||
|
||||
@Composable
|
||||
fun ExpandableRichTextViewer(
|
||||
@@ -45,11 +46,25 @@ fun ExpandableRichTextViewer(
|
||||
var showFullText by remember { mutableStateOf(false) }
|
||||
|
||||
val whereToCut = remember(content) {
|
||||
// Cuts the text in the first space after 350
|
||||
// Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters
|
||||
val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||
val firstNewLineAfterCut = content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||
|
||||
minOf(firstSpaceAfterCut, firstNewLineAfterCut)
|
||||
// or after SHORTEN_AFTER_LINES lines
|
||||
val numberOfLines = content.count { it == '\n' }
|
||||
|
||||
var charactersInLines = minOf(firstSpaceAfterCut, firstNewLineAfterCut)
|
||||
|
||||
if (numberOfLines > SHORTEN_AFTER_LINES) {
|
||||
val shortContent = content.lines().take(SHORTEN_AFTER_LINES)
|
||||
charactersInLines = 0
|
||||
for (line in shortContent) {
|
||||
// +1 because new line character is omitted from .lines
|
||||
charactersInLines += (line.length + 1)
|
||||
}
|
||||
}
|
||||
|
||||
minOf(firstSpaceAfterCut, firstNewLineAfterCut, charactersInLines)
|
||||
}
|
||||
|
||||
val text by remember(content) {
|
||||
@@ -82,7 +97,7 @@ fun ExpandableRichTextViewer(
|
||||
.fillMaxWidth()
|
||||
.background(getGradient(backgroundColor))
|
||||
) {
|
||||
ShowMoreButton() {
|
||||
ShowMoreButton {
|
||||
showFullText = !showFullText
|
||||
}
|
||||
}
|
||||
@@ -97,7 +112,7 @@ fun ShowMoreButton(onClick: () -> Unit) {
|
||||
onClick = onClick,
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.secondaryButtonBackground
|
||||
containerColor = MaterialTheme.colorScheme.secondaryButtonBackground
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -10,7 +7,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -23,8 +20,9 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
@@ -67,14 +65,23 @@ fun MayBeInvoicePreview(lnbcWord: String) {
|
||||
@Composable
|
||||
fun InvoicePreview(lnInvoice: String, amount: String?) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
if (showErrorMessageDialog != null) {
|
||||
ErrorMessageDialog(
|
||||
title = context.getString(R.string.error_dialog_pay_invoice_error),
|
||||
textContent = showErrorMessageDialog ?: "",
|
||||
onDismiss = { showErrorMessageDialog = null }
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 30.dp, end = 30.dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
|
||||
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -120,23 +127,13 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp),
|
||||
onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$lnInvoice"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.lightning_wallets_not_found),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
payViaIntent(lnInvoice, context) {
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.pay), color = Color.White, fontSize = 20.sp)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -8,13 +7,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -50,21 +49,22 @@ fun InvoiceRequestCard(
|
||||
titleText: String? = null,
|
||||
buttonText: String? = null,
|
||||
onSuccess: (String) -> Unit,
|
||||
onClose: () -> Unit
|
||||
onClose: () -> Unit,
|
||||
onError: (String, String) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 30.dp, end = 30.dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
|
||||
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp)
|
||||
) {
|
||||
InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose)
|
||||
InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose, onError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,8 @@ fun InvoiceRequest(
|
||||
titleText: String? = null,
|
||||
buttonText: String? = null,
|
||||
onSuccess: (String) -> Unit,
|
||||
onClose: () -> Unit
|
||||
onClose: () -> Unit,
|
||||
onError: (String, String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -116,7 +117,7 @@ fun InvoiceRequest(
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.thank_you_so_much),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -141,7 +142,7 @@ fun InvoiceRequest(
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "1000",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -162,20 +163,16 @@ fun InvoiceRequest(
|
||||
message,
|
||||
zapRequest?.toJson(),
|
||||
onSuccess = onSuccess,
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = {
|
||||
}
|
||||
},
|
||||
context = context
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(text = buttonText ?: stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.startsWithNIP19Scheme
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
|
||||
class MarkdownParser {
|
||||
private fun getDisplayNameAndNIP19FromTag(tag: String, tags: ImmutableListOfLists<String>): Pair<String, String>? {
|
||||
val matcher = tagIndex.matcher(tag)
|
||||
val (index, suffix) = try {
|
||||
matcher.find()
|
||||
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
|
||||
} catch (e: Exception) {
|
||||
Log.w("Tag Parser", "Couldn't link tag $tag", e)
|
||||
Pair(null, null)
|
||||
}
|
||||
|
||||
if (index != null && index >= 0 && index < tags.lists.size) {
|
||||
val tag = tags.lists[index]
|
||||
|
||||
if (tag.size > 1) {
|
||||
if (tag[0] == "p") {
|
||||
LocalCache.checkGetOrCreateUser(tag[1])?.let {
|
||||
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
|
||||
}
|
||||
} else if (tag[0] == "e" || tag[0] == "a") {
|
||||
LocalCache.checkGetOrCreateNote(tag[1])?.let {
|
||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getDisplayNameFromNip19(nip19: Nip19.Return): Pair<String, String>? {
|
||||
if (nip19.type == Nip19.Type.USER) {
|
||||
LocalCache.users[nip19.hex]?.let {
|
||||
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
|
||||
}
|
||||
} else if (nip19.type == Nip19.Type.NOTE) {
|
||||
LocalCache.notes[nip19.hex]?.let {
|
||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
||||
}
|
||||
} else if (nip19.type == Nip19.Type.ADDRESS) {
|
||||
LocalCache.addressables[nip19.hex]?.let {
|
||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
||||
}
|
||||
} else if (nip19.type == Nip19.Type.EVENT) {
|
||||
LocalCache.notes[nip19.hex]?.let {
|
||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun returnNIP19References(content: String, tags: ImmutableListOfLists<String>?): List<Nip19.Return> {
|
||||
checkNotInMainThread()
|
||||
|
||||
val listOfReferences = mutableListOf<Nip19.Return>()
|
||||
content.split('\n').forEach { paragraph ->
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
if (startsWithNIP19Scheme(word)) {
|
||||
val parsedNip19 = Nip19.uriToRoute(word)
|
||||
parsedNip19?.let {
|
||||
listOfReferences.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags?.lists?.forEach {
|
||||
if (it[0] == "p" && it.size > 1) {
|
||||
listOfReferences.add(Nip19.Return(Nip19.Type.USER, it[1], null, null, null, ""))
|
||||
} else if (it[0] == "e" && it.size > 1) {
|
||||
listOfReferences.add(Nip19.Return(Nip19.Type.NOTE, it[1], null, null, null, ""))
|
||||
} else if (it[0] == "a" && it.size > 1) {
|
||||
listOfReferences.add(Nip19.Return(Nip19.Type.ADDRESS, it[1], null, null, null, ""))
|
||||
}
|
||||
}
|
||||
|
||||
return listOfReferences
|
||||
}
|
||||
|
||||
fun returnMarkdownWithSpecialContent(content: String, tags: ImmutableListOfLists<String>?): String {
|
||||
var returnContent = ""
|
||||
content.split('\n').forEach { paragraph ->
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
if (isValidURL(word)) {
|
||||
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
returnContent += " "
|
||||
} else {
|
||||
returnContent += "[$word]($word) "
|
||||
}
|
||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||
returnContent += "[$word](mailto:$word) "
|
||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||
returnContent += "[$word](tel:$word) "
|
||||
} else if (startsWithNIP19Scheme(word)) {
|
||||
val parsedNip19 = Nip19.uriToRoute(word)
|
||||
returnContent += if (parsedNip19 !== null) {
|
||||
val pair = getDisplayNameFromNip19(parsedNip19)
|
||||
if (pair != null) {
|
||||
val (displayName, nip19) = pair
|
||||
"[$displayName](nostr:$nip19) "
|
||||
} else {
|
||||
"$word "
|
||||
}
|
||||
} else {
|
||||
"$word "
|
||||
}
|
||||
} else if (word.startsWith("#")) {
|
||||
if (tagIndex.matcher(word).matches() && tags != null) {
|
||||
val pair = getDisplayNameAndNIP19FromTag(word, tags)
|
||||
if (pair != null) {
|
||||
returnContent += "[${pair.first}](nostr:${pair.second}) "
|
||||
} else {
|
||||
returnContent += "$word "
|
||||
}
|
||||
} else if (hashTagsPattern.matcher(word).matches()) {
|
||||
val hashtagMatcher = hashTagsPattern.matcher(word)
|
||||
|
||||
val (myTag, mySuffix) = try {
|
||||
hashtagMatcher.find()
|
||||
Pair(hashtagMatcher.group(1), hashtagMatcher.group(2))
|
||||
} catch (e: Exception) {
|
||||
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
|
||||
Pair(null, null)
|
||||
}
|
||||
|
||||
if (myTag != null) {
|
||||
returnContent += "[#$myTag](nostr:Hashtag?id=$myTag)$mySuffix "
|
||||
} else {
|
||||
returnContent += "$word "
|
||||
}
|
||||
} else {
|
||||
returnContent += "$word "
|
||||
}
|
||||
} else {
|
||||
returnContent += "$word "
|
||||
}
|
||||
}
|
||||
returnContent += "\n"
|
||||
}
|
||||
return returnContent
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -32,9 +29,8 @@ import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.em
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
||||
import com.halilibo.richtext.ui.material.MaterialRichText
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.vitorpamplona.amethyst.model.HashtagIcon
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||
@@ -59,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
|
||||
@@ -113,7 +110,7 @@ fun RichTextViewer(
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
if (remember(content) { isMarkdown(content) }) {
|
||||
RenderContentAsMarkdown(content, tags, nav)
|
||||
RenderContentAsMarkdown(content, tags, accountViewModel, nav)
|
||||
} else {
|
||||
RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
@@ -139,7 +136,7 @@ private fun RenderRegular(
|
||||
val textStyle = currentTextStyle.copy(
|
||||
lineHeight = 1.4.em,
|
||||
color = currentTextStyle.color.takeOrElse {
|
||||
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||
LocalContentColor.current
|
||||
}
|
||||
)
|
||||
|
||||
@@ -243,7 +240,7 @@ private fun RenderWordWithoutPreview(
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> NormalWord(word.segmentText, style)
|
||||
@@ -270,7 +267,7 @@ private fun RenderWordWithPreview(
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> NormalWord(word.segmentText, style)
|
||||
@@ -320,7 +317,7 @@ fun RenderCustomEmoji(word: String, state: RichTextViewerState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<String>?, nav: (String) -> Unit) {
|
||||
private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<String>?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val uri = LocalUriHandler.current
|
||||
val onClick = remember {
|
||||
{ link: String ->
|
||||
@@ -335,10 +332,10 @@ private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<
|
||||
}
|
||||
|
||||
ProvideTextStyle(MarkdownTextStyle) {
|
||||
MaterialRichText(
|
||||
style = MaterialTheme.colors.markdownStyle
|
||||
Material3RichText(
|
||||
style = MaterialTheme.colorScheme.markdownStyle
|
||||
) {
|
||||
RefreshableContent(content, tags) {
|
||||
RefreshableContent(content, tags, accountViewModel) {
|
||||
Markdown(
|
||||
content = it,
|
||||
markdownParseOptions = MarkdownParseOptions.Default,
|
||||
@@ -350,13 +347,14 @@ private fun RenderContentAsMarkdown(content: String, tags: ImmutableListOfLists<
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RefreshableContent(content: String, tags: ImmutableListOfLists<String>?, onCompose: @Composable (String) -> Unit) {
|
||||
private fun RefreshableContent(content: String, tags: ImmutableListOfLists<String>?, accountViewModel: AccountViewModel, onCompose: @Composable (String) -> Unit) {
|
||||
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(content) }
|
||||
|
||||
ObserverAllNIP19References(content, tags) {
|
||||
val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
|
||||
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
|
||||
markdownWithSpecialContent = newMarkdownWithSpecialContent
|
||||
ObserverAllNIP19References(content, tags, accountViewModel) {
|
||||
accountViewModel.returnMarkdownWithSpecialContent(content, tags) { newMarkdownWithSpecialContent ->
|
||||
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
|
||||
markdownWithSpecialContent = newMarkdownWithSpecialContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,47 +364,47 @@ private fun RefreshableContent(content: String, tags: ImmutableListOfLists<Strin
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserverAllNIP19References(content: String, tags: ImmutableListOfLists<String>?, onRefresh: () -> Unit) {
|
||||
fun ObserverAllNIP19References(content: String, tags: ImmutableListOfLists<String>?, accountViewModel: AccountViewModel, onRefresh: () -> Unit) {
|
||||
var nip19References by remember(content) { mutableStateOf<List<Nip19.Return>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(key1 = content) {
|
||||
launch(Dispatchers.IO) {
|
||||
nip19References = returnNIP19References(content, tags)
|
||||
accountViewModel.returnNIP19References(content, tags) {
|
||||
nip19References = it
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
nip19References.forEach {
|
||||
ObserveNIP19(it, onRefresh)
|
||||
ObserveNIP19(it, accountViewModel, onRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveNIP19(
|
||||
it: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
onRefresh: () -> Unit
|
||||
) {
|
||||
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
|
||||
ObserveNIP19Event(it, onRefresh)
|
||||
ObserveNIP19Event(it, accountViewModel, onRefresh)
|
||||
} else if (it.type == Nip19.Type.USER) {
|
||||
ObserveNIP19User(it, onRefresh)
|
||||
ObserveNIP19User(it, accountViewModel, onRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ObserveNIP19Event(
|
||||
it: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
onRefresh: () -> Unit
|
||||
) {
|
||||
var baseNote by remember(it) { mutableStateOf<Note?>(LocalCache.getNoteIfExists(it.hex)) }
|
||||
var baseNote by remember(it) { mutableStateOf<Note?>(accountViewModel.getNoteIfExists(it.hex)) }
|
||||
|
||||
if (baseNote == null) {
|
||||
LaunchedEffect(key1 = it.hex) {
|
||||
launch(Dispatchers.IO) {
|
||||
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
|
||||
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
|
||||
launch(Dispatchers.Main) { baseNote = note }
|
||||
}
|
||||
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
|
||||
accountViewModel.checkGetOrCreateNote(it.hex) { note ->
|
||||
launch(Dispatchers.Main) { baseNote = note }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,9 +421,7 @@ fun ObserveNote(note: Note, onRefresh: () -> Unit) {
|
||||
|
||||
LaunchedEffect(key1 = loadedNoteId) {
|
||||
if (loadedNoteId != null) {
|
||||
launch(Dispatchers.IO) {
|
||||
onRefresh()
|
||||
}
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,17 +429,16 @@ fun ObserveNote(note: Note, onRefresh: () -> Unit) {
|
||||
@Composable
|
||||
private fun ObserveNIP19User(
|
||||
it: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
onRefresh: () -> Unit
|
||||
) {
|
||||
var baseUser by remember(it) { mutableStateOf<User?>(LocalCache.getUserIfExists(it.hex)) }
|
||||
var baseUser by remember(it) { mutableStateOf<User?>(accountViewModel.getUserIfExists(it.hex)) }
|
||||
|
||||
if (baseUser == null) {
|
||||
LaunchedEffect(key1 = it.hex) {
|
||||
launch(Dispatchers.IO) {
|
||||
if (it.type == Nip19.Type.USER) {
|
||||
LocalCache.checkGetOrCreateUser(it.hex)?.let { user ->
|
||||
launch(Dispatchers.Main) { baseUser = user }
|
||||
}
|
||||
if (it.type == Nip19.Type.USER) {
|
||||
accountViewModel.checkGetOrCreateUser(it.hex)?.let { user ->
|
||||
launch(Dispatchers.Main) { baseUser = user }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,184 +455,19 @@ private fun ObserveUser(user: User, onRefresh: () -> Unit) {
|
||||
|
||||
LaunchedEffect(key1 = loadedUserMetaId) {
|
||||
if (loadedUserMetaId != null) {
|
||||
launch(Dispatchers.IO) {
|
||||
onRefresh()
|
||||
}
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDisplayNameAndNIP19FromTag(tag: String, tags: ImmutableListOfLists<String>): Pair<String, String>? {
|
||||
val matcher = tagIndex.matcher(tag)
|
||||
val (index, suffix) = try {
|
||||
matcher.find()
|
||||
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
|
||||
} catch (e: Exception) {
|
||||
Log.w("Tag Parser", "Couldn't link tag $tag", e)
|
||||
Pair(null, null)
|
||||
}
|
||||
|
||||
if (index != null && index >= 0 && index < tags.lists.size) {
|
||||
val tag = tags.lists[index]
|
||||
|
||||
if (tag.size > 1) {
|
||||
if (tag[0] == "p") {
|
||||
LocalCache.checkGetOrCreateUser(tag[1])?.let {
|
||||
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
|
||||
}
|
||||
} else if (tag[0] == "e" || tag[0] == "a") {
|
||||
LocalCache.checkGetOrCreateNote(tag[1])?.let {
|
||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getDisplayNameFromNip19(nip19: Nip19.Return): Pair<String, String>? {
|
||||
if (nip19.type == Nip19.Type.USER) {
|
||||
LocalCache.users[nip19.hex]?.let {
|
||||
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
|
||||
}
|
||||
} else if (nip19.type == Nip19.Type.NOTE) {
|
||||
LocalCache.notes[nip19.hex]?.let {
|
||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
||||
}
|
||||
} else if (nip19.type == Nip19.Type.ADDRESS) {
|
||||
LocalCache.addressables[nip19.hex]?.let {
|
||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
||||
}
|
||||
} else if (nip19.type == Nip19.Type.EVENT) {
|
||||
LocalCache.notes[nip19.hex]?.let {
|
||||
return Pair(it.idDisplayNote(), it.toNEvent())
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun returnNIP19References(content: String, tags: ImmutableListOfLists<String>?): List<Nip19.Return> {
|
||||
val listOfReferences = mutableListOf<Nip19.Return>()
|
||||
content.split('\n').forEach { paragraph ->
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
if (startsWithNIP19Scheme(word)) {
|
||||
val parsedNip19 = Nip19.uriToRoute(word)
|
||||
parsedNip19?.let {
|
||||
listOfReferences.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags?.lists?.forEach {
|
||||
if (it[0] == "p" && it.size > 1) {
|
||||
listOfReferences.add(Nip19.Return(Nip19.Type.USER, it[1], null, null, null, ""))
|
||||
} else if (it[0] == "e" && it.size > 1) {
|
||||
listOfReferences.add(Nip19.Return(Nip19.Type.NOTE, it[1], null, null, null, ""))
|
||||
} else if (it[0] == "a" && it.size > 1) {
|
||||
listOfReferences.add(Nip19.Return(Nip19.Type.ADDRESS, it[1], null, null, null, ""))
|
||||
}
|
||||
}
|
||||
|
||||
return listOfReferences
|
||||
}
|
||||
|
||||
private fun returnMarkdownWithSpecialContent(content: String, tags: ImmutableListOfLists<String>?): String {
|
||||
var returnContent = ""
|
||||
content.split('\n').forEach { paragraph ->
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
if (isValidURL(word)) {
|
||||
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
returnContent += " "
|
||||
} else {
|
||||
returnContent += "[$word]($word) "
|
||||
}
|
||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||
returnContent += "[$word](mailto:$word) "
|
||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||
returnContent += "[$word](tel:$word) "
|
||||
} else if (startsWithNIP19Scheme(word)) {
|
||||
val parsedNip19 = Nip19.uriToRoute(word)
|
||||
returnContent += if (parsedNip19 !== null) {
|
||||
val pair = getDisplayNameFromNip19(parsedNip19)
|
||||
if (pair != null) {
|
||||
val (displayName, nip19) = pair
|
||||
"[$displayName](nostr:$nip19) "
|
||||
} else {
|
||||
"$word "
|
||||
}
|
||||
} else {
|
||||
"$word "
|
||||
}
|
||||
} else if (word.startsWith("#")) {
|
||||
if (tagIndex.matcher(word).matches() && tags != null) {
|
||||
val pair = getDisplayNameAndNIP19FromTag(word, tags)
|
||||
if (pair != null) {
|
||||
returnContent += "[${pair.first}](nostr:${pair.second}) "
|
||||
} else {
|
||||
returnContent += "$word "
|
||||
}
|
||||
} else if (hashTagsPattern.matcher(word).matches()) {
|
||||
val hashtagMatcher = hashTagsPattern.matcher(word)
|
||||
|
||||
val (myTag, mySuffix) = try {
|
||||
hashtagMatcher.find()
|
||||
Pair(hashtagMatcher.group(1), hashtagMatcher.group(2))
|
||||
} catch (e: Exception) {
|
||||
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
|
||||
Pair(null, null)
|
||||
}
|
||||
|
||||
if (myTag != null) {
|
||||
returnContent += "[#$myTag](nostr:Hashtag?id=$myTag)$mySuffix "
|
||||
} else {
|
||||
returnContent += "$word "
|
||||
}
|
||||
} else {
|
||||
returnContent += "$word "
|
||||
}
|
||||
} else {
|
||||
returnContent += "$word "
|
||||
}
|
||||
}
|
||||
returnContent += "\n"
|
||||
}
|
||||
return returnContent
|
||||
}
|
||||
|
||||
fun startsWithNIP19Scheme(word: String): Boolean {
|
||||
val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@")
|
||||
|
||||
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19.Return)
|
||||
|
||||
@Composable
|
||||
fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Color>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
var loadedLink by remember { mutableStateOf<LoadedBechLink?>(null) }
|
||||
|
||||
if (loadedLink == null) {
|
||||
LaunchedEffect(key1 = word) {
|
||||
launch(Dispatchers.IO) {
|
||||
Nip19.uriToRoute(word)?.let {
|
||||
var returningNote: Note? = null
|
||||
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
|
||||
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
|
||||
returningNote = note
|
||||
}
|
||||
}
|
||||
|
||||
val newLink = LoadedBechLink(returningNote, it)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
loadedLink = newLink
|
||||
}
|
||||
}
|
||||
accountViewModel.parseNIP19(word) {
|
||||
loadedLink = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,10 +484,10 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Co
|
||||
}
|
||||
} else if (loadedLink?.nip19 != null) {
|
||||
Row() {
|
||||
ClickableRoute(loadedLink?.nip19!!, nav)
|
||||
ClickableRoute(loadedLink?.nip19!!, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
val text = remember {
|
||||
val text = remember(word) {
|
||||
if (word.length > 16) {
|
||||
word.replaceRange(8, word.length - 8, ":")
|
||||
} else {
|
||||
@@ -680,7 +510,7 @@ private fun DisplayFullNote(
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = MaterialTheme.colors.replyModifier,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
isQuotedNote = true,
|
||||
nav = nav
|
||||
@@ -709,12 +539,12 @@ private fun RenderHashtag(
|
||||
segment: HashTagSegment,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val primary = MaterialTheme.colors.primary
|
||||
val primary = MaterialTheme.colorScheme.primary
|
||||
val hashtagIcon = remember(segment.hashtag) { checkForHashtagWithIcon(segment.hashtag, primary) }
|
||||
ClickableText(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary).toSpanStyle()
|
||||
) {
|
||||
append("#${segment.hashtag}")
|
||||
}
|
||||
@@ -726,7 +556,7 @@ private fun RenderHashtag(
|
||||
val myId = "inlineContent"
|
||||
val emptytext = buildAnnotatedString {
|
||||
withStyle(
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary).toSpanStyle()
|
||||
) {
|
||||
append("")
|
||||
appendInlineContent(myId, "[icon]")
|
||||
@@ -763,8 +593,8 @@ private fun InlineIcon(hashtagIcon: HashtagIcon) =
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TagLink(word: HashIndexUserSegment, nav: (String) -> Unit) {
|
||||
LoadUser(baseUserHex = word.hex) {
|
||||
fun TagLink(word: HashIndexUserSegment, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LoadUser(baseUserHex = word.hex, accountViewModel) {
|
||||
if (it == null) {
|
||||
Text(text = word.segmentText)
|
||||
} else {
|
||||
@@ -776,15 +606,15 @@ fun TagLink(word: HashIndexUserSegment, nav: (String) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadNote(baseNoteHex: String, content: @Composable (Note?) -> Unit) {
|
||||
fun LoadNote(baseNoteHex: String, accountViewModel: AccountViewModel, content: @Composable (Note?) -> Unit) {
|
||||
var note by remember(baseNoteHex) {
|
||||
mutableStateOf<Note?>(LocalCache.getNoteIfExists(baseNoteHex))
|
||||
mutableStateOf<Note?>(accountViewModel.getNoteIfExists(baseNoteHex))
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
LaunchedEffect(key1 = baseNoteHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
note = LocalCache.checkGetOrCreateNote(baseNoteHex)
|
||||
accountViewModel.checkGetOrCreateNote(baseNoteHex) {
|
||||
note = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -794,7 +624,7 @@ fun LoadNote(baseNoteHex: String, content: @Composable (Note?) -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun TagLink(word: HashIndexEventSegment, canPreview: Boolean, backgroundColor: MutableState<Color>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LoadNote(baseNoteHex = word.hex) {
|
||||
LoadNote(baseNoteHex = word.hex, accountViewModel) {
|
||||
if (it == null) {
|
||||
Text(text = remember { word.segmentText.toShortenHex() })
|
||||
} else {
|
||||
@@ -825,7 +655,7 @@ private fun DisplayNoteFromTag(
|
||||
NoteCompose(
|
||||
baseNote = baseNote,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = MaterialTheme.colors.innerPostModifier,
|
||||
modifier = MaterialTheme.colorScheme.innerPostModifier,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
isQuotedNote = true,
|
||||
nav = nav
|
||||
|
||||
@@ -58,14 +58,15 @@ fun RobohashFallbackAsyncImage(
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
alpha: Float = DefaultAlpha,
|
||||
colorFilter: ColorFilter? = null,
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
|
||||
loadProfilePicture: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val painter = rememberAsyncImagePainter(
|
||||
model = Robohash.imageRequest(context, robot)
|
||||
)
|
||||
|
||||
if (model != null) {
|
||||
if (model != null && loadProfilePicture) {
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
@@ -101,7 +102,8 @@ fun RobohashAsyncImageProxy(
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
alpha: Float = DefaultAlpha,
|
||||
colorFilter: ColorFilter? = null,
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
|
||||
loadProfilePicture: Boolean
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = robot,
|
||||
@@ -112,6 +114,7 @@ fun RobohashAsyncImageProxy(
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality
|
||||
filterQuality = filterQuality,
|
||||
loadProfilePicture = loadProfilePicture
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@@ -11,14 +11,14 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -104,7 +104,7 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
|
||||
modifier = Modifier
|
||||
.size(70.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
@@ -112,7 +112,7 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
|
||||
@@ -14,8 +14,8 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -66,8 +66,8 @@ fun DotsIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
totalDots: Int,
|
||||
selectedIndex: Int,
|
||||
selectedColor: Color = MaterialTheme.colors.primary /* Color.Yellow */,
|
||||
unSelectedColor: Color = MaterialTheme.colors.placeholderText /* Color.Gray */,
|
||||
selectedColor: Color = MaterialTheme.colorScheme.primary /* Color.Yellow */,
|
||||
unSelectedColor: Color = MaterialTheme.colorScheme.placeholderText /* Color.Gray */,
|
||||
dotSize: Dp
|
||||
) {
|
||||
LazyRow(
|
||||
|
||||
@@ -14,12 +14,13 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -37,8 +38,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
fun TextSpinner(
|
||||
label: String?,
|
||||
placeholder: String,
|
||||
options: ImmutableList<String>,
|
||||
explainers: ImmutableList<String>? = null,
|
||||
options: ImmutableList<TitleExplainer>,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -74,8 +74,8 @@ fun TextSpinner(
|
||||
|
||||
if (optionsShowing) {
|
||||
options.isNotEmpty().also {
|
||||
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) {
|
||||
currentText = options[it]
|
||||
SpinnerSelectionDialog(options = options, onDismiss = { optionsShowing = false }) {
|
||||
currentText = options[it].title
|
||||
optionsShowing = false
|
||||
onSelect(it)
|
||||
}
|
||||
@@ -85,10 +85,40 @@ fun TextSpinner(
|
||||
|
||||
@Composable
|
||||
fun SpinnerSelectionDialog(
|
||||
options: ImmutableList<String>,
|
||||
explainers: ImmutableList<String>?,
|
||||
options: ImmutableList<TitleExplainer>,
|
||||
onDismiss: () -> Unit,
|
||||
onSelect: (Int) -> Unit
|
||||
) {
|
||||
SpinnerSelectionDialog(
|
||||
options = options,
|
||||
onSelect = onSelect,
|
||||
onDismiss = onDismiss
|
||||
) { item ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = item.title, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
item.explainer?.let {
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = it, color = Color.Gray, fontSize = Font14SP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> SpinnerSelectionDialog(
|
||||
options: ImmutableList<T>,
|
||||
onSelect: (Int) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onRenderItem: @Composable (T) -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
@@ -106,23 +136,7 @@ fun SpinnerSelectionDialog(
|
||||
}
|
||||
) {
|
||||
Column() {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = item, color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
explainers?.getOrNull(index)?.let {
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = it, color = Color.Gray, fontSize = Font14SP)
|
||||
}
|
||||
}
|
||||
onRenderItem(item)
|
||||
}
|
||||
}
|
||||
if (index < options.lastIndex) {
|
||||
@@ -133,3 +147,6 @@ fun SpinnerSelectionDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class TitleExplainer(val title: String, val explainer: String? = null)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class TranslationConfig(
|
||||
val result: String?,
|
||||
val sourceLang: String?,
|
||||
val targetLang: String?,
|
||||
val showOriginal: Boolean
|
||||
)
|
||||
@@ -3,10 +3,10 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -15,13 +15,15 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
|
||||
@Composable
|
||||
@@ -44,7 +46,7 @@ fun UrlPreviewCard(
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
Row(
|
||||
modifier = MaterialTheme.colors.innerPostModifier
|
||||
modifier = MaterialTheme.colorScheme.innerPostModifier
|
||||
.clickable {
|
||||
runCatching { uri.openUri(url) }
|
||||
}
|
||||
@@ -57,12 +59,12 @@ fun UrlPreviewCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
|
||||
Text(
|
||||
text = previewInfo.verifiedUrl?.host ?: previewInfo.url,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -70,24 +72,22 @@ fun UrlPreviewCard(
|
||||
|
||||
Text(
|
||||
text = previewInfo.title,
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = previewInfo.description,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = MaxWidthWithHorzPadding,
|
||||
color = Color.Gray,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
@@ -21,14 +22,15 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -210,27 +212,29 @@ fun VideoViewInner(
|
||||
} else {
|
||||
VideoPlayerActiveMutex(videoUri) { activeOnScreen ->
|
||||
val mediaItem = remember(videoUri) {
|
||||
MediaItem.Builder()
|
||||
.setMediaId(videoUri)
|
||||
.setUri(videoUri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setArtist(authorName?.ifBlank { null })
|
||||
.setTitle(title?.ifBlank { null } ?: videoUri)
|
||||
.setArtworkUri(
|
||||
try {
|
||||
if (artworkUri != null) {
|
||||
Uri.parse(artworkUri)
|
||||
} else {
|
||||
mutableStateOf(
|
||||
MediaItem.Builder()
|
||||
.setMediaId(videoUri)
|
||||
.setUri(videoUri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setArtist(authorName?.ifBlank { null })
|
||||
.setTitle(title?.ifBlank { null } ?: videoUri)
|
||||
.setArtworkUri(
|
||||
try {
|
||||
if (artworkUri != null) {
|
||||
Uri.parse(artworkUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
GetVideoController(
|
||||
@@ -257,7 +261,7 @@ fun VideoViewInner(
|
||||
@Composable
|
||||
@OptIn(UnstableApi::class)
|
||||
fun GetVideoController(
|
||||
mediaItem: MediaItem,
|
||||
mediaItem: MutableState<MediaItem>,
|
||||
videoUri: String,
|
||||
defaultToStart: Boolean = false,
|
||||
nostrUriCallback: String? = null,
|
||||
@@ -281,18 +285,22 @@ fun GetVideoController(
|
||||
UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Prepares a VideoPlayer from the foreground service.
|
||||
LaunchedEffect(key1 = videoUri) {
|
||||
DisposableEffect(key1 = videoUri) {
|
||||
// If it is not null, the user might have come back from a playing video, like clicking on
|
||||
// the notification of the video player.
|
||||
if (controller.value == null) {
|
||||
launch(Dispatchers.IO) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
PlaybackClientController.prepareController(
|
||||
uid,
|
||||
videoUri,
|
||||
nostrUriCallback,
|
||||
context
|
||||
) {
|
||||
// REQUIRED TO BE RUN IN THE MAIN THREAD
|
||||
|
||||
// checks again because of race conditions.
|
||||
if (controller.value == null) { // still prone to race conditions.
|
||||
controller.value = it
|
||||
@@ -308,7 +316,7 @@ fun GetVideoController(
|
||||
}
|
||||
}
|
||||
|
||||
controller.value?.setMediaItem(mediaItem)
|
||||
controller.value?.setMediaItem(mediaItem.value)
|
||||
controller.value?.prepare()
|
||||
} else if (controller.value != it) {
|
||||
// discards the new controller because there is an existing one
|
||||
@@ -326,7 +334,7 @@ fun GetVideoController(
|
||||
it.volume = if (defaultToStart) 0f else 1f
|
||||
}
|
||||
|
||||
it.setMediaItem(mediaItem)
|
||||
it.setMediaItem(mediaItem.value)
|
||||
it.prepare()
|
||||
}
|
||||
}
|
||||
@@ -345,29 +353,39 @@ fun GetVideoController(
|
||||
it.volume = if (defaultToStart) 0f else 1f
|
||||
}
|
||||
|
||||
it.setMediaItem(mediaItem)
|
||||
it.setMediaItem(mediaItem.value)
|
||||
it.prepare()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDispose {
|
||||
if (!keepPlaying.value) {
|
||||
// Stops and releases the media.
|
||||
controller.value?.stop()
|
||||
controller.value?.release()
|
||||
controller.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User pauses and resumes the app. What to do with videos?
|
||||
val scope = rememberCoroutineScope()
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(key1 = videoUri) {
|
||||
DisposableEffect(key1 = lifeCycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
// if the controller is null, restarts the controller with a new one
|
||||
// if the controller is not null, just continue playing what the controller was playing
|
||||
if (controller.value == null) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (controller.value == null) {
|
||||
PlaybackClientController.prepareController(
|
||||
UUID.randomUUID().toString(),
|
||||
uid,
|
||||
videoUri,
|
||||
nostrUriCallback,
|
||||
context
|
||||
) {
|
||||
// REQUIRED TO BE RUN IN THE MAIN THREAD
|
||||
|
||||
// checks again to make sure no other thread has created a controller.
|
||||
if (controller.value == null) {
|
||||
controller.value = it
|
||||
@@ -383,7 +401,7 @@ fun GetVideoController(
|
||||
}
|
||||
}
|
||||
|
||||
controller.value?.setMediaItem(mediaItem)
|
||||
controller.value?.setMediaItem(mediaItem.value)
|
||||
controller.value?.prepare()
|
||||
} else if (controller.value != it) {
|
||||
// discards the new controller because there is an existing one
|
||||
@@ -407,13 +425,6 @@ fun GetVideoController(
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
|
||||
if (!keepPlaying.value) {
|
||||
// Stops and releases the media.
|
||||
controller.value?.stop()
|
||||
controller.value?.release()
|
||||
controller.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +467,7 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Bo
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
val myModifier = remember(videoUri) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 70.dp)
|
||||
@@ -485,7 +496,9 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState<Bo
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
}
|
||||
|
||||
Box(modifier = myModifier) {
|
||||
inner(active)
|
||||
}
|
||||
}
|
||||
@@ -507,8 +520,6 @@ private fun RenderVideoPlayer(
|
||||
activeOnScreen: MutableState<Boolean>,
|
||||
onDialog: ((Boolean) -> Unit)?
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
ControlWhenPlayerIsActive(controller, keepPlaying, automaticallyStartPlayback, activeOnScreen)
|
||||
|
||||
val controllerVisible = remember(controller) {
|
||||
@@ -516,17 +527,23 @@ private fun RenderVideoPlayer(
|
||||
}
|
||||
|
||||
BoxWithConstraints() {
|
||||
AndroidView(
|
||||
modifier = if (roundedCorner) {
|
||||
MaterialTheme.colors.imageModifier
|
||||
val borders = MaterialTheme.colorScheme.imageModifier
|
||||
|
||||
val myModifier = remember {
|
||||
if (roundedCorner) {
|
||||
borders
|
||||
.defaultMinSize(minHeight = 100.dp)
|
||||
.align(Alignment.Center)
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 100.dp)
|
||||
.align(Alignment.Center)
|
||||
},
|
||||
factory = {
|
||||
}
|
||||
}
|
||||
|
||||
val factory = remember(controller) {
|
||||
{ context: Context ->
|
||||
PlayerView(context).apply {
|
||||
player = controller
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
@@ -551,10 +568,15 @@ private fun RenderVideoPlayer(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = myModifier,
|
||||
factory = factory
|
||||
)
|
||||
|
||||
waveform?.let {
|
||||
Waveform(it, controller, Modifier.align(Alignment.Center))
|
||||
Waveform(it, controller, remember { Modifier.align(Alignment.Center) })
|
||||
}
|
||||
|
||||
val startingMuteState = remember(controller) {
|
||||
@@ -575,7 +597,7 @@ private fun RenderVideoPlayer(
|
||||
controller.volume = if (mute) 0f else 1f
|
||||
}
|
||||
|
||||
KeepPlayingButton(keepPlaying, controllerVisible, Modifier.align(Alignment.TopEnd)) { newKeepPlaying: Boolean ->
|
||||
KeepPlayingButton(keepPlaying, controllerVisible, remember { Modifier.align(Alignment.TopEnd) }) { newKeepPlaying: Boolean ->
|
||||
// If something else is playing and the user marks this video to keep playing, stops the other one.
|
||||
if (newKeepPlaying) {
|
||||
if (keepPlayingMutex != null && keepPlayingMutex != controller) {
|
||||
@@ -612,7 +634,7 @@ fun Waveform(
|
||||
DrawWaveform(waveform, waveformProgress, modifier)
|
||||
|
||||
val restartFlow = remember {
|
||||
mutableStateOf(0)
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
// Keeps the screen on while playing and viewing videos.
|
||||
@@ -693,7 +715,7 @@ fun ControlWhenPlayerIsActive(
|
||||
val view = LocalView.current
|
||||
|
||||
// Keeps the screen on while playing and viewing videos.
|
||||
DisposableEffect(key1 = controller) {
|
||||
DisposableEffect(key1 = controller, key2 = view) {
|
||||
val listener = object : Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
// doesn't consider the mutex because the screen can turn off if the video
|
||||
@@ -775,7 +797,7 @@ private fun MuteButton(
|
||||
.clip(CircleShape)
|
||||
.fillMaxSize(0.6f)
|
||||
.align(Alignment.Center)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
)
|
||||
|
||||
IconButton(
|
||||
@@ -816,7 +838,7 @@ private fun KeepPlayingButton(
|
||||
.clip(CircleShape)
|
||||
.fillMaxSize(0.6f)
|
||||
.align(Alignment.Center)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
)
|
||||
|
||||
IconButton(
|
||||
@@ -827,9 +849,9 @@ private fun KeepPlayingButton(
|
||||
modifier = Size50Modifier
|
||||
) {
|
||||
if (keepPlaying.value) {
|
||||
LyricsIcon(Size22Modifier, MaterialTheme.colors.onBackground)
|
||||
LyricsIcon(Size22Modifier, MaterialTheme.colorScheme.onBackground)
|
||||
} else {
|
||||
LyricsOffIcon(Size22Modifier, MaterialTheme.colors.onBackground)
|
||||
LyricsOffIcon(Size22Modifier, MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -60,7 +60,7 @@ fun ZapRaiserRequest(
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.zapraiser_explainer),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ fun ZapRaiserRequest(
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "1000",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.ContextWrapper
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.Window
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -16,6 +15,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -29,18 +29,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 +48,9 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -79,6 +76,7 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.service.BlurHashRequester
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
@@ -86,15 +84,14 @@ import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -212,7 +209,7 @@ fun ZoomableContentView(
|
||||
}
|
||||
|
||||
var mainImageModifier = if (roundedCorner) {
|
||||
MaterialTheme.colors.imageModifier
|
||||
MaterialTheme.colorScheme.imageModifier
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
@@ -477,14 +474,16 @@ private fun AddedImageFeatures(
|
||||
if (content.blurhash != null) {
|
||||
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
|
||||
} else {
|
||||
FlowRow() {
|
||||
FlowRow(Modifier.fillMaxWidth()) {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
ClickableUrl(urlText = "${content.url} ", url = content.url)
|
||||
FlowRow(Modifier.fillMaxWidth()) {
|
||||
ClickableUrl(urlText = "${content.url} ", url = content.url)
|
||||
}
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
@@ -551,7 +550,7 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) {
|
||||
val myId = "inlineContent"
|
||||
val emptytext = buildAnnotatedString {
|
||||
withStyle(
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary).toSpanStyle()
|
||||
) {
|
||||
append("")
|
||||
appendInlineContent(myId, "[icon]")
|
||||
@@ -612,7 +611,7 @@ fun ZoomableImageDialog(
|
||||
) {
|
||||
val view = LocalView.current
|
||||
|
||||
DisposableEffect(key1 = Unit) {
|
||||
DisposableEffect(key1 = view) {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
view.windowInsetsController?.hide(
|
||||
android.view.WindowInsets.Type.systemBars()
|
||||
@@ -635,7 +634,7 @@ fun ZoomableImageDialog(
|
||||
decorFitsSystemWindows = false
|
||||
)
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
val pagerState: PagerState = rememberPagerState() { allImages.size }
|
||||
|
||||
@@ -669,6 +668,7 @@ fun ZoomableImageDialog(
|
||||
if (myContent is ZoomableUrlContent) {
|
||||
Row() {
|
||||
CopyToClipboard(content = myContent)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
SaveToGallery(url = myContent.url)
|
||||
}
|
||||
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
|
||||
@@ -689,17 +689,11 @@ private fun CopyToClipboard(
|
||||
) {
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
OutlinedButton(
|
||||
modifier = Modifier.padding(horizontal = Size5dp),
|
||||
onClick = { popupExpanded.value = true },
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
onClick = { popupExpanded.value = true }
|
||||
) {
|
||||
Icon(
|
||||
tint = Color.White,
|
||||
imageVector = Icons.Default.Share,
|
||||
modifier = Size20Modifier,
|
||||
contentDescription = stringResource(R.string.copy_url_to_clipboard)
|
||||
@@ -723,20 +717,35 @@ private fun ShareImageAction(
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
if (content is ZoomableUrlContent) {
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.url)); onDismiss() }) {
|
||||
Text(stringResource(R.string.copy_url_to_clipboard))
|
||||
}
|
||||
if (content.uri != null) {
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.uri)); onDismiss() }) {
|
||||
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.copy_url_to_clipboard))
|
||||
},
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(content.url)); onDismiss()
|
||||
}
|
||||
)
|
||||
if (content.uri != null) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
|
||||
},
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(content.uri)); onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (content is ZoomablePreloadedContent) {
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.uri)); onDismiss() }) {
|
||||
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
|
||||
},
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(content.uri)); onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,7 +807,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
|
||||
@@ -809,13 +828,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)
|
||||
@@ -823,13 +836,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)
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
|
||||
object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
@@ -12,18 +14,44 @@ object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val privKey = account.keyPair.privKey ?: return emptyList()
|
||||
|
||||
val bookmarks = account.userProfile().latestBookmarkList
|
||||
|
||||
val notes = bookmarks?.privateTaggedEvents(privKey)
|
||||
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
if (account.loginWithExternalSigner) {
|
||||
val id = bookmarks?.id
|
||||
if (id != null) {
|
||||
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id]
|
||||
if (decryptedContent == null) {
|
||||
ExternalSignerUtils.decryptBookmark(
|
||||
bookmarks.content,
|
||||
account.keyPair.pubKey.toHexKey(),
|
||||
id
|
||||
)
|
||||
} else {
|
||||
bookmarks.decryptedContent = decryptedContent
|
||||
}
|
||||
}
|
||||
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] ?: ""
|
||||
|
||||
val addresses = bookmarks?.privateTaggedAddresses(privKey)
|
||||
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
val notes = bookmarks?.privateTaggedEvents(decryptedContent)
|
||||
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
val addresses = bookmarks?.privateTaggedAddresses(decryptedContent)
|
||||
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
} else {
|
||||
val privKey = account.keyPair.privKey ?: return emptyList()
|
||||
|
||||
val notes = bookmarks?.privateTaggedEvents(privKey)
|
||||
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
|
||||
val addresses = bookmarks?.privateTaggedAddresses(privKey)
|
||||
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,50 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
}
|
||||
|
||||
override fun feed(): List<User> {
|
||||
return account.getBlockList()
|
||||
val blockList = account.getBlockList()
|
||||
val decryptedContent = blockList?.decryptedContent ?: ""
|
||||
if (account.loginWithExternalSigner) {
|
||||
if (decryptedContent.isEmpty()) return emptyList()
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateUsers(decryptedContent)
|
||||
?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateUsers(account.keyPair.privKey)
|
||||
?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenWordsFeedFilter(val account: Account) : FeedFilter<String>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun feed(): List<String> {
|
||||
val blockList = account.getBlockList()
|
||||
val decryptedContent = blockList?.decryptedContent ?: ""
|
||||
if (account.loginWithExternalSigner) {
|
||||
if (decryptedContent.isEmpty()) return emptyList()
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateWords(decryptedContent)?.toList()
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateWords(account.keyPair.privKey)?.toList()
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
|
||||
@@ -12,17 +12,19 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Logout
|
||||
import androidx.compose.material.icons.filled.RadioButtonChecked
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -33,7 +35,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -44,8 +45,10 @@ import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
@@ -60,6 +63,7 @@ import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountSwitchBottomSheet(
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -111,8 +115,9 @@ fun AccountSwitchBottomSheet(
|
||||
ArrowBackIcon()
|
||||
}
|
||||
},
|
||||
backgroundColor = Color.Transparent,
|
||||
elevation = 0.dp
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -136,6 +141,14 @@ fun DisplayAccount(
|
||||
)
|
||||
}
|
||||
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
if (baseUser == null) {
|
||||
LaunchedEffect(key1 = acc.npub) {
|
||||
launch(Dispatchers.IO) {
|
||||
@@ -173,7 +186,7 @@ fun DisplayAccount(
|
||||
.width(55.dp)
|
||||
.padding(0.dp)
|
||||
) {
|
||||
AccountPicture(it)
|
||||
AccountPicture(it, automaticallyShowProfilePicture)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
@@ -202,22 +215,21 @@ private fun ActiveMarker(acc: AccountInfo, accountViewModel: AccountViewModel) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RadioButtonChecked,
|
||||
contentDescription = stringResource(R.string.account_switch_active_account),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountPicture(user: User) {
|
||||
val profilePicture by user.live().metadata.map {
|
||||
it.user.profilePicture()
|
||||
}.observeAsState()
|
||||
private fun AccountPicture(user: User, loadProfilePicture: Boolean) {
|
||||
val profilePicture by user.live().profilePictureChanges.observeAsState()
|
||||
|
||||
RobohashAsyncImageProxy(
|
||||
robot = remember(user) { user.pubkeyHex },
|
||||
model = profilePicture,
|
||||
contentDescription = stringResource(R.string.profile_image),
|
||||
modifier = AccountPictureModifier
|
||||
modifier = AccountPictureModifier,
|
||||
loadProfilePicture = loadProfilePicture
|
||||
)
|
||||
}
|
||||
|
||||
@@ -293,7 +305,7 @@ private fun LogoutButton(
|
||||
Icon(
|
||||
imageVector = Icons.Default.Logout,
|
||||
contentDescription = stringResource(R.string.log_out),
|
||||
tint = MaterialTheme.colors.onSurface
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,28 +6,23 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.BottomNavigation
|
||||
import androidx.compose.material.BottomNavigationItem
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -39,13 +34,14 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
val bottomNavigationItems = listOf(
|
||||
val bottomNavigationItems = persistentListOf(
|
||||
Route.Home,
|
||||
Route.Message,
|
||||
Route.Video,
|
||||
@@ -61,6 +57,7 @@ enum class Keyboard {
|
||||
fun keyboardAsState(): State<Keyboard> {
|
||||
val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
|
||||
val view = LocalView.current
|
||||
|
||||
DisposableEffect(view) {
|
||||
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
@@ -104,13 +101,9 @@ private fun RenderBottomMenu(
|
||||
) {
|
||||
Column(modifier = BottomTopHeight) {
|
||||
Divider(
|
||||
thickness = 0.25.dp
|
||||
thickness = DividerThickness
|
||||
)
|
||||
BottomNavigation(
|
||||
modifier = Modifier,
|
||||
elevation = 0.dp,
|
||||
backgroundColor = MaterialTheme.colors.background
|
||||
) {
|
||||
NavigationBar(tonalElevation = Size0dp) {
|
||||
bottomNavigationItems.forEach { item ->
|
||||
HasNewItemsIcon(item, accountViewModel, navEntryState, nav)
|
||||
}
|
||||
@@ -125,16 +118,12 @@ private fun RowScope.HasNewItemsIcon(
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
nav: (Route, Boolean) -> Unit
|
||||
) {
|
||||
var hasNewItems by remember { mutableStateOf(false) }
|
||||
|
||||
WatchPossibleNotificationChanges(route, accountViewModel) {
|
||||
if (it != hasNewItems) {
|
||||
hasNewItems = it
|
||||
val selected by remember(navEntryState.value) {
|
||||
derivedStateOf {
|
||||
navEntryState.value?.destination?.route?.substringBefore("?") == route.base
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val size = remember {
|
||||
if ("Home" == route.base) 25.dp else 23.dp
|
||||
}
|
||||
@@ -142,78 +131,12 @@ private fun RowScope.HasNewItemsIcon(
|
||||
if ("Home" == route.base) 24.dp else 20.dp
|
||||
}
|
||||
|
||||
BottomIcon(
|
||||
icon = route.icon,
|
||||
size = size,
|
||||
iconSize = iconSize,
|
||||
base = route.base,
|
||||
hasNewItems = hasNewItems,
|
||||
navEntryState = navEntryState
|
||||
) { selected ->
|
||||
scope.launch {
|
||||
nav(route, selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchPossibleNotificationChanges(
|
||||
route: Route,
|
||||
accountViewModel: AccountViewModel,
|
||||
onChange: (Boolean) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val notifState by accountViewModel.accountLastReadLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = notifState, key2 = accountState) {
|
||||
launch(Dispatchers.IO) {
|
||||
onChange(route.hasNewItems(accountViewModel.account, emptySet()))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
launch(Dispatchers.IO) {
|
||||
onChange(route.hasNewItems(accountViewModel.account, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.BottomIcon(
|
||||
icon: Int,
|
||||
size: Dp,
|
||||
iconSize: Dp,
|
||||
base: String,
|
||||
hasNewItems: Boolean,
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
onClick: (Boolean) -> Unit
|
||||
) {
|
||||
val selected by remember(navEntryState.value) {
|
||||
derivedStateOf {
|
||||
navEntryState.value?.destination?.route?.substringBefore("?") == base
|
||||
}
|
||||
}
|
||||
|
||||
NavigationIcon(icon, size, iconSize, selected, hasNewItems, onClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.NavigationIcon(
|
||||
icon: Int,
|
||||
size: Dp,
|
||||
iconSize: Dp,
|
||||
selected: Boolean,
|
||||
hasNewItems: Boolean,
|
||||
onClick: (Boolean) -> Unit
|
||||
) {
|
||||
BottomNavigationItem(
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
val hasNewItems = accountViewModel.notificationDots.hasNewItems[route]?.collectAsState()
|
||||
|
||||
NotifiableIcon(
|
||||
icon,
|
||||
route.icon,
|
||||
size,
|
||||
iconSize,
|
||||
selected,
|
||||
@@ -221,51 +144,56 @@ private fun RowScope.NavigationIcon(
|
||||
)
|
||||
},
|
||||
selected = selected,
|
||||
onClick = { onClick(selected) }
|
||||
onClick = { nav(route, selected) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, hasNewItems: Boolean) {
|
||||
private fun NotifiableIcon(
|
||||
icon: Int,
|
||||
size: Dp,
|
||||
iconSize: Dp,
|
||||
selected: Boolean,
|
||||
hasNewItems: State<Boolean>?
|
||||
) {
|
||||
Box(remember { Modifier.size(size) }) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified
|
||||
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified
|
||||
)
|
||||
|
||||
if (hasNewItems) {
|
||||
Box(
|
||||
remember {
|
||||
Modifier
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.clip(shape = CircleShape)
|
||||
}.background(MaterialTheme.colors.primary),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
Text(
|
||||
"",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 12.sp,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (hasNewItems?.value == true) {
|
||||
NotificationDotIcon(
|
||||
Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationDotIcon(modifier: Modifier) {
|
||||
Box(modifier.size(Size10dp)) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(Size10dp)
|
||||
.clip(shape = CircleShape)
|
||||
}.background(MaterialTheme.colorScheme.primary),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
Text(
|
||||
"",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 12.sp,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ fun AppNavigation(
|
||||
actionableNextPage = null
|
||||
}
|
||||
|
||||
DisposableEffect(navController) {
|
||||
DisposableEffect(activity) {
|
||||
val consumer = Consumer<Intent> { intent ->
|
||||
val uri = intent?.data?.toString()
|
||||
val newPage = uriToRoute(uri)
|
||||
|
||||
@@ -17,22 +17,19 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AppBarDefaults
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -50,18 +47,19 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import coil.Coil
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
@@ -80,6 +78,7 @@ import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
@@ -88,6 +87,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadCityName
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
||||
@@ -106,9 +106,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongRoomHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.RoomNameOnlyDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShortChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowVideoStreaming
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
@@ -117,6 +117,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -131,7 +132,7 @@ import kotlinx.coroutines.launch
|
||||
fun AppTopBar(
|
||||
followLists: FollowListViewModel,
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
scaffoldState: ScaffoldState,
|
||||
drawerState: DrawerState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
navPopBack: () -> Unit
|
||||
@@ -148,7 +149,7 @@ fun AppTopBar(
|
||||
}
|
||||
}
|
||||
|
||||
RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav, navPopBack)
|
||||
RenderTopRouteBar(currentRoute, id, followLists, drawerState, accountViewModel, nav, navPopBack)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -156,16 +157,16 @@ private fun RenderTopRouteBar(
|
||||
currentRoute: String?,
|
||||
id: String?,
|
||||
followLists: FollowListViewModel,
|
||||
scaffoldState: ScaffoldState,
|
||||
drawerState: DrawerState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
navPopBack: () -> Unit
|
||||
) {
|
||||
when (currentRoute) {
|
||||
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Discover.base -> DiscoveryTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Home.base -> HomeTopBar(followLists, drawerState, accountViewModel, nav)
|
||||
Route.Video.base -> StoriesTopBar(followLists, drawerState, accountViewModel, nav)
|
||||
Route.Discover.base -> DiscoveryTopBar(followLists, drawerState, accountViewModel, nav)
|
||||
Route.Notification.base -> NotificationTopBar(followLists, drawerState, accountViewModel, nav)
|
||||
Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
|
||||
else -> {
|
||||
if (id != null) {
|
||||
@@ -176,10 +177,10 @@ private fun RenderTopRouteBar(
|
||||
Route.Community.base -> CommunityTopBar(id, accountViewModel, nav, navPopBack)
|
||||
Route.Hashtag.base -> HashTagTopBar(id, accountViewModel, navPopBack)
|
||||
Route.Geohash.base -> GeoHashTopBar(id, accountViewModel, navPopBack)
|
||||
else -> MainTopBar(scaffoldState, accountViewModel, nav)
|
||||
else -> MainTopBar(drawerState, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
MainTopBar(scaffoldState, accountViewModel, nav)
|
||||
MainTopBar(drawerState, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,7 +228,7 @@ private fun CommunityTopBar(
|
||||
nav: (String) -> Unit,
|
||||
navPopBack: () -> Unit
|
||||
) {
|
||||
LoadAddressableNote(aTagHex = id) { baseNote ->
|
||||
LoadAddressableNote(aTagHex = id, accountViewModel) { baseNote ->
|
||||
if (baseNote != null) {
|
||||
FlexibleTopBarWithBackButton(
|
||||
title = {
|
||||
@@ -288,7 +289,7 @@ private fun RenderRoomTopBar(
|
||||
if (room.users.size == 1) {
|
||||
FlexibleTopBarWithBackButton(
|
||||
title = {
|
||||
LoadUser(baseUserHex = room.users.first()) { baseUser ->
|
||||
LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser ->
|
||||
if (baseUser != null) {
|
||||
ClickableUserPicture(
|
||||
baseUser = baseUser,
|
||||
@@ -303,7 +304,7 @@ private fun RenderRoomTopBar(
|
||||
}
|
||||
},
|
||||
extendableRow = {
|
||||
LoadUser(baseUserHex = room.users.first()) {
|
||||
LoadUser(baseUserHex = room.users.first(), accountViewModel) {
|
||||
if (it != null) {
|
||||
UserCompose(
|
||||
baseUser = it,
|
||||
@@ -348,13 +349,8 @@ private fun ChannelTopBar(
|
||||
nav: (String) -> Unit,
|
||||
navPopBack: () -> Unit
|
||||
) {
|
||||
LoadChannel(baseChannelHex = id) { baseChannel ->
|
||||
LoadChannel(baseChannelHex = id, accountViewModel) { baseChannel ->
|
||||
FlexibleTopBarWithBackButton(
|
||||
prefixRow = {
|
||||
if (baseChannel is LiveActivitiesChannel) {
|
||||
ShowVideoStreaming(baseChannel, accountViewModel)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
ShortChannelHeader(
|
||||
baseChannel = baseChannel,
|
||||
@@ -377,84 +373,85 @@ fun NoTopBar() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
|
||||
fun StoriesTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
FollowListWithRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultStoriesFollowList(listName)
|
||||
accountViewModel.account.changeDefaultStoriesFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
|
||||
fun HomeTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
FollowListWithRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultHomeFollowList(listName)
|
||||
if (listName.type == CodeNameType.ROUTE) {
|
||||
nav(listName.code)
|
||||
} else {
|
||||
accountViewModel.account.changeDefaultHomeFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
|
||||
fun NotificationTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
FollowListWithoutRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultNotificationFollowList(listName)
|
||||
accountViewModel.account.changeDefaultNotificationFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
|
||||
fun DiscoveryTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
FollowListWithoutRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultDiscoveryFollowList(listName)
|
||||
accountViewModel.account.changeDefaultDiscoveryFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(scaffoldState, accountViewModel, nav) {
|
||||
fun MainTopBar(drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) {
|
||||
AmethystClickableIcon()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun GenericMainTopBar(
|
||||
scaffoldState: ScaffoldState,
|
||||
drawerState: DrawerState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
content: @Composable (AccountViewModel) -> Unit
|
||||
) {
|
||||
Column(modifier = BottomTopHeight) {
|
||||
MyTopAppBar(
|
||||
elevation = 0.dp,
|
||||
backgroundColor = MaterialTheme.colors.surface,
|
||||
TopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
title = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -477,7 +474,7 @@ fun GenericMainTopBar(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
LoggedInUserPictureDrawer(accountViewModel) {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.open()
|
||||
drawerState.open()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -487,7 +484,7 @@ fun GenericMainTopBar(
|
||||
}
|
||||
}
|
||||
)
|
||||
Divider(thickness = 0.25.dp)
|
||||
Divider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,9 +503,16 @@ private fun LoggedInUserPictureDrawer(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val profilePicture by accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState()
|
||||
|
||||
val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex }
|
||||
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onClick
|
||||
) {
|
||||
@@ -517,43 +521,86 @@ private fun LoggedInUserPictureDrawer(
|
||||
model = profilePicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = HeaderPictureModifier,
|
||||
contentScale = ContentScale.Crop
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = automaticallyShowProfilePicture
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowList(followListsModel: FollowListViewModel, listName: String, withGlobal: Boolean, onChange: (String) -> Unit) {
|
||||
val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows))
|
||||
val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global))
|
||||
|
||||
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
||||
|
||||
val followLists by followListsModel.followLists.collectAsState()
|
||||
|
||||
val allLists = remember(followLists) {
|
||||
(defaultOptions + followLists)
|
||||
}
|
||||
|
||||
val followNames by remember(followLists) {
|
||||
derivedStateOf {
|
||||
allLists.map { it.second }.toImmutableList()
|
||||
}
|
||||
}
|
||||
fun FollowListWithRoutes(
|
||||
followListsModel: FollowListViewModel,
|
||||
listName: String,
|
||||
onChange: (CodeName) -> Unit
|
||||
) {
|
||||
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsState()
|
||||
|
||||
SimpleTextSpinner(
|
||||
placeholder = allLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
|
||||
options = followNames,
|
||||
placeholderCode = listName,
|
||||
options = allLists,
|
||||
onSelect = {
|
||||
onChange(allLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
|
||||
onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowListWithoutRoutes(
|
||||
followListsModel: FollowListViewModel,
|
||||
listName: String,
|
||||
onChange: (CodeName) -> Unit
|
||||
) {
|
||||
val allLists by followListsModel.kind3GlobalPeople.collectAsState()
|
||||
|
||||
SimpleTextSpinner(
|
||||
placeholderCode = listName,
|
||||
options = allLists,
|
||||
onSelect = {
|
||||
onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum class CodeNameType {
|
||||
HARDCODED, PEOPLE_LIST, ROUTE
|
||||
}
|
||||
|
||||
abstract class Name {
|
||||
abstract fun name(): String
|
||||
open fun name(context: Context) = name()
|
||||
}
|
||||
|
||||
class GeoHashName(val geoHashTag: String) : Name() {
|
||||
override fun name() = "/g/$geoHashTag"
|
||||
}
|
||||
class HashtagName(val hashTag: String) : Name() {
|
||||
override fun name() = "#$hashTag"
|
||||
}
|
||||
class ResourceName(val resourceId: Int) : Name() {
|
||||
override fun name() = " $resourceId " // Space to make sure it goes first
|
||||
override fun name(context: Context) = context.getString(resourceId)
|
||||
}
|
||||
|
||||
class PeopleListName(val note: AddressableNote) : Name() {
|
||||
override fun name() = (note.event as? PeopleListEvent)?.nameOrTitle() ?: note.dTag() ?: ""
|
||||
}
|
||||
class CommunityName(val note: AddressableNote) : Name() {
|
||||
override fun name() = "/n/${(note.dTag() ?: "")}"
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class CodeName(val code: String, val name: Name, val type: CodeNameType)
|
||||
|
||||
@Stable
|
||||
class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
private var _followLists = MutableStateFlow<ImmutableList<Pair<String, String>>>(emptyList<Pair<String, String>>().toPersistentList())
|
||||
val followLists = _followLists.asStateFlow()
|
||||
val kind3Follow = CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows), CodeNameType.HARDCODED)
|
||||
val globalFollow = CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global), CodeNameType.HARDCODED)
|
||||
|
||||
private var _kind3GlobalPeopleRoutes = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
|
||||
val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.asStateFlow()
|
||||
|
||||
private var _kind3GlobalPeople = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
|
||||
val kind3GlobalPeople = _kind3GlobalPeople.asStateFlow()
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
@@ -571,14 +618,38 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
event.pubKey == account.userProfile().pubkeyHex &&
|
||||
(event.tags.size > 1 || event.content.length > 50)
|
||||
) {
|
||||
Pair(event.dTag(), event.dTag())
|
||||
CodeName(event.address().toTag(), PeopleListName(it.value), CodeNameType.PEOPLE_LIST)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.sortedBy { it.second }.toImmutableList()
|
||||
}.sortedBy { it.name.name() }
|
||||
|
||||
if (!equalImmutableLists(_followLists.value, newFollowLists)) {
|
||||
_followLists.emit(newFollowLists)
|
||||
val communities = account.userProfile().cachedFollowingCommunitiesSet().mapNotNull {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
||||
CodeName("Community/${communityNote.idHex}", CommunityName(communityNote), CodeNameType.ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
val hashtags = account.userProfile().cachedFollowingTagSet().map {
|
||||
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
|
||||
}
|
||||
|
||||
val geotags = account.userProfile().cachedFollowingGeohashSet().map {
|
||||
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
|
||||
}
|
||||
|
||||
val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() }
|
||||
|
||||
val kind3GlobalPeopleRouteList = listOf(listOf(kind3Follow, globalFollow), newFollowLists, routeList).flatten().toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(_kind3GlobalPeopleRoutes.value, kind3GlobalPeopleRouteList)) {
|
||||
_kind3GlobalPeopleRoutes.emit(kind3GlobalPeopleRouteList)
|
||||
}
|
||||
|
||||
val kind3GlobalPeopleList = listOf(listOf(kind3Follow, globalFollow), newFollowLists).flatten().toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(_kind3GlobalPeople.value, kind3GlobalPeopleList)) {
|
||||
_kind3GlobalPeople.emit(kind3GlobalPeopleList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +661,10 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
checkNotInMainThread()
|
||||
if (newNotes.any { it.event is PeopleListEvent }) {
|
||||
if (newNotes.any {
|
||||
it.event?.pubKey() == account.userProfile().pubkeyHex && (it.event is PeopleListEvent || it.event is ContactListEvent)
|
||||
}
|
||||
) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
@@ -611,15 +685,24 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
|
||||
@Composable
|
||||
fun SimpleTextSpinner(
|
||||
placeholder: String,
|
||||
options: ImmutableList<String>,
|
||||
explainers: ImmutableList<String>? = null,
|
||||
placeholderCode: String,
|
||||
options: ImmutableList<CodeName>,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
var optionsShowing by remember { mutableStateOf(false) }
|
||||
var currentText by remember(placeholder) { mutableStateOf(placeholder) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val selectAnOption = stringResource(
|
||||
id = R.string.select_an_option
|
||||
)
|
||||
|
||||
var currentText by remember {
|
||||
mutableStateOf(
|
||||
options.firstOrNull { it.code == placeholderCode }?.name?.name(context) ?: selectAnOption
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
@@ -627,12 +710,12 @@ fun SimpleTextSpinner(
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
Text(placeholder)
|
||||
Text(currentText)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
Box(
|
||||
@@ -649,20 +732,78 @@ fun SimpleTextSpinner(
|
||||
|
||||
if (optionsShowing) {
|
||||
options.isNotEmpty().also {
|
||||
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) {
|
||||
currentText = options[it]
|
||||
optionsShowing = false
|
||||
onSelect(it)
|
||||
SpinnerSelectionDialog(
|
||||
options = options,
|
||||
onDismiss = { optionsShowing = false },
|
||||
onSelect = {
|
||||
currentText = options[it].name.name(context)
|
||||
optionsShowing = false
|
||||
onSelect(it)
|
||||
}
|
||||
) {
|
||||
RenderOption(it.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderOption(option: Name) {
|
||||
when (option) {
|
||||
is GeoHashName -> {
|
||||
LoadCityName(option.geoHashTag) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
is HashtagName -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = option.name(), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
is ResourceName -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(id = option.resourceId), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
is PeopleListName -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = option.name(), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
is CommunityName -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val name by option.note.live().metadata.map {
|
||||
"/n/" + (it.note as? AddressableNote)?.dTag()
|
||||
}.observeAsState()
|
||||
|
||||
Text(text = name ?: "", color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopBarWithBackButton(caption: String, popBack: () -> Unit) {
|
||||
Column(modifier = BottomTopHeight) {
|
||||
MyTopAppBar(
|
||||
elevation = 0.dp,
|
||||
TopAppBar(
|
||||
title = { Text(caption) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
@@ -680,15 +821,12 @@ fun TopBarWithBackButton(caption: String, popBack: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun FlexibleTopBarWithBackButton(
|
||||
prefixRow: (@Composable () -> Unit)? = null,
|
||||
title: @Composable RowScope.() -> Unit,
|
||||
extendableRow: (@Composable () -> Unit)? = null,
|
||||
popBack: () -> Unit
|
||||
) {
|
||||
Column() {
|
||||
MyExtensibleTopAppBar(
|
||||
elevation = 0.dp,
|
||||
prefixRow = prefixRow,
|
||||
title = title,
|
||||
extendableRow = extendableRow,
|
||||
navigationIcon = {
|
||||
@@ -766,135 +904,53 @@ fun debugState(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MyTopAppBar(
|
||||
title: @Composable RowScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
navigationIcon: @Composable (() -> Unit)? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
backgroundColor: Color = MaterialTheme.colors.surface,
|
||||
contentColor: Color = contentColorFor(backgroundColor),
|
||||
elevation: Dp = AppBarDefaults.TopAppBarElevation
|
||||
) {
|
||||
Surface(
|
||||
contentColor = contentColor,
|
||||
elevation = elevation,
|
||||
modifier = modifier
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppBarDefaults.ContentPadding),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (navigationIcon == null) {
|
||||
Spacer(TitleInsetWithoutIcon)
|
||||
} else {
|
||||
Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentAlpha provides ContentAlpha.high,
|
||||
content = navigationIcon
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProvideTextStyle(MaterialTheme.typography.h6) {
|
||||
title()
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = actions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MyExtensibleTopAppBar(
|
||||
prefixRow: (@Composable () -> Unit)? = null,
|
||||
title: @Composable RowScope.() -> Unit,
|
||||
extendableRow: (@Composable () -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
navigationIcon: @Composable (() -> Unit)? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
backgroundColor: Color = MaterialTheme.colors.surface,
|
||||
contentColor: Color = contentColorFor(backgroundColor),
|
||||
elevation: Dp = AppBarDefaults.TopAppBarElevation
|
||||
actions: @Composable RowScope.() -> Unit = {}
|
||||
) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
Surface(
|
||||
color = backgroundColor,
|
||||
contentColor = contentColor,
|
||||
elevation = elevation,
|
||||
modifier = modifier.clickable {
|
||||
Column(
|
||||
Modifier.clickable {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppBarDefaults.ContentPadding),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(modifier = BottomTopHeight) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
title()
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
navigationIcon = {
|
||||
if (navigationIcon == null) {
|
||||
Spacer(TitleInsetWithoutIcon)
|
||||
} else {
|
||||
Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentAlpha provides ContentAlpha.high,
|
||||
content = navigationIcon
|
||||
)
|
||||
navigationIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = actions
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProvideTextStyle(MaterialTheme.typography.h6) {
|
||||
title()
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = actions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (expanded.value && extendableRow != null) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
extendableRow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prefixRow != null) {
|
||||
prefixRow()
|
||||
if (expanded.value && extendableRow != null) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
extendableRow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,21 +22,20 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -70,8 +69,10 @@ import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||
@@ -92,17 +93,25 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DrawerContent(
|
||||
nav: (String) -> Unit,
|
||||
scaffoldState: ScaffoldState,
|
||||
sheetState: ModalBottomSheetState,
|
||||
drawerState: DrawerState,
|
||||
openSheet: () -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.background
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
ModalDrawerSheet(
|
||||
drawerContainerColor = MaterialTheme.colorScheme.background,
|
||||
drawerTonalElevation = 0.dp
|
||||
) {
|
||||
Column() {
|
||||
ProfileContent(
|
||||
@@ -111,7 +120,7 @@ fun DrawerContent(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 25.dp)
|
||||
.padding(top = 70.dp),
|
||||
scaffoldState,
|
||||
drawerState,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
@@ -121,15 +130,15 @@ fun DrawerContent(
|
||||
)
|
||||
ListContent(
|
||||
nav,
|
||||
scaffoldState,
|
||||
sheetState,
|
||||
drawerState,
|
||||
openSheet,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
accountViewModel
|
||||
)
|
||||
|
||||
BottomContent(accountViewModel.account.userProfile(), scaffoldState, nav)
|
||||
BottomContent(accountViewModel.account.userProfile(), drawerState, automaticallyShowProfilePicture, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +147,7 @@ fun DrawerContent(
|
||||
fun ProfileContent(
|
||||
baseAccountUser: User,
|
||||
modifier: Modifier = Modifier,
|
||||
scaffoldState: ScaffoldState,
|
||||
drawerState: DrawerState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@@ -150,11 +159,19 @@ fun ProfileContent(
|
||||
|
||||
val profileBanner = remember(accountUserState) { accountUserState?.user?.info?.banner?.ifBlank { null } }
|
||||
val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture() }
|
||||
val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() }
|
||||
val bestDisplayName = remember(accountUserState) { accountUserState?.user?.bestDisplayName() }
|
||||
// val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() }
|
||||
val bestDisplayName = remember(accountUserState) { accountUserState?.user?.toBestDisplayName() }
|
||||
val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
|
||||
val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" }
|
||||
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
if (profileBanner != null) {
|
||||
AsyncImage(
|
||||
@@ -170,8 +187,7 @@ fun ProfileContent(
|
||||
painter = painterResource(R.drawable.profile_banner),
|
||||
contentDescription = stringResource(R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.height(120.dp)
|
||||
)
|
||||
}
|
||||
@@ -185,14 +201,15 @@ fun ProfileContent(
|
||||
.width(100.dp)
|
||||
.height(100.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.border(3.dp, MaterialTheme.colors.background, CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.clickable(onClick = {
|
||||
nav(route)
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
})
|
||||
}),
|
||||
loadProfilePicture = automaticallyShowProfilePicture
|
||||
)
|
||||
|
||||
if (bestDisplayName != null) {
|
||||
@@ -204,7 +221,7 @@ fun ProfileContent(
|
||||
.clickable(onClick = {
|
||||
nav(route)
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
}),
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -213,24 +230,6 @@ fun ProfileContent(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (bestUserName != null) {
|
||||
CreateTextWithEmoji(
|
||||
text = remember { " @$bestUserName" },
|
||||
tags = tags,
|
||||
color = Color.LightGray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = {
|
||||
nav(route)
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Row(Modifier.padding(top = Size10dp)) {
|
||||
EditStatusBox(baseAccountUser, accountViewModel)
|
||||
@@ -242,7 +241,7 @@ fun ProfileContent(
|
||||
.clickable(onClick = {
|
||||
nav(route)
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
})
|
||||
) {
|
||||
@@ -257,7 +256,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
LoadStatuses(user = baseAccountUser) { statuses ->
|
||||
LoadStatuses(user = baseAccountUser, accountViewModel) { statuses ->
|
||||
if (statuses.isEmpty()) {
|
||||
val currentStatus = remember {
|
||||
mutableStateOf("")
|
||||
@@ -276,7 +275,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.status_update),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -292,7 +291,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (hasChanged) {
|
||||
UserStatusSendButton() {
|
||||
SendButton() {
|
||||
accountViewModel.createStatus(currentStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
@@ -322,7 +321,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.status_update),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -338,7 +337,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (hasChanged) {
|
||||
UserStatusSendButton() {
|
||||
SendButton() {
|
||||
accountViewModel.updateStatus(it, thisStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
@@ -356,7 +355,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserStatusSendButton(onClick: () -> Unit) {
|
||||
fun SendButton(onClick: () -> Unit) {
|
||||
IconButton(
|
||||
modifier = Size26Modifier,
|
||||
onClick = onClick
|
||||
@@ -365,7 +364,7 @@ fun UserStatusSendButton(onClick: () -> Unit) {
|
||||
imageVector = Icons.Default.Send,
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -380,7 +379,7 @@ fun UserStatusDeleteButton(onClick: () -> Unit) {
|
||||
imageVector = Icons.Default.Delete,
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -441,12 +440,11 @@ fun WatchFollower(baseAccountUser: User, onReady: (String) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun ListContent(
|
||||
nav: (String) -> Unit,
|
||||
scaffoldState: ScaffoldState,
|
||||
sheetState: ModalBottomSheetState,
|
||||
drawerState: DrawerState,
|
||||
openSheet: () -> Unit,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
@@ -474,18 +472,18 @@ fun ListContent(
|
||||
NavigationRow(
|
||||
title = stringResource(R.string.profile),
|
||||
icon = Route.Profile.icon,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
nav = nav,
|
||||
scaffoldState = scaffoldState,
|
||||
drawerState = drawerState,
|
||||
route = route
|
||||
)
|
||||
|
||||
NavigationRow(
|
||||
title = stringResource(R.string.bookmarks),
|
||||
icon = Route.Bookmarks.icon,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
nav = nav,
|
||||
scaffoldState = scaffoldState,
|
||||
drawerState = drawerState,
|
||||
route = Route.Bookmarks.route
|
||||
)
|
||||
|
||||
@@ -493,7 +491,7 @@ fun ListContent(
|
||||
accountViewModel = accountViewModel,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
wantsToEditRelays = true
|
||||
}
|
||||
@@ -502,32 +500,34 @@ fun ListContent(
|
||||
NavigationRow(
|
||||
title = stringResource(R.string.security_filters),
|
||||
icon = Route.BlockedUsers.icon,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
nav = nav,
|
||||
scaffoldState = scaffoldState,
|
||||
drawerState = drawerState,
|
||||
route = Route.BlockedUsers.route
|
||||
)
|
||||
|
||||
IconRow(
|
||||
title = stringResource(R.string.backup_keys),
|
||||
icon = R.drawable.ic_key,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
accountViewModel.account.keyPair.privKey?.let {
|
||||
IconRow(
|
||||
title = stringResource(R.string.backup_keys),
|
||||
icon = R.drawable.ic_key,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
backupDialogOpen = true
|
||||
}
|
||||
backupDialogOpen = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val textTorProxy = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor_short)
|
||||
IconRow(
|
||||
title = textTorProxy,
|
||||
icon = R.drawable.ic_tor,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onLongClick = {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
conectOrbotDialogOpen = true
|
||||
},
|
||||
@@ -536,7 +536,7 @@ fun ListContent(
|
||||
disconnectTorDialog = true
|
||||
} else {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
conectOrbotDialogOpen = true
|
||||
}
|
||||
@@ -546,9 +546,9 @@ fun ListContent(
|
||||
NavigationRow(
|
||||
title = stringResource(R.string.settings),
|
||||
icon = Route.Settings.icon,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
nav = nav,
|
||||
scaffoldState = scaffoldState,
|
||||
drawerState = drawerState,
|
||||
route = Route.Settings.route
|
||||
)
|
||||
|
||||
@@ -557,8 +557,8 @@ fun ListContent(
|
||||
IconRow(
|
||||
title = stringResource(R.string.drawer_accounts),
|
||||
icon = R.drawable.manage_accounts,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
onClick = { coroutineScope.launch { sheetState.show() } }
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = openSheet
|
||||
)
|
||||
}
|
||||
|
||||
@@ -566,7 +566,7 @@ fun ListContent(
|
||||
NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav)
|
||||
}
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(accountViewModel.account, onClose = { backupDialogOpen = false })
|
||||
AccountBackupDialog(accountViewModel, onClose = { backupDialogOpen = false })
|
||||
}
|
||||
if (conectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
@@ -577,6 +577,12 @@ fun ListContent(
|
||||
checked = true
|
||||
enableTor(accountViewModel.account, true, proxyPort, context, coroutineScope)
|
||||
},
|
||||
onError = {
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.could_not_connect_to_tor),
|
||||
it
|
||||
)
|
||||
},
|
||||
proxyPort
|
||||
)
|
||||
}
|
||||
@@ -649,7 +655,7 @@ private fun RenderRelayStatus(
|
||||
}
|
||||
}
|
||||
|
||||
val placeHolder = MaterialTheme.colors.placeholderText
|
||||
val placeHolder = MaterialTheme.colorScheme.placeholderText
|
||||
|
||||
val color by remember(relayPool) {
|
||||
derivedStateOf {
|
||||
@@ -664,7 +670,7 @@ private fun RenderRelayStatus(
|
||||
Text(
|
||||
text = text,
|
||||
color = color,
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
@@ -674,14 +680,14 @@ fun NavigationRow(
|
||||
icon: Int,
|
||||
tint: Color,
|
||||
nav: (String) -> Unit,
|
||||
scaffoldState: ScaffoldState,
|
||||
drawerState: DrawerState,
|
||||
route: String
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
IconRow(title, icon, tint, onClick = {
|
||||
nav(route)
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -735,7 +741,7 @@ fun IconRowRelays(accountViewModel: AccountViewModel, onClick: () -> Unit) {
|
||||
painter = painterResource(R.drawable.relays),
|
||||
null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = MaterialTheme.colors.onSurface
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -752,7 +758,7 @@ fun IconRowRelays(accountViewModel: AccountViewModel, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomContent(user: User, scaffoldState: ScaffoldState, nav: (String) -> Unit) {
|
||||
fun BottomContent(user: User, drawerState: DrawerState, loadProfilePicture: Boolean, nav: (String) -> Unit) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
// store the dialog open or close state
|
||||
@@ -791,21 +797,21 @@ fun BottomContent(user: User, scaffoldState: ScaffoldState, nav: (String) -> Uni
|
||||
painter = painterResource(R.drawable.ic_theme),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}*/
|
||||
Box(modifier = Modifier.weight(1F))
|
||||
IconButton(onClick = {
|
||||
dialogOpen = true
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -814,10 +820,11 @@ fun BottomContent(user: User, scaffoldState: ScaffoldState, nav: (String) -> Uni
|
||||
if (dialogOpen) {
|
||||
ShowQRDialog(
|
||||
user,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
onScan = {
|
||||
dialogOpen = false
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
nav(it)
|
||||
},
|
||||
|
||||
@@ -150,6 +150,14 @@ sealed class Route(
|
||||
route = "Settings",
|
||||
icon = R.drawable.ic_settings
|
||||
)
|
||||
|
||||
companion object {
|
||||
val InvertedLayouts = setOf(
|
||||
Channel.route,
|
||||
Room.route,
|
||||
RoomByAuthor.route
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// **
|
||||
|
||||
@@ -10,13 +10,14 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -36,6 +37,8 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddBountyAmountViewModel : ViewModel() {
|
||||
private var account: Account? = null
|
||||
@@ -80,6 +83,7 @@ class AddBountyAmountViewModel : ViewModel() {
|
||||
fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onClose: () -> Unit) {
|
||||
val postViewModel: AddBountyAmountViewModel = viewModel()
|
||||
postViewModel.load(accountViewModel.account, bounty)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
@@ -89,7 +93,11 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
|
||||
)
|
||||
) {
|
||||
Surface() {
|
||||
Column(modifier = Modifier.padding(10.dp).width(IntrinsicSize.Min)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -102,8 +110,10 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
isActive = postViewModel.hasChanged()
|
||||
)
|
||||
@@ -128,7 +138,7 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "10000, 50000, 5000000",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
color = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
},
|
||||
singleLine = true
|
||||
|
||||
@@ -9,14 +9,14 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MilitaryTech
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -59,16 +58,12 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
if (note == null) {
|
||||
BlankNote(Modifier, isInnerNote)
|
||||
} else {
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = likeSetCard) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val isNew = likeSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
accountViewModel.account.markAsRead(routeForLastRead, likeSetCard.createdAt())
|
||||
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, likeSetCard.createdAt()) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
@@ -117,7 +112,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
modifier = Modifier
|
||||
.size(25.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -134,7 +129,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
|
||||
Text(
|
||||
timeAgo(note.createdAt(), context = context),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
@@ -146,7 +141,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = Modifier.size(15.dp),
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
|
||||
NoteDropDownMenu(note, popupExpanded, accountViewModel)
|
||||
|
||||
@@ -7,11 +7,11 @@ import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -108,7 +108,7 @@ fun HiddenNote(
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
|
||||
@@ -15,9 +15,9 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
@@ -275,39 +276,26 @@ private fun CheckNewAndRenderChannelCard(
|
||||
showPopup: () -> Unit,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember {
|
||||
mutableStateOf<Color>(
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
|
||||
launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = accountViewModel.account.loadLastRead(it)
|
||||
|
||||
val createdAt = baseNote.createdAt()
|
||||
if (createdAt != null) {
|
||||
accountViewModel.account.markAsRead(it, createdAt)
|
||||
|
||||
val isNew = createdAt > lastTime
|
||||
|
||||
val newBackgroundColor = if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
newItemColor.compositeOver(parentBackgroundColor.value)
|
||||
} else {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
routeForLastRead?.let {
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, baseNote.createdAt()) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
newItemColor.compositeOver(parentBackgroundColor.value)
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
}
|
||||
} ?: run {
|
||||
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
@@ -315,6 +303,13 @@ private fun CheckNewAndRenderChannelCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,6 +505,8 @@ fun RenderLiveActivityThumb(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
ChannelHeader(
|
||||
channelHex = remember { baseNote.idHex },
|
||||
showVideo = false,
|
||||
@@ -614,9 +611,9 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
|
||||
)
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
description?.let {
|
||||
@@ -624,7 +621,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
|
||||
Row() {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 14.sp
|
||||
@@ -646,7 +643,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
|
||||
fun RenderChannelThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteEvent = baseNote.event as? ChannelCreateEvent ?: return
|
||||
|
||||
LoadChannel(baseChannelHex = baseNote.idHex) {
|
||||
LoadChannel(baseChannelHex = baseNote.idHex, accountViewModel) {
|
||||
RenderChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
@@ -716,7 +713,9 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().fillMaxHeight()
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
@@ -728,9 +727,9 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
|
||||
)
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
|
||||
description?.let {
|
||||
@@ -738,7 +737,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
|
||||
Row() {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 14.sp
|
||||
@@ -768,7 +767,7 @@ fun Gallery(users: List<User>, accountViewModel: AccountViewModel) {
|
||||
Text(
|
||||
text = remember(users) { " + " + (showCount(users.size - 6)).toString() },
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -47,9 +47,10 @@ import androidx.lifecycle.map
|
||||
import com.patrykandpatrick.vico.core.extension.forEachIndexedExtended
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -99,7 +100,7 @@ fun ChatroomComposeChannelOrUser(
|
||||
}
|
||||
|
||||
if (channelHex != null) {
|
||||
ChatroomChannel(channelHex, baseNote, accountViewModel, nav)
|
||||
ChatroomChannel(channelHex!!, baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
ChatroomPrivateMessages(baseNote, accountViewModel, nav)
|
||||
}
|
||||
@@ -134,12 +135,12 @@ private fun ChatroomPrivateMessages(
|
||||
|
||||
@Composable
|
||||
private fun ChatroomChannel(
|
||||
channelHex: HexKey?,
|
||||
channelHex: HexKey,
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadChannel(baseChannelHex = channelHex!!) { channel ->
|
||||
LoadChannel(baseChannelHex = channelHex, accountViewModel) { channel ->
|
||||
ChannelRoomCompose(baseNote, channel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
@@ -186,6 +187,14 @@ private fun ChannelRoomCompose(
|
||||
|
||||
val hasNewMessages = remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
if (hasNewMessages.value != newHasNewMessages) {
|
||||
hasNewMessages.value = newHasNewMessages
|
||||
@@ -201,6 +210,7 @@ private fun ChannelRoomCompose(
|
||||
channelLastTime = remember(note) { note.createdAt() },
|
||||
channelLastContent = remember(note) { "$authorName: $description" },
|
||||
hasNewMessages = hasNewMessages,
|
||||
loadProfilePicture = automaticallyShowProfilePicture,
|
||||
onClick = { nav(route) }
|
||||
)
|
||||
}
|
||||
@@ -208,7 +218,7 @@ private fun ChannelRoomCompose(
|
||||
@Composable
|
||||
private fun ChannelTitleWithLabelInfo(channelName: String, modifier: Modifier) {
|
||||
val label = stringResource(id = R.string.public_chat)
|
||||
val placeHolderColor = MaterialTheme.colors.placeholderText
|
||||
val placeHolderColor = MaterialTheme.colorScheme.placeholderText
|
||||
val channelNameAndBoostInfo = remember(channelName) {
|
||||
buildAnnotatedString {
|
||||
withStyle(
|
||||
@@ -256,11 +266,10 @@ private fun UserRoomCompose(
|
||||
note.createdAt()
|
||||
}
|
||||
}
|
||||
|
||||
val content by remember(note) {
|
||||
derivedStateOf {
|
||||
mutableStateOf(
|
||||
accountViewModel.decrypt(note)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
@@ -278,7 +287,7 @@ private fun UserRoomCompose(
|
||||
)
|
||||
},
|
||||
channelTitle = {
|
||||
RoomNameDisplay(room, it, accountViewModel.userProfile())
|
||||
RoomNameDisplay(room, it, accountViewModel)
|
||||
},
|
||||
channelLastTime = createAt,
|
||||
channelLastContent = content,
|
||||
@@ -288,20 +297,20 @@ private fun UserRoomCompose(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, loggedInUser: User) {
|
||||
val roomSubject by loggedInUser.live().messages.map {
|
||||
fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, accountViewModel: AccountViewModel) {
|
||||
val roomSubject by accountViewModel.userProfile().live().messages.map {
|
||||
it.user.privateChatrooms[room]?.subject
|
||||
}.distinctUntilChanged().observeAsState(loggedInUser.privateChatrooms[room]?.subject)
|
||||
}.distinctUntilChanged().observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject)
|
||||
|
||||
Crossfade(targetState = roomSubject, modifier) {
|
||||
if (it != null && it.isNotBlank()) {
|
||||
if (room.users.size > 1) {
|
||||
DisplayRoomSubject(it)
|
||||
} else {
|
||||
DisplayUserAndSubject(room.users.first(), it)
|
||||
DisplayUserAndSubject(room.users.first(), it, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
DisplayUserSetAsSubject(room)
|
||||
DisplayUserSetAsSubject(room, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +318,8 @@ fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, loggedInUser: User) {
|
||||
@Composable
|
||||
private fun DisplayUserAndSubject(
|
||||
user: HexKey,
|
||||
subject: String
|
||||
subject: String,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row() {
|
||||
Text(
|
||||
@@ -323,7 +333,7 @@ private fun DisplayUserAndSubject(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
LoadUser(baseUserHex = user) {
|
||||
LoadUser(baseUserHex = user, accountViewModel = accountViewModel) {
|
||||
it?.let {
|
||||
UsernameDisplay(it, Modifier.weight(1f))
|
||||
}
|
||||
@@ -334,6 +344,7 @@ private fun DisplayUserAndSubject(
|
||||
@Composable
|
||||
fun DisplayUserSetAsSubject(
|
||||
room: ChatroomKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
fontWeight: FontWeight = FontWeight.Bold
|
||||
) {
|
||||
val userList = remember(room) {
|
||||
@@ -343,7 +354,7 @@ fun DisplayUserSetAsSubject(
|
||||
if (userList.size == 1) {
|
||||
// Regular Design
|
||||
Row() {
|
||||
LoadUser(baseUserHex = userList[0]) {
|
||||
LoadUser(baseUserHex = userList[0], accountViewModel) {
|
||||
it?.let {
|
||||
UsernameDisplay(it, Modifier.weight(1f), fontWeight = fontWeight)
|
||||
}
|
||||
@@ -352,7 +363,7 @@ fun DisplayUserSetAsSubject(
|
||||
} else {
|
||||
Row() {
|
||||
userList.take(4).forEachIndexedExtended { index, isFirst, isLast, value ->
|
||||
LoadUser(baseUserHex = value) {
|
||||
LoadUser(baseUserHex = value, accountViewModel) {
|
||||
it?.let {
|
||||
ShortUsernameDisplay(baseUser = it, fontWeight = fontWeight)
|
||||
}
|
||||
@@ -404,9 +415,7 @@ private fun WatchNotificationChanges(
|
||||
accountViewModel: AccountViewModel,
|
||||
onNewStatus: (Boolean) -> Unit
|
||||
) {
|
||||
val cacheState by accountViewModel.accountLastReadLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = note, cacheState) {
|
||||
LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.value) {
|
||||
launch(Dispatchers.IO) {
|
||||
note.event?.createdAt()?.let {
|
||||
val lastTime = accountViewModel.account.loadLastRead(route)
|
||||
@@ -417,15 +426,14 @@ private fun WatchNotificationChanges(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadUser(baseUserHex: String, content: @Composable (User?) -> Unit) {
|
||||
fun LoadUser(baseUserHex: String, accountViewModel: AccountViewModel, content: @Composable (User?) -> Unit) {
|
||||
var user by remember(baseUserHex) {
|
||||
mutableStateOf(LocalCache.getUserIfExists(baseUserHex))
|
||||
mutableStateOf(accountViewModel.getUserIfExists(baseUserHex))
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
LaunchedEffect(key1 = baseUserHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newUser = LocalCache.checkGetOrCreateUser(baseUserHex)
|
||||
accountViewModel.checkGetOrCreateUser(baseUserHex) { newUser ->
|
||||
if (user != newUser) {
|
||||
user = newUser
|
||||
}
|
||||
@@ -444,6 +452,7 @@ fun ChannelName(
|
||||
channelLastTime: Long?,
|
||||
channelLastContent: String?,
|
||||
hasNewMessages: MutableState<Boolean>,
|
||||
loadProfilePicture: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
ChannelName(
|
||||
@@ -457,7 +466,8 @@ fun ChannelName(
|
||||
.width(Size55dp)
|
||||
.height(Size55dp)
|
||||
.clip(shape = CircleShape)
|
||||
}
|
||||
},
|
||||
loadProfilePicture = loadProfilePicture
|
||||
)
|
||||
},
|
||||
channelTitle,
|
||||
@@ -518,7 +528,7 @@ private fun SecondRow(channelLastContent: String?, hasNewMessages: MutableState<
|
||||
if (channelLastContent != null) {
|
||||
Text(
|
||||
channelLastContent,
|
||||
color = MaterialTheme.colors.grayText,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
@@ -527,7 +537,7 @@ private fun SecondRow(channelLastContent: String?, hasNewMessages: MutableState<
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.referenced_event_not_found),
|
||||
color = MaterialTheme.colors.grayText,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
@@ -561,7 +571,7 @@ private fun TimeAgo(channelLastTime: Long?) {
|
||||
}
|
||||
Text(
|
||||
text = timeAgo,
|
||||
color = MaterialTheme.colors.grayText,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
@@ -574,7 +584,7 @@ fun NewItemsBubble() {
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.primary),
|
||||
.background(MaterialTheme.colorScheme.primary),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
|
||||
@@ -15,16 +15,17 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -42,8 +43,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
@@ -66,11 +69,10 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -228,9 +230,9 @@ fun NormalChatNote(
|
||||
}
|
||||
}
|
||||
|
||||
val loggedInColors = MaterialTheme.colors.mediumImportanceLink
|
||||
val otherColors = MaterialTheme.colors.subtleBorder
|
||||
val defaultBackground = MaterialTheme.colors.background
|
||||
val loggedInColors = MaterialTheme.colorScheme.mediumImportanceLink
|
||||
val otherColors = MaterialTheme.colorScheme.subtleBorder
|
||||
val defaultBackground = MaterialTheme.colorScheme.background
|
||||
|
||||
val backgroundBubbleColor = remember {
|
||||
if (accountViewModel.isLoggedUser(note.author)) {
|
||||
@@ -256,12 +258,7 @@ fun NormalChatNote(
|
||||
|
||||
if (routeForLastRead != null) {
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
launch(Dispatchers.IO) {
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
accountViewModel.account.markAsRead(routeForLastRead, createdAt)
|
||||
}
|
||||
}
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +282,7 @@ fun NormalChatNote(
|
||||
modifier = modif,
|
||||
horizontalArrangement = alignment
|
||||
) {
|
||||
val availableBubbleSize = remember { mutableStateOf(0) }
|
||||
val availableBubbleSize = remember { mutableIntStateOf(0) }
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val modif2 = remember {
|
||||
@@ -355,7 +352,7 @@ private fun RenderBubble(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val bubbleSize = remember { mutableStateOf(0) }
|
||||
val bubbleSize = remember { mutableIntStateOf(0) }
|
||||
|
||||
val bubbleModifier = remember {
|
||||
Modifier
|
||||
@@ -398,10 +395,19 @@ private fun MessageBubbleLines(
|
||||
bubbleSize: MutableState<Int>,
|
||||
availableBubbleSize: MutableState<Int>
|
||||
) {
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
if (drawAuthorInfo) {
|
||||
DrawAuthorInfo(
|
||||
baseNote,
|
||||
alignment,
|
||||
automaticallyShowProfilePicture,
|
||||
nav
|
||||
)
|
||||
} else {
|
||||
@@ -559,13 +565,13 @@ private fun StatusRow(
|
||||
|
||||
Column(modifier = ReactionRowHeightChat) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
|
||||
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
|
||||
LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav)
|
||||
ZapReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav)
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
ReplyReaction(
|
||||
baseNote = baseNote,
|
||||
grayTint = MaterialTheme.colors.placeholderText,
|
||||
grayTint = MaterialTheme.colorScheme.placeholderText,
|
||||
accountViewModel = accountViewModel,
|
||||
showCounter = false,
|
||||
iconSize = Size15dp
|
||||
@@ -586,7 +592,7 @@ fun IncognitoBadge(baseNote: Note) {
|
||||
modifier = Modifier
|
||||
.padding(top = 1.dp)
|
||||
.size(14.dp),
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
} else if (baseNote.event is PrivateDmEvent) {
|
||||
Icon(
|
||||
@@ -595,7 +601,7 @@ fun IncognitoBadge(baseNote: Note) {
|
||||
modifier = Modifier
|
||||
.padding(top = 1.dp)
|
||||
.size(14.dp),
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -612,7 +618,7 @@ fun ChatTimeAgo(baseNote: Note) {
|
||||
|
||||
Text(
|
||||
text = time,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontSize = Font12SP,
|
||||
maxLines = 1
|
||||
)
|
||||
@@ -626,8 +632,8 @@ private fun RenderRegularTextNote(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
|
||||
val modifier = remember { Modifier.padding(top = 5.dp) }
|
||||
|
||||
if (eventContent != null) {
|
||||
@@ -636,7 +642,7 @@ private fun RenderRegularTextNote(
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
content = eventContent!!,
|
||||
canPreview = canPreview,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
@@ -710,6 +716,7 @@ private fun RenderCreateChannelNote(note: Note) {
|
||||
private fun DrawAuthorInfo(
|
||||
baseNote: Note,
|
||||
alignment: Arrangement.Horizontal,
|
||||
loadProfilePicture: Boolean,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Row(
|
||||
@@ -717,26 +724,28 @@ private fun DrawAuthorInfo(
|
||||
horizontalArrangement = alignment,
|
||||
modifier = Modifier.padding(top = 5.dp)
|
||||
) {
|
||||
DisplayAndWatchNoteAuthor(baseNote, nav)
|
||||
DisplayAndWatchNoteAuthor(baseNote, loadProfilePicture, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayAndWatchNoteAuthor(
|
||||
baseNote: Note,
|
||||
loadProfilePicture: Boolean,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val author = remember {
|
||||
baseNote.author
|
||||
}
|
||||
author?.let {
|
||||
WatchAndDisplayUser(it, nav)
|
||||
WatchAndDisplayUser(it, loadProfilePicture, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchAndDisplayUser(
|
||||
author: User,
|
||||
loadProfilePicture: Boolean,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val pubkeyHex = remember { author.pubkeyHex }
|
||||
@@ -762,7 +771,7 @@ private fun WatchAndDisplayUser(
|
||||
}
|
||||
}
|
||||
|
||||
UserIcon(pubkeyHex, userProfilePicture, nav, route)
|
||||
UserIcon(pubkeyHex, userProfilePicture, loadProfilePicture, nav, route)
|
||||
|
||||
userDisplayName?.let {
|
||||
DisplayMessageUsername(it, userTags, route, nav)
|
||||
@@ -773,6 +782,7 @@ private fun WatchAndDisplayUser(
|
||||
private fun UserIcon(
|
||||
pubkeyHex: String,
|
||||
userProfilePicture: String?,
|
||||
loadProfilePicture: Boolean,
|
||||
nav: (String) -> Unit,
|
||||
route: String
|
||||
) {
|
||||
@@ -780,6 +790,7 @@ private fun UserIcon(
|
||||
robot = pubkeyHex,
|
||||
model = userProfilePicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(Size25dp)
|
||||
@@ -806,7 +817,7 @@ private fun DisplayMessageUsername(
|
||||
maxLines = 1,
|
||||
tags = userTags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
overrideColor = MaterialTheme.colors.onBackground, // we do not want clickable names in purple here.
|
||||
overrideColor = MaterialTheme.colorScheme.onBackground, // we do not want clickable names in purple here.
|
||||
route = route,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
@@ -20,6 +18,8 @@ import androidx.compose.material.icons.filled.VolumeOff
|
||||
import androidx.compose.material.icons.filled.VolumeUp
|
||||
import androidx.compose.material.icons.outlined.BarChart
|
||||
import androidx.compose.material.icons.outlined.PlayCircle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -61,7 +61,7 @@ fun ArrowBackIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = MaterialTheme.colors.grayText
|
||||
tint = MaterialTheme.colorScheme.grayText
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ fun MessageIcon(modifier: Modifier) {
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ fun DownloadForOfflineIcon(iconSize: Dp) {
|
||||
imageVector = Icons.Default.DownloadForOffline,
|
||||
null,
|
||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ fun ExpandLessIcon(modifier: Modifier) {
|
||||
imageVector = Icons.Default.ExpandLess,
|
||||
null,
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.colors.subtleButton
|
||||
tint = MaterialTheme.colorScheme.subtleButton
|
||||
)
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ fun ExpandMoreIcon(modifier: Modifier) {
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.colors.subtleButton
|
||||
tint = MaterialTheme.colorScheme.subtleButton
|
||||
)
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ fun PollIcon() {
|
||||
painter = painterResource(R.drawable.ic_poll),
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ fun RegularPostIcon() {
|
||||
painter = painterResource(R.drawable.ic_lists),
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ fun CancelIcon() {
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Size30Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
@@ -255,8 +255,7 @@ fun CloseIcon() {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close),
|
||||
contentDescription = stringResource(id = R.string.cancel),
|
||||
modifier = Size20Modifier,
|
||||
tint = Color.White
|
||||
modifier = Size20Modifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -265,7 +264,7 @@ fun MutedIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VolumeOff,
|
||||
contentDescription = stringResource(id = R.string.muted_button),
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Size30Modifier
|
||||
)
|
||||
}
|
||||
@@ -275,7 +274,7 @@ fun MuteIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VolumeUp,
|
||||
contentDescription = stringResource(id = R.string.mute_button),
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Size30Modifier
|
||||
)
|
||||
}
|
||||
@@ -354,7 +353,7 @@ fun VerticalDotsIcon() {
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
null,
|
||||
modifier = Size18Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -43,16 +42,12 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = messageSetCard) {
|
||||
launch(Dispatchers.IO) {
|
||||
val isNew = messageSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
accountViewModel.account.markAsRead(routeForLastRead, messageSetCard.createdAt())
|
||||
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, messageSetCard.createdAt()) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -20,9 +17,9 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -33,6 +30,7 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -45,8 +43,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
@@ -70,8 +70,9 @@ import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -90,16 +91,12 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = multiSetCard) {
|
||||
launch(Dispatchers.IO) {
|
||||
val isNew = multiSetCard.maxCreatedAt > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
accountViewModel.account.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
|
||||
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, multiSetCard.maxCreatedAt) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
@@ -172,7 +169,6 @@ private fun Galeries(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
|
||||
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
|
||||
val likeEvents by remember { derivedStateOf { multiSetCard.likeEventsByType } }
|
||||
|
||||
@@ -181,6 +177,16 @@ private fun Galeries(
|
||||
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
|
||||
|
||||
if (hasZapEvents) {
|
||||
var zapEvents by remember(multiSetCard.zapEvents) {
|
||||
mutableStateOf<ImmutableList<ZapAmountCommentNotification>>(persistentListOf())
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
accountViewModel.decryptAmountMessageInGroup(multiSetCard.zapEvents) {
|
||||
zapEvents = it
|
||||
}
|
||||
}
|
||||
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
@@ -250,7 +256,7 @@ fun RenderLikeGallery(
|
||||
|
||||
@Composable
|
||||
fun RenderZapGallery(
|
||||
zapEvents: ImmutableList<CombinedZap>,
|
||||
zapEvents: ImmutableList<ZapAmountCommentNotification>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
@@ -297,10 +303,29 @@ fun RenderBoostGallery(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MapZaps(
|
||||
zaps: ImmutableList<CombinedZap>,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (ImmutableList<ZapAmountCommentNotification>) -> Unit
|
||||
) {
|
||||
var zapEvents by remember(zaps) {
|
||||
mutableStateOf<ImmutableList<ZapAmountCommentNotification>>(persistentListOf())
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
accountViewModel.decryptAmountMessageInGroup(zaps) {
|
||||
zapEvents = it
|
||||
}
|
||||
}
|
||||
|
||||
content(zapEvents)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun AuthorGalleryZaps(
|
||||
authorNotes: ImmutableList<CombinedZap>,
|
||||
authorNotes: ImmutableList<ZapAmountCommentNotification>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
@@ -308,24 +333,12 @@ fun AuthorGalleryZaps(
|
||||
Column(modifier = StdStartPadding) {
|
||||
FlowRow() {
|
||||
authorNotes.forEach {
|
||||
ParseAuthorCommentAndAmount(it, backgroundColor, nav, accountViewModel)
|
||||
RenderState(it, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ParseAuthorCommentAndAmount(
|
||||
zap: CombinedZap,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
ParseAuthorCommentAndAmount(zap.request, zap.response, accountViewModel) { state ->
|
||||
RenderState(state, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class ZapAmountCommentNotification(
|
||||
val user: User?,
|
||||
@@ -361,15 +374,15 @@ private fun ParseAuthorCommentAndAmount(
|
||||
onReady(content)
|
||||
}
|
||||
|
||||
fun click(content: MutableState<ZapAmountCommentNotification>, nav: (String) -> Unit) {
|
||||
content.value.user?.let {
|
||||
fun click(content: ZapAmountCommentNotification, nav: (String) -> Unit) {
|
||||
content.user?.let {
|
||||
nav(routeFor(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderState(
|
||||
content: MutableState<ZapAmountCommentNotification>,
|
||||
content: ZapAmountCommentNotification,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -401,97 +414,65 @@ val commentTextSize = 12.sp
|
||||
|
||||
@Composable
|
||||
private fun DisplayAuthorCommentAndAmount(
|
||||
authorComment: MutableState<ZapAmountCommentNotification>,
|
||||
authorComment: ZapAmountCommentNotification,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.BottomCenter) {
|
||||
CrossfadeToDisplayPicture(authorComment, accountViewModel)
|
||||
CrossfadeToDisplayAmount(authorComment)
|
||||
}
|
||||
|
||||
CrossfadeToDisplayComment(authorComment, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayPicture(authorComment: MutableState<ZapAmountCommentNotification>, accountViewModel: AccountViewModel) {
|
||||
Crossfade(authorComment.value) {
|
||||
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(it.user, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
|
||||
val visible by remember(authorComment) {
|
||||
derivedStateOf {
|
||||
authorComment.value.amount != null
|
||||
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(authorComment.user, accountViewModel)
|
||||
authorComment.amount?.let {
|
||||
CrossfadeToDisplayAmount(it)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
authorComment.comment?.let {
|
||||
CrossfadeToDisplayComment(it, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayAmount(amount: String) {
|
||||
Box(
|
||||
modifier = amountBoxModifier,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
authorComment.value.amount?.let {
|
||||
Box(
|
||||
modifier = amountBoxModifier,
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
val backgroundColor = MaterialTheme.colors.overPictureBackground
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(Size35dp)
|
||||
.background(backgroundColor)
|
||||
},
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.bitcoinColor,
|
||||
fontSize = commentTextSize,
|
||||
modifier = bottomPadding1dp
|
||||
)
|
||||
}
|
||||
}
|
||||
val backgroundColor = MaterialTheme.colorScheme.overPictureBackground
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(Size35dp)
|
||||
.background(backgroundColor)
|
||||
},
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = amount,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.bitcoinColor,
|
||||
fontSize = commentTextSize,
|
||||
modifier = bottomPadding1dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayComment(
|
||||
authorComment: MutableState<ZapAmountCommentNotification>,
|
||||
comment: String,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val visible by remember(authorComment) {
|
||||
derivedStateOf {
|
||||
authorComment.value.comment != null
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
authorComment.value.comment?.let {
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
tags = remember { ImmutableListOfLists() },
|
||||
modifier = textBoxModifier,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
TranslatableRichTextViewer(
|
||||
content = comment,
|
||||
canPreview = true,
|
||||
tags = EmptyTagList,
|
||||
modifier = textBoxModifier,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@@ -542,26 +523,35 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
||||
author: User,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
WatchUserMetadata(author) { baseUserPicture ->
|
||||
Crossfade(targetState = baseUserPicture) { userPicture ->
|
||||
RobohashAsyncImageProxy(
|
||||
robot = author.pubkeyHex,
|
||||
model = userPicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = MaterialTheme.colors.profile35dpModifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
when (accountViewModel.account.settings.automaticallyShowProfilePictures) {
|
||||
ConnectivityType.WIFI_ONLY -> !ConnectivityStatus.isOnMobileData.value
|
||||
ConnectivityType.NEVER -> false
|
||||
ConnectivityType.ALWAYS -> true
|
||||
}
|
||||
}
|
||||
|
||||
WatchUserMetadata(author) { baseUserPicture ->
|
||||
// Crossfade(targetState = baseUserPicture) { userPicture ->
|
||||
RobohashAsyncImageProxy(
|
||||
robot = author.pubkeyHex,
|
||||
model = baseUserPicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = MaterialTheme.colorScheme.profile35dpModifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = automaticallyShowProfilePicture
|
||||
)
|
||||
// }
|
||||
}
|
||||
|
||||
WatchUserFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
|
||||
Crossfade(targetState = isFollowing) {
|
||||
if (it) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
|
||||
FollowingIcon(Size10dp)
|
||||
}
|
||||
// Crossfade(targetState = isFollowing) {
|
||||
if (isFollowing) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
|
||||
FollowingIcon(Size10dp)
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.OpenInNew
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -106,7 +106,7 @@ fun ObserveDisplayNip05Status(
|
||||
) {
|
||||
val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05())
|
||||
|
||||
LoadStatuses(baseUser) { statuses ->
|
||||
LoadStatuses(baseUser, accountViewModel) { statuses ->
|
||||
Crossfade(targetState = nip05, modifier = columnModifier, label = "ObserveDisplayNip05StatusCrossfade") {
|
||||
VerifyAndDisplayNIP05OrStatusLine(it, statuses, baseUser, columnModifier, accountViewModel, nav)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ fun DisplayUsersNpub(npub: String) {
|
||||
Text(
|
||||
text = npub,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -205,7 +205,7 @@ fun DisplayStatus(
|
||||
painter = painterResource(id = R.drawable.tunestr),
|
||||
null,
|
||||
modifier = Size15Modifier.padding(end = Size5dp),
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
tint = MaterialTheme.colorScheme.placeholderText
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ fun DisplayStatus(
|
||||
Text(
|
||||
text = content,
|
||||
fontSize = Font14SP,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -229,11 +229,11 @@ fun DisplayStatus(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.lessImportantLink
|
||||
tint = MaterialTheme.colorScheme.lessImportantLink
|
||||
)
|
||||
}
|
||||
} else if (nostrATag != null) {
|
||||
LoadAddressableNote(nostrATag) { note ->
|
||||
LoadAddressableNote(nostrATag, accountViewModel) { note ->
|
||||
if (note != null) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
IconButton(
|
||||
@@ -249,13 +249,13 @@ fun DisplayStatus(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.lessImportantLink
|
||||
tint = MaterialTheme.colorScheme.lessImportantLink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (nostrHexID != null) {
|
||||
LoadNote(baseNoteHex = nostrHexID) {
|
||||
LoadNote(baseNoteHex = nostrHexID, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
IconButton(
|
||||
@@ -271,7 +271,7 @@ fun DisplayStatus(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.lessImportantLink
|
||||
tint = MaterialTheme.colorScheme.lessImportantLink
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -298,7 +298,7 @@ private fun DisplayNIP05(
|
||||
Text(
|
||||
text = remember(nip05) { AnnotatedString(user) },
|
||||
fontSize = Font14SP,
|
||||
color = MaterialTheme.colors.nip05,
|
||||
color = MaterialTheme.colorScheme.nip05,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -309,7 +309,7 @@ private fun DisplayNIP05(
|
||||
ClickableText(
|
||||
text = remember(nip05) { AnnotatedString(domain) },
|
||||
onClick = { runCatching { uri.openUri("https://$domain") } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.nip05, fontSize = Font14SP),
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible
|
||||
)
|
||||
@@ -349,7 +349,7 @@ fun DisplayNip05ProfileStatus(user: User, accountViewModel: AccountViewModel) {
|
||||
if (user != "_") {
|
||||
Text(
|
||||
text = remember { AnnotatedString(user + "@") },
|
||||
color = MaterialTheme.colors.primary,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -360,7 +360,7 @@ fun DisplayNip05ProfileStatus(user: User, accountViewModel: AccountViewModel) {
|
||||
ClickableText(
|
||||
text = AnnotatedString(domain),
|
||||
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = domainPadStart),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user