From 63ab2a9fbe308976780e1105c50ecced1b4ea80b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 10:31:51 -0400 Subject: [PATCH 01/46] Moves NIP315 to NIP38 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4b82fcdd..c2ac6b03f 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [x] Gift Wraps & Seals (NIP-59) - [x] Versioned Encrypted Payloads (NIP-44) - [x] Expiration Support (NIP-40) -- [x] Status Event (NIP-315) +- [x] Status Event (NIP-38) - [ ] Marketplace (NIP-15) - [ ] Image/Video Capture in the app - [ ] Local Database From 53d5c5f354a444042e4d69f3f39876775fb91947 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 11:49:41 -0400 Subject: [PATCH 02/46] BugFix for serialization filter --- .../com/vitorpamplona/amethyst/service/relays/JsonFilter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt index 0fe851b6f..16a97086d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt @@ -60,7 +60,7 @@ class JsonFilter( } else { val jsonObjectSince = factory.objectNode() entries.forEach { sincePairs -> - put(sincePairs.key, "${sincePairs.value}") + jsonObjectSince.put(sincePairs.key, "${sincePairs.value}") } put("since", jsonObjectSince) } From dc74c3014f02f9d21b9504087cefebdc447a1cb2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 11:51:41 -0400 Subject: [PATCH 03/46] updates cached content when note changes --- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index c5e2971a5..a66f92d06 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1315,7 +1315,7 @@ fun RenderPoll( nav: (String) -> Unit ) { val noteEvent = note.event as? PollNoteEvent ?: return - val eventContent = remember { noteEvent.content() } + val eventContent = remember(note) { noteEvent.content() } if (makeItShort && accountViewModel.isLoggedUser(note.author)) { Text( From ed70658be98d9a33723f6e4f973d9ff00fb264b1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 11:52:44 -0400 Subject: [PATCH 04/46] Removes unecessary @ExperimentalTime tag --- .../amethyst/ui/dal/ChatroomListKnownFeedFilter.kt | 2 -- .../amethyst/ui/dal/ChatroomListNewFeedFilter.kt | 2 -- .../java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt | 3 --- .../vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt | 5 ----- 4 files changed, 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt index 2f7a22ddd..2964850eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt @@ -7,7 +7,6 @@ import com.vitorpamplona.amethyst.ui.actions.updated import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKeyable -import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter() { @@ -50,7 +49,6 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter, newItems: Set): List { val (feed, elapsed) = measureTimedValue { val me = account.userProfile() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt index 7b5b8b9b4..7356b3544 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt @@ -6,7 +6,6 @@ import com.vitorpamplona.amethyst.ui.actions.updated import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.PrivateDmEvent -import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter() { @@ -38,7 +37,6 @@ class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter .reversed() } - @OptIn(ExperimentalTime::class) override fun updateListWith(oldList: List, newItems: Set): List { val (feed, elapsed) = measureTimedValue { val me = account.userProfile() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index 86f8bd3aa..1b86baca3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -2,11 +2,9 @@ package com.vitorpamplona.amethyst.ui.dal import android.util.Log import com.vitorpamplona.amethyst.service.checkNotInMainThread -import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue abstract class FeedFilter { - @OptIn(ExperimentalTime::class) fun loadTop(): List { checkNotInMainThread() @@ -33,7 +31,6 @@ abstract class AdditiveFeedFilter : FeedFilter() { abstract fun applyFilter(collection: Set): Set abstract fun sort(collection: Set): List - @OptIn(ExperimentalTime::class) open fun updateListWith(oldList: List, newItems: Set): List { checkNotInMainThread() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 04d8a249c..4d521730a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -34,7 +34,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue @Stable @@ -270,7 +269,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { private val bundler = BundledUpdate(1000, Dispatchers.IO) private val bundlerInsert = BundledInsert>(1000, Dispatchers.IO) - @OptIn(ExperimentalTime::class) fun invalidateData(ignoreIfDoing: Boolean = false) { bundler.invalidate(ignoreIfDoing) { // adds the time to perform the refresh into this delay @@ -282,7 +280,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { } } - @OptIn(ExperimentalTime::class) fun invalidateDataAndSendToTop() { clear() bundler.invalidate(false) { @@ -296,7 +293,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { } } - @OptIn(ExperimentalTime::class) fun checkKeysInvalidateDataAndSendToTop() { if (lastFeedKey != localFilter.feedKey()) { clear() @@ -312,7 +308,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { } } - @OptIn(ExperimentalTime::class) fun invalidateInsertData(newItems: Set) { bundlerInsert.invalidateList(newItems) { val newObjects = it.flatten().toSet() From 017df9845df5006cc8dfb14efd732f787ba17a7d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 12:59:21 -0400 Subject: [PATCH 05/46] Fixes `r` filter in tags --- quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 7026382df..6cbaff6ea 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -72,7 +72,7 @@ open class Event( override fun firstTaggedUser() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] } override fun firstTaggedEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] } override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] } - override fun firstTaggedAddress() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { + override fun firstTaggedAddress() = tags.firstOrNull { it.size > 1 && it[0] == "a" }?.let { val aTagValue = it[1] val relay = it.getOrNull(2) From 5328740bff7a3d26d5d45127eb51055954628d73 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 13:01:08 -0400 Subject: [PATCH 06/46] Improves blinking when loading a thread. --- .../amethyst/service/NostrThreadDataSource.kt | 6 ++++-- .../amethyst/ui/screen/loggedIn/ThreadScreen.kt | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt index ec7ade398..2032f551e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt @@ -36,8 +36,10 @@ object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") { } fun loadThread(noteId: String?) { - eventToWatch = noteId + if (eventToWatch != noteId) { + eventToWatch = noteId - invalidateFilters() + invalidateFilters() + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt index 06ea14cec..a7107e819 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt @@ -26,9 +26,10 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (Stri factory = NostrThreadFeedViewModel.Factory(noteId) ) + NostrThreadDataSource.loadThread(noteId) + LaunchedEffect(noteId) { - NostrThreadDataSource.loadThread(noteId) - feedViewModel.invalidateData() + feedViewModel.invalidateData(true) } DisposableEffect(accountViewModel) { @@ -37,7 +38,7 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (Stri println("Thread Start") NostrThreadDataSource.loadThread(noteId) NostrThreadDataSource.start() - feedViewModel.invalidateData() + feedViewModel.invalidateData(true) } if (event == Lifecycle.Event.ON_PAUSE) { println("Thread Stop") From e2c011ec60f762b87482f7490771806187cfe8d1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 13:01:49 -0400 Subject: [PATCH 07/46] Avoids lists with the same hexId because the app was pruning events while loading them --- .../java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index fd1d6aafa..d4cb36cf5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -236,7 +236,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel(), I checkNotInMainThread() lastFeedKey = localFilter.feedKey() - val notes = localFilter.loadTop().toImmutableList() + val notes = localFilter.loadTop().distinctBy { it.idHex }.toImmutableList() val oldNotesState = _feedContent.value if (oldNotesState is FeedState.Loaded) { From e9ff9d3b37b914a01bba769fce1f939bf8273e91 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 13:24:47 -0400 Subject: [PATCH 08/46] Adds a space between the status and the clickable icon --- .../amethyst/ui/note/NIP05VerificationDisplay.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 2acdcfaff..3338cabe5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Column 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 @@ -46,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.ui.theme.Size18Modifier +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -249,6 +251,7 @@ fun DisplayStatus( if (url != null) { val uri = LocalUriHandler.current + Spacer(modifier = StdHorzSpacer) IconButton( modifier = Size15Modifier, onClick = { runCatching { uri.openUri(url.trim()) } } @@ -263,6 +266,7 @@ fun DisplayStatus( } else if (nostrATag != null) { LoadAddressableNote(nostrATag) { note -> if (note != null) { + Spacer(modifier = StdHorzSpacer) IconButton( modifier = Size15Modifier, onClick = { @@ -284,6 +288,7 @@ fun DisplayStatus( } else if (nostrHexID != null) { LoadNote(baseNoteHex = nostrHexID) { if (it != null) { + Spacer(modifier = StdHorzSpacer) IconButton( modifier = Size15Modifier, onClick = { From 53eeea32255e0882aa0d0c18ab29cee2189a0f52 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 13:26:56 -0400 Subject: [PATCH 09/46] Removes unused time --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 294676ca6..4e88c8726 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -14,7 +14,6 @@ import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.* -import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList @@ -1307,8 +1306,6 @@ object LocalCache { fun pruneExpiredEvents() { checkNotInMainThread() - val now = TimeUtils.now() - val toBeRemoved = notes.filter { it.value.event?.isExpired() == true }.values From 86c0ca975b3355594e235238875d076d2386d5bb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 13:27:57 -0400 Subject: [PATCH 10/46] Moves the User DataSource invalidation to the right channel --- .../amethyst/service/NostrSingleUserDataSource.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index 5dfa266f6..cb835c6cf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -66,14 +66,14 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") { eose.time = time } } + } + val userChannelOnce = requestNewChannel() { time, relayUrl -> // Many relays operate with limits in the amount of filters. // As information comes, the filters will be rotated to get more data. invalidateFilters() } - val userChannelOnce = requestNewChannel() - override fun updateChannelFilters() { userChannel.typedFilters = listOfNotNull(createUserReportFilter(), createUserStatusFilter()).flatten().ifEmpty { null } userChannelOnce.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null } From 469e1241d5b1398d218db7aa48c4388b6da32125 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 13:28:17 -0400 Subject: [PATCH 11/46] Updates some dependencies --- app/build.gradle | 2 +- build.gradle | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 67fb47dbe..e4ceff795 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -171,7 +171,7 @@ dependencies { playImplementation 'com.google.mlkit:translate:17.0.1' // PushNotifications - playImplementation platform('com.google.firebase:firebase-bom:32.2.2') + playImplementation platform('com.google.firebase:firebase-bom:32.2.3') playImplementation 'com.google.firebase:firebase-messaging-ktx' // Charts diff --git a/build.gradle b/build.gradle index f00613616..7e7e64f1c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,13 +5,13 @@ buildscript { fragment_version = "1.6.1" lifecycle_version = '2.6.1' compose_ui_version = '1.5.0' - nav_version = "2.7.0" + nav_version = "2.7.1" room_version = "2.4.3" accompanist_version = '0.30.1' coil_version = '2.4.0' - vico_version = '1.9.2' + vico_version = '1.11.0' exoplayer_version = '1.1.1' - media3_version = '1.1.0' + media3_version = '1.1.1' core_ktx_version = '1.10.1' } dependencies { From 3d34e1fc4e6ca9b8542b0624de7c78a760cf877e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 13:41:17 -0400 Subject: [PATCH 12/46] Reduces the icon to the music and adds space before the status --- .../amethyst/ui/note/NIP05VerificationDisplay.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 3338cabe5..e18db8176 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -46,7 +46,7 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size16Modifier -import com.vitorpamplona.amethyst.ui.theme.Size18Modifier +import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.nip05 @@ -235,7 +235,7 @@ fun DisplayStatus( "music" -> Icon( painter = painterResource(id = R.drawable.tunestr), null, - modifier = Size18Modifier, + modifier = Size15Modifier.padding(end = Size5dp), tint = MaterialTheme.colors.placeholderText ) else -> {} From 6b395629250199669e42db0864180956cbdb07e4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 13:55:41 -0400 Subject: [PATCH 13/46] Allows the keyboard action to be used as commit in the status field. --- .../amethyst/ui/navigation/DrawerContent.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index f180aaef9..beb6e17fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState 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 @@ -56,6 +57,7 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -278,8 +280,17 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo ) }, keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Send, capitalization = KeyboardCapitalization.Sentences ), + keyboardActions = KeyboardActions( + onSend = { + scope.launch(Dispatchers.IO) { + accountViewModel.createStatus(currentStatus.value) + focusManager.clearFocus(true) + } + } + ), singleLine = true, trailingIcon = { if (hasChanged) { @@ -319,8 +330,17 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo ) }, keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Send, capitalization = KeyboardCapitalization.Sentences ), + keyboardActions = KeyboardActions( + onSend = { + scope.launch(Dispatchers.IO) { + accountViewModel.updateStatus(it, thisStatus.value) + focusManager.clearFocus(true) + } + } + ), singleLine = true, trailingIcon = { if (hasChanged) { From 1bb2459277f010c5bd789bfa2ddd7e13c3192ba8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 15:08:10 -0400 Subject: [PATCH 14/46] Activates min/max values for polls --- .../ui/actions/NewPollVoteValueRange.kt | 80 +++++++------------ .../amethyst/ui/actions/NewPostView.kt | 2 + .../amethyst/ui/actions/NewPostViewModel.kt | 50 +++++++++++- app/src/main/res/values/strings.xml | 2 + 4 files changed, 80 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt index f585f6e0a..f99259dc0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt @@ -1,9 +1,11 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.foundation.layout.Arrangement +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.width +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField @@ -11,8 +13,6 @@ import androidx.compose.material.Text import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -21,51 +21,11 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText @Composable fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) { - var textMax by rememberSaveable { mutableStateOf("") } - var textMin by rememberSaveable { mutableStateOf("") } - - // check for zapMax amounts < 1 - pollViewModel.isValidvalueMaximum.value = true - if (textMax.isNotEmpty()) { - try { - val int = textMax.toInt() - if (int < 1) { - pollViewModel.isValidvalueMaximum.value = false - } else { pollViewModel.valueMaximum = int } - } catch (e: Exception) { pollViewModel.isValidvalueMaximum.value = false } - } - - // check for minZap amounts < 1 - pollViewModel.isValidvalueMinimum.value = true - if (textMin.isNotEmpty()) { - try { - val int = textMin.toInt() - if (int < 1) { - pollViewModel.isValidvalueMinimum.value = false - } else { pollViewModel.valueMinimum = int } - } catch (e: Exception) { pollViewModel.isValidvalueMinimum.value = false } - } - - // check for zapMin > zapMax - if (textMin.isNotEmpty() && textMax.isNotEmpty()) { - try { - val intMin = textMin.toInt() - val intMax = textMax.toInt() - - if (intMin > intMax) { - pollViewModel.isValidvalueMinimum.value = false - pollViewModel.isValidvalueMaximum.value = false - } - } catch (e: Exception) { - pollViewModel.isValidvalueMinimum.value = false - pollViewModel.isValidvalueMaximum.value = false - } - } - val colorInValid = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colors.error, unfocusedBorderColor = Color.Red @@ -80,10 +40,10 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) { horizontalArrangement = Arrangement.Center ) { OutlinedTextField( - value = textMin, - onValueChange = { textMin = it }, + value = pollViewModel.valueMinimum?.toString() ?: "", + onValueChange = { pollViewModel.updateMinZapAmountForPoll(it) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.width(150.dp), + modifier = Modifier.weight(1f), colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid, label = { Text( @@ -98,11 +58,14 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) { ) } ) + + Spacer(modifier = DoubleHorzSpacer) + OutlinedTextField( - value = textMax, - onValueChange = { textMax = it }, + value = pollViewModel.valueMaximum?.toString() ?: "", + onValueChange = { pollViewModel.updateMaxZapAmountForPoll(it) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.width(150.dp), + modifier = Modifier.weight(1f), colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid, label = { Text( @@ -118,10 +81,25 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) { } ) } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.poll_zap_value_min_max_explainer), + color = MaterialTheme.colors.placeholderText, + modifier = Modifier.padding(vertical = 10.dp) + ) + } } @Preview @Composable fun NewPollVoteValueRangePreview() { - NewPollVoteValueRange(NewPostViewModel()) + Column( + modifier = Modifier.fillMaxWidth() + ) { + NewPollVoteValueRange(NewPostViewModel()) + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index e0b21fa77..fc43a4293 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -518,6 +518,8 @@ private fun PollField(postViewModel: NewPostViewModel) { NewPollOption(postViewModel, index) } + NewPollVoteValueRange(postViewModel) + Button( onClick = { postViewModel.pollOptions[postViewModel.pollOptions.size] = diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 340d1134e..1bcbdecaf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -73,8 +73,8 @@ open class NewPostViewModel() : ViewModel() { var wantsPoll by mutableStateOf(false) var zapRecipients = mutableStateListOf() var pollOptions = newStateMapPollOptions() - var valueMaximum: Int? = null - var valueMinimum: Int? = null + var valueMaximum by mutableStateOf(null) + var valueMinimum by mutableStateOf(null) var consensusThreshold: Int? = null var closedAt: Int? = null @@ -494,7 +494,7 @@ open class NewPostViewModel() : ViewModel() { return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount != null) && (!wantsDirectMessage || !toUsers.text.isNullOrBlank()) && - (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) && + (!wantsPoll || (pollOptions.values.all { it.isNotEmpty() } && isValidvalueMinimum.value && isValidvalueMaximum.value)) && contentToAddUrl == null } @@ -612,6 +612,50 @@ open class NewPostViewModel() : ViewModel() { nip24 = !nip24 } } + + fun updateMinZapAmountForPoll(textMin: String) { + if (textMin.isNotEmpty()) { + try { + val int = textMin.toInt() + if (int < 1) { + valueMinimum = null + } else { + valueMinimum = int + } + } catch (e: Exception) {} + } else { + valueMinimum = null + } + + checkMinMax() + } + + fun updateMaxZapAmountForPoll(textMax: String) { + if (textMax.isNotEmpty()) { + try { + val int = textMax.toInt() + if (int < 1) { + valueMaximum = null + } else { + valueMaximum = int + } + } catch (e: Exception) {} + } else { + valueMaximum = null + } + + checkMinMax() + } + + fun checkMinMax() { + if ((valueMinimum ?: 0) > (valueMaximum ?: Int.MAX_VALUE)) { + isValidvalueMinimum.value = false + isValidvalueMaximum.value = false + } else { + isValidvalueMinimum.value = true + isValidvalueMaximum.value = true + } + } } enum class GeohashPrecision(val digits: Int) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 992ec266c..7bb0ea5dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -551,4 +551,6 @@ Rules Update your status + + Votes are weighted by the zap amount. You can set a minimum amount to avoid spammers and a maximum amount to avoid a large zappers taking over the poll. Use the same amount in both fields to make sure every vote is valued the same amount. Leave it empty to accept any amount. From a7d8c87c473c01c8b1d24d22de51fd4549e5bc19 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 15:18:01 -0400 Subject: [PATCH 15/46] 1. Triggers a Toast when no wallet is found to pay the invoice 2. Starts the wallet in a separate task to allow people to come back to Amethyst switching apps instead of the back button. --- .../ui/components/ClickableWithdrawal.kt | 20 +++++++++++++------ .../amethyst/ui/components/InvoicePreview.kt | 13 +++++++++++- .../ui/screen/loggedIn/AccountViewModel.kt | 5 ++++- .../ui/screen/loggedIn/ProfileScreen.kt | 11 +++++++++- app/src/main/res/values/strings.xml | 1 + 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt index 3a10db49e..5a5d6db32 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt @@ -2,6 +2,7 @@ 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 @@ -12,6 +13,7 @@ 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.quartz.encoders.LnWithdrawalUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -41,10 +43,7 @@ fun MayBeWithdrawal(lnurlWord: String) { @Composable fun ClickableWithdrawal(withdrawalString: String) { val context = LocalContext.current - - val uri = remember(withdrawalString) { - Uri.parse("lightning:$withdrawalString") - } + val scope = rememberCoroutineScope() val withdraw = remember(withdrawalString) { AnnotatedString("$withdrawalString ") @@ -53,9 +52,18 @@ fun ClickableWithdrawal(withdrawalString: String) { ClickableText( text = withdraw, onClick = { - runCatching { - val intent = Intent(Intent.ACTION_VIEW, uri) + 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() + } } }, style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt index 87d6179df..52c0f2c50 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt @@ -2,6 +2,7 @@ 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 @@ -66,6 +67,7 @@ fun MayBeInvoicePreview(lnbcWord: String) { @Composable fun InvoicePreview(lnInvoice: String, amount: String?) { val context = LocalContext.current + val scope = rememberCoroutineScope() Column( modifier = Modifier @@ -118,9 +120,18 @@ fun InvoicePreview(lnInvoice: String, amount: String?) { .fillMaxWidth() .padding(vertical = 10.dp), onClick = { - runCatching { + 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() + } } }, shape = QuoteBorder, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 35213f3dc..bc6eaab68 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -186,9 +186,12 @@ class AccountViewModel(val account: Account) : ViewModel() { onProgress(0f) } } else { - runCatching { + try { val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK ContextCompat.startActivity(context, intent, null) + } catch (e: Exception) { + onError(context.getString(R.string.lightning_wallets_not_found)) } onProgress(0f) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 1985b72b6..0654d3be9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1050,9 +1050,18 @@ fun DisplayLNAddress( } } } else { - runCatching { + try { val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) + 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() + } } } }, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7bb0ea5dc..e1ecc33a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -552,5 +552,6 @@ Update your status + Error parsing error message Votes are weighted by the zap amount. You can set a minimum amount to avoid spammers and a maximum amount to avoid a large zappers taking over the poll. Use the same amount in both fields to make sure every vote is valued the same amount. Leave it empty to accept any amount. From 7ef46a26621351adf356aa4844eec81d1ee3a7ed Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 17:14:33 -0400 Subject: [PATCH 16/46] Continues to move away from starting co-routines directly in composables --- .../com/vitorpamplona/amethyst/model/Note.kt | 8 +- .../amethyst/service/NostrDataSource.kt | 12 ++- .../amethyst/ui/components/UrlPreview.kt | 8 +- .../amethyst/ui/navigation/DrawerContent.kt | 31 ++---- .../amethyst/ui/note/MultiSetCompose.kt | 35 +----- .../amethyst/ui/note/PollNote.kt | 4 +- .../amethyst/ui/note/PollNoteViewModel.kt | 21 ++-- .../amethyst/ui/note/ReactionsRow.kt | 85 ++++----------- .../amethyst/ui/screen/CardFeedView.kt | 9 +- .../amethyst/ui/screen/FeedViewModel.kt | 12 ++- .../ui/screen/loggedIn/AccountViewModel.kt | 101 +++++++++++++++++- .../ui/screen/loggedIn/ChannelScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 18 ++-- .../ui/screen/loggedIn/LoadRedirectScreen.kt | 54 +++++----- .../quartz/events/StatusEvent.kt | 13 +++ 15 files changed, 213 insertions(+), 200 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 3a4ce8925..78b690678 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -463,10 +463,6 @@ open class Note(val idHex: String) { }.flatten() } - fun countReactions(): Int { - return reactions.values.sumOf { it.size } - } - fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal { // Regular Zap Receipts val completedZaps = zaps.asSequence() @@ -683,6 +679,10 @@ class NoteLiveSet(u: Note) { it.note.replies.size }.distinctUntilChanged() + val reactionCount = reactions.map { + it.note.reactions.values.sumOf { it.size } + }.distinctUntilChanged() + val boostCount = boosts.map { it.note.boosts.size }.distinctUntilChanged() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 54e4cac10..7bf7084f1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -111,12 +111,14 @@ abstract class NostrDataSource(val debugName: String) { private val bundler = BundledUpdate(300, Dispatchers.IO) fun invalidateFilters() { - bundler.invalidate() { - // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") + scope.launch(Dispatchers.IO) { + bundler.invalidate() { + // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") - // adds the time to perform the refresh into this delay - // holding off new updates in case of heavy refresh routines. - resetFiltersSuspend() + // adds the time to perform the refresh into this delay + // holding off new updates in case of heavy refresh routines. + resetFiltersSuspend() + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt index 8f88efade..b17da032e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt @@ -12,8 +12,6 @@ import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) { @@ -37,10 +35,8 @@ fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) // Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created). if (urlPreviewState == UrlPreviewState.Loading) { LaunchedEffect(url) { - launch(Dispatchers.IO) { - UrlCachedPreviewer.previewInfo(url) { - urlPreviewState = it - } + accountViewModel.urlPreview(url) { + urlPreviewState = it } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index beb6e17fb..a038e28ce 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -285,20 +285,16 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo ), keyboardActions = KeyboardActions( onSend = { - scope.launch(Dispatchers.IO) { - accountViewModel.createStatus(currentStatus.value) - focusManager.clearFocus(true) - } + accountViewModel.createStatus(currentStatus.value) + focusManager.clearFocus(true) } ), singleLine = true, trailingIcon = { if (hasChanged) { UserStatusSendButton() { - scope.launch(Dispatchers.IO) { - accountViewModel.createStatus(currentStatus.value) - focusManager.clearFocus(true) - } + accountViewModel.createStatus(currentStatus.value) + focusManager.clearFocus(true) } } } @@ -335,28 +331,21 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo ), keyboardActions = KeyboardActions( onSend = { - scope.launch(Dispatchers.IO) { - accountViewModel.updateStatus(it, thisStatus.value) - focusManager.clearFocus(true) - } + accountViewModel.updateStatus(it, thisStatus.value) + focusManager.clearFocus(true) } ), singleLine = true, trailingIcon = { if (hasChanged) { UserStatusSendButton() { - scope.launch(Dispatchers.IO) { - accountViewModel.updateStatus(it, thisStatus.value) - focusManager.clearFocus(true) - } + accountViewModel.updateStatus(it, thisStatus.value) + focusManager.clearFocus(true) } } else { UserStatusDeleteButton() { - scope.launch(Dispatchers.IO) { - accountViewModel.updateStatus(it, "") - accountViewModel.delete(it) - focusManager.clearFocus(true) - } + accountViewModel.deleteStatus(it) + focusManager.clearFocus(true) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 618e052bf..b5164442b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -42,7 +42,6 @@ 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.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ImageUrlType @@ -52,7 +51,6 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.CombinedZap import com.vitorpamplona.amethyst.ui.screen.MultiSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller @@ -70,8 +68,6 @@ 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.LnZapEvent -import com.vitorpamplona.quartz.events.LnZapRequestEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -352,8 +348,7 @@ private fun ParseAuthorCommentAndAmount( } LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) { - launch(Dispatchers.IO) { - val newState = loadAmountState(zapRequest, zapEvent, accountViewModel) + accountViewModel.decryptAmountMessage(zapRequest, zapEvent) { newState -> if (newState != null) { content.value = newState } @@ -363,34 +358,6 @@ private fun ParseAuthorCommentAndAmount( onReady(content) } -private suspend fun loadAmountState( - zapRequest: Note, - zapEvent: Note?, - accountViewModel: AccountViewModel -): ZapAmountCommentNotification? { - (zapRequest.event as? LnZapRequestEvent)?.let { - val decryptedContent = accountViewModel.decryptZap(zapRequest) - val amount = (zapEvent?.event as? LnZapEvent)?.amount - if (decryptedContent != null) { - val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey) - return ZapAmountCommentNotification( - newAuthor, - decryptedContent.content.ifBlank { null }, - showAmountAxis(amount) - ) - } else { - if (!zapRequest.event?.content().isNullOrBlank() || amount != null) { - return ZapAmountCommentNotification( - zapRequest.author, - zapRequest.event?.content()?.ifBlank { null }, - showAmountAxis(amount) - ) - } - } - } - return null -} - @Composable private fun RenderState( content: MutableState, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 1ae831a26..c78a3bd83 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -98,9 +98,7 @@ private fun WatchZapsAndUpdateTallies( val zapsState by baseNote.live().zaps.observeAsState() LaunchedEffect(key1 = zapsState) { - launch(Dispatchers.Default) { - pollViewModel.refreshTallies() - } + pollViewModel.refreshTallies() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index e9675f200..e5cd1c436 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -3,13 +3,16 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.events.* import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import java.math.BigDecimal import java.math.RoundingMode @@ -52,12 +55,12 @@ class PollNoteViewModel : ViewModel() { closedAt = pollEvent?.getTagInt(CLOSED_AT) } - suspend fun refreshTallies() { - totalZapped = totalZapped() - wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false + fun refreshTallies() { + viewModelScope.launch(Dispatchers.Default) { + totalZapped = totalZapped() + wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false - _tallies.emit( - pollOptions?.keys?.map { + val newOptions = pollOptions?.keys?.map { val zappedInOption = zappedPollOptionAmount(it) val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { @@ -71,8 +74,12 @@ class PollNoteViewModel : ViewModel() { val consensus = consensusThreshold != null && myTally >= consensusThreshold!! PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn) - } ?: emptyList() - ) + } + + _tallies.emit( + newOptions ?: emptyList() + ) + } } fun canZap(): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 8a2426021..ff7d3bc46 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -40,6 +40,7 @@ import androidx.compose.material.ProgressIndicatorDefaults import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf @@ -241,7 +242,7 @@ private fun LoadAndDisplayZapraiser( wantsToSeeReactions: MutableState, accountViewModel: AccountViewModel ) { - val zapraiserAmount by remember { + val zapraiserAmount by remember(baseNote) { derivedStateOf { baseNote.event?.zapraiserAmount() ?: 0 } @@ -261,42 +262,26 @@ private fun LoadAndDisplayZapraiser( } } +@Immutable +data class ZapraiserStatus(val progress: Float, val left: String) + @Composable fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, accountViewModel: AccountViewModel) { val zapsState by baseNote.live().zaps.observeAsState() - var zapraiserProgress by remember { mutableStateOf(0F) } - var zapraiserLeft by remember { mutableStateOf("$zapraiserAmount") } + var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) } LaunchedEffect(key1 = zapsState) { - launch(Dispatchers.Default) { - zapsState?.note?.let { - val newZapAmount = accountViewModel.calculateZapAmount(it) - var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat() - - if (percentage > 1) { - percentage = 1f - } - - if (Math.abs(zapraiserProgress - percentage) > 0.001) { - val newZapraiserProgress = percentage - val newZapraiserLeft = if (percentage > 0.99) { - "0" - } else { - showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal()) - } - if (zapraiserLeft != newZapraiserLeft) { - zapraiserLeft = newZapraiserLeft - } - if (zapraiserProgress != newZapraiserProgress) { - zapraiserProgress = newZapraiserProgress - } + zapsState?.note?.let { + accountViewModel.calculateZapraiser(baseNote) { newStatus -> + if (zapraiserStatus != newStatus) { + zapraiserStatus = newStatus } } } } - val color = if (zapraiserProgress > 0.99) { + val color = if (zapraiserStatus.progress > 0.99) { DarkerGreen } else { MaterialTheme.colors.mediumImportanceLink @@ -307,7 +292,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc .fillMaxWidth() .height(if (details) 24.dp else 4.dp), color = color, - progress = zapraiserProgress + progress = zapraiserStatus.progress ) if (details) { @@ -315,14 +300,14 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc contentAlignment = Center, modifier = TinyBorders ) { - val totalPercentage by remember(zapraiserProgress) { + val totalPercentage by remember(zapraiserStatus) { derivedStateOf { - "${(zapraiserProgress * 100).roundToInt()}%" + "${(zapraiserStatus.progress * 100).roundToInt()}%" } } Text( - text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserLeft), + text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserStatus.left), modifier = NoSoTinyBorders, color = MaterialTheme.colors.placeholderText, fontSize = Font14SP, @@ -836,10 +821,7 @@ private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountVi val reactionsState by baseNote.live().reactions.observeAsState() LaunchedEffect(key1 = reactionsState) { - launch(Dispatchers.Default) { - val reactionNote = reactionsState?.note?.getReactionBy(accountViewModel.userProfile()) - onNewReactionType(reactionNote) - } + accountViewModel.loadReactionTo(reactionsState?.note, onNewReactionType) } } @@ -874,32 +856,9 @@ private fun RenderReactionType( @Composable fun LikeText(baseNote: Note, grayTint: Color) { - val reactionsCount = remember(baseNote) { - mutableStateOf(baseNote.reactions.size) - } + val reactionCount by baseNote.live().reactionCount.observeAsState(0) - val scope = rememberCoroutineScope() - - WatchReactionCountForNote(baseNote) { newReactionsCount -> - if (reactionsCount.value != newReactionsCount) { - scope.launch(Dispatchers.Main) { - reactionsCount.value = newReactionsCount - } - } - } - - SlidingAnimationCount(reactionsCount, grayTint) -} - -@Composable -private fun WatchReactionCountForNote(baseNote: Note, onNewReactionCount: (Int) -> Unit) { - val reactionsState by baseNote.live().reactions.observeAsState() - - LaunchedEffect(key1 = reactionsState) { - launch(Dispatchers.Default) { - onNewReactionCount(reactionsState?.note?.countReactions() ?: 0) - } - } + SlidingAnimationCount(reactionCount, grayTint) } private fun likeClick( @@ -1139,9 +1098,7 @@ private fun WatchZapsForNote(baseNote: Note, accountViewModel: AccountViewModel, val zapsState by baseNote.live().zaps.observeAsState() LaunchedEffect(key1 = zapsState) { - launch(Dispatchers.Default) { - onWasZapped(accountViewModel.calculateIfNoteWasZappedByAccount(baseNote)) - } + accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, onWasZapped) } } @@ -1171,9 +1128,7 @@ fun WatchZapAmountsForNote(baseNote: Note, accountViewModel: AccountViewModel, o val zapsState by baseNote.live().zaps.observeAsState() LaunchedEffect(key1 = zapsState) { - launch(Dispatchers.Default) { - onZapAmount(showAmount(accountViewModel.calculateZapAmount(baseNote))) - } + accountViewModel.calculateZapAmount(baseNote, onZapAmount) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index c7087e1a7..e35a1fcb0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.ui.note.MultiSetCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable @@ -102,11 +101,9 @@ private fun WatchScrollToTop( val scrollToTop by viewModel.scrollToTop.collectAsState() LaunchedEffect(scrollToTop) { - launch { - if (scrollToTop > 0 && viewModel.scrolltoTopPending) { - listState.scrollToItem(index = 0) - viewModel.sentToTop() - } + if (scrollToTop > 0 && viewModel.scrolltoTopPending) { + listState.scrollToItem(index = 0) + viewModel.sentToTop() } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index d4cb36cf5..7561b818d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -301,11 +301,13 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel(), I fun checkKeysInvalidateDataAndSendToTop() { if (lastFeedKey != localFilter.feedKey()) { - bundler.invalidate(false) { - // adds the time to perform the refresh into this delay - // holding off new updates in case of heavy refresh routines. - refreshSuspended() - sendToTop() + viewModelScope.launch(Dispatchers.IO) { + bundler.invalidate(false) { + // adds the time to perform the refresh into this delay + // holding off new updates in case of heavy refresh routines. + refreshSuspended() + sendToTop() + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index bc6eaab68..eabb999d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -16,14 +16,22 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountState import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.ConnectivityType +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver +import com.vitorpamplona.amethyst.ui.components.UrlPreviewState +import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification +import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus +import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.LnZapEvent +import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.SealedGossipEvent @@ -121,21 +129,86 @@ class AccountViewModel(val account: Account) : ViewModel() { account.delete(account.boostsTo(note)) } - fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean { - return account.calculateIfNoteWasZappedByAccount(zappedNote) + fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) { + viewModelScope.launch(Dispatchers.Default) { + onWasZapped(account.calculateIfNoteWasZappedByAccount(zappedNote)) + } } - fun calculateZapAmount(zappedNote: Note): BigDecimal { + suspend fun calculateZapAmount(zappedNote: Note): BigDecimal { return account.calculateZappedAmount(zappedNote) } + fun calculateZapAmount(zappedNote: Note, onZapAmount: (String) -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + onZapAmount(showAmount(account.calculateZappedAmount(zappedNote))) + } + } + + fun calculateZapraiser(zappedNote: Note, onZapraiserStatus: (ZapraiserStatus) -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0 + val newZapAmount = calculateZapAmount(zappedNote) + var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat() + + if (percentage > 1) { + percentage = 1f + } + + val newZapraiserProgress = percentage + val newZapraiserLeft = if (percentage > 0.99) { + "0" + } else { + showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal()) + } + onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft)) + } + } + + fun decryptAmountMessage( + zapRequest: Note, + zapEvent: Note?, + onNewState: (ZapAmountCommentNotification?) -> Unit + ) { + viewModelScope.launch(Dispatchers.IO) { + onNewState(innerDecryptAmountMessage(zapRequest, zapEvent)) + } + } + + private suspend fun innerDecryptAmountMessage( + zapRequest: Note, + zapEvent: Note? + ): ZapAmountCommentNotification? { + (zapRequest.event as? LnZapRequestEvent)?.let { + val decryptedContent = decryptZap(zapRequest) + val amount = (zapEvent?.event as? LnZapEvent)?.amount + if (decryptedContent != null) { + val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey) + return ZapAmountCommentNotification( + newAuthor, + decryptedContent.content.ifBlank { null }, + showAmountAxis(amount) + ) + } else { + if (!zapRequest.event?.content().isNullOrBlank() || amount != null) { + return ZapAmountCommentNotification( + zapRequest.author, + zapRequest.event?.content()?.ifBlank { null }, + showAmountAxis(amount) + ) + } + } + } + return null + } + fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) { viewModelScope.launch(Dispatchers.IO) { innerZap(note, amount, pollOption, message, context, onError, onProgress, zapType) } } - suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) { + private suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) { val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() if (lud16.isNullOrBlank()) { @@ -388,6 +461,26 @@ class AccountViewModel(val account: Account) : ViewModel() { fun updateStatus(it: AddressableNote, newStatus: String) { account.updateStatus(it, newStatus) + + fun checkIfOnline(url: String, onResult: (Boolean) -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + val isOnline = OnlineChecker.isOnline(url) + onResult(isOnline) + } + } + + fun urlPreview(url: String, onResult: suspend (UrlPreviewState) -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + UrlCachedPreviewer.previewInfo(url, onResult) + } + } + + fun loadReactionTo(note: Note?, onNewReactionType: (String?) -> Unit) { + if (note == null) return + + viewModelScope.launch(Dispatchers.Default) { + onNewReactionType(note.getReactionBy(userProfile())) + } } class Factory(val account: Account) : ViewModelProvider.Factory { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 00ad2cc63..724c1d574 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -623,7 +623,7 @@ fun ShowVideoStreaming( } url?.let { - CheckIfUrlIsOnline(url) { + CheckIfUrlIsOnline(url, accountViewModel) { Row( verticalAlignment = Alignment.CenterVertically, modifier = remember { Modifier.heightIn(max = 300.dp) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 36de0257a..3b146279b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -29,7 +29,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog import com.vitorpamplona.amethyst.ui.screen.FeedViewModel @@ -42,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -136,12 +134,14 @@ private fun HomePages( } @Composable -fun CheckIfUrlIsOnline(url: String, whenOnline: @Composable () -> Unit) { +fun CheckIfUrlIsOnline(url: String, accountViewModel: AccountViewModel, whenOnline: @Composable () -> Unit) { var online by remember { mutableStateOf(false) } LaunchedEffect(key1 = url) { - launch(Dispatchers.IO) { - online = OnlineChecker.isOnline(url) + accountViewModel.checkIfOnline(url) { isOnline -> + if (online != isOnline) { + online = isOnline + } } } @@ -162,11 +162,9 @@ fun WatchAccountForHomeScreen( val followState by accountViewModel.account.userProfile().live().follows.observeAsState() LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList, followState) { - launch(Dispatchers.IO) { - NostrHomeDataSource.invalidateFilters() - homeFeedViewModel.checkKeysInvalidateDataAndSendToTop() - repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop() - } + NostrHomeDataSource.invalidateFilters() + homeFeedViewModel.checkKeysInvalidateDataAndSendToTop() + repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt index d48a598c9..7687ea956 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt @@ -73,42 +73,38 @@ fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, nav fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by baseNote.live().metadata.observeAsState() - val scope = rememberCoroutineScope() - LaunchedEffect(key1 = noteState) { - scope.launch { - val note = noteState?.note ?: return@launch - var event = note.event - val channelHex = note.channelHex() + val note = noteState?.note ?: return@LaunchedEffect + var event = note.event + val channelHex = note.channelHex() - if (event is GiftWrapEvent) { - event = accountViewModel.unwrap(event) - } + if (event is GiftWrapEvent) { + event = accountViewModel.unwrap(event) + } - if (event is SealedGossipEvent) { - event = accountViewModel.unseal(event) - } + if (event is SealedGossipEvent) { + event = accountViewModel.unseal(event) + } - if (event == null) { - // stay here, loading - } else if (event is ChannelCreateEvent) { - nav("Channel/${note.idHex}") - } else if (event is ChatroomKeyable) { - note.author?.let { - val withKey = (event as ChatroomKeyable) - .chatroomKey(accountViewModel.userProfile().pubkeyHex) + if (event == null) { + // stay here, loading + } else if (event is ChannelCreateEvent) { + nav("Channel/${note.idHex}") + } else if (event is ChatroomKeyable) { + note.author?.let { + val withKey = (event as ChatroomKeyable) + .chatroomKey(accountViewModel.userProfile().pubkeyHex) - withContext(Dispatchers.IO) { - accountViewModel.userProfile().createChatroom(withKey) - } - - nav("Room/${withKey.hashCode()}") + withContext(Dispatchers.IO) { + accountViewModel.userProfile().createChatroom(withKey) } - } else if (channelHex != null) { - nav("Channel/$channelHex") - } else { - nav("Note/${note.idHex}") + + nav("Room/${withKey.hashCode()}") } + } else if (channelHex != null) { + nav("Channel/$channelHex") + } else { + nav("Note/${note.idHex}") } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt index 299335c96..9ad3a2c55 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt @@ -50,5 +50,18 @@ class StatusEvent( val sig = CryptoUtils.sign(id, privateKey) return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig.toHexKey()) } + + fun clear( + event: StatusEvent, + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): StatusEvent { + val msg = "" + val tags = event.tags.filter { it.size > 1 && it[0] == "d" } + val pubKey = event.pubKey() + val id = generateId(pubKey, createdAt, kind, tags, msg) + val sig = CryptoUtils.sign(id, privateKey) + return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) + } } } From 9d9ad63b4def6a0a52ad41affbfc38552c4218b8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 17:15:17 -0400 Subject: [PATCH 17/46] BugFix deleting status --- .../com/vitorpamplona/amethyst/model/Account.kt | 15 +++++++++++++++ .../ui/screen/loggedIn/AccountViewModel.kt | 15 +++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 6b74a5855..7b556e08d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1027,6 +1027,21 @@ class Account( LocalCache.consume(event, null) } + fun deleteStatus(oldStatus: AddressableNote) { + if (!isWriteable()) return + val oldEvent = oldStatus.event as? StatusEvent ?: return + + val event = StatusEvent.clear(oldEvent, keyPair.privKey!!) + + Client.send(event) + LocalCache.consume(event, null) + + val event2 = DeletionEvent.create(listOf(event.id), keyPair.privKey!!) + + Client.send(event2) + LocalCache.consume(event2) + } + fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) { if (!isWriteable()) return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index eabb999d9..5300f38a4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -456,11 +456,22 @@ class AccountViewModel(val account: Account) : ViewModel() { } fun createStatus(newStatus: String) { - account.createStatus(newStatus) + viewModelScope.launch(Dispatchers.IO) { + account.createStatus(newStatus) + } } fun updateStatus(it: AddressableNote, newStatus: String) { - account.updateStatus(it, newStatus) + viewModelScope.launch(Dispatchers.IO) { + account.updateStatus(it, newStatus) + } + } + + fun deleteStatus(it: AddressableNote) { + viewModelScope.launch(Dispatchers.IO) { + account.deleteStatus(it) + } + } fun checkIfOnline(url: String, onResult: (Boolean) -> Unit) { viewModelScope.launch(Dispatchers.IO) { From 6c09e47e4f902ba4d1e15b2c2a16b04a227fefcf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 17:26:47 -0400 Subject: [PATCH 18/46] Moves coroutine of NIP-05 verification to the viewModel --- .../ui/note/NIP05VerificationDisplay.kt | 45 +++---------------- .../ui/screen/loggedIn/AccountViewModel.kt | 33 ++++++++++++++ .../ui/screen/loggedIn/ProfileScreen.kt | 2 +- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index e18db8176..db9e3fcad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -34,7 +34,6 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.Nip05Verifier import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadStatuses import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon @@ -55,13 +54,11 @@ import com.vitorpamplona.quartz.events.AddressableEvent import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds @Composable -fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState { +fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String, accountViewModel: AccountViewModel): MutableState { val nip05Verified = remember(userMetadata.nip05) { // starts with null if must verify or already filled in if verified in the last hour val default = if ((userMetadata.nip05LastVerificationTime ?: 0) > TimeUtils.oneHourAgo()) { @@ -75,37 +72,9 @@ fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): Mu if (nip05Verified.value == null) { LaunchedEffect(key1 = userMetadata.nip05) { - launch(Dispatchers.IO) { - userMetadata.nip05?.ifBlank { null }?.let { nip05 -> - Nip05Verifier().verifyNip05( - nip05, - onSuccess = { - // Marks user as verified - if (it == pubkeyHex) { - userMetadata.nip05Verified = true - userMetadata.nip05LastVerificationTime = TimeUtils.now() - - if (nip05Verified.value != true) { - nip05Verified.value = true - } - } else { - userMetadata.nip05Verified = false - userMetadata.nip05LastVerificationTime = 0 - - if (nip05Verified.value != false) { - nip05Verified.value = false - } - } - }, - onError = { - userMetadata.nip05LastVerificationTime = 0 - userMetadata.nip05Verified = false - - if (nip05Verified.value != false) { - nip05Verified.value = false - } - } - ) + accountViewModel.verifyNip05(userMetadata, pubkeyHex) { newVerificationStatus -> + if (nip05Verified.value != newVerificationStatus) { + nip05Verified.value = newVerificationStatus } } } @@ -156,7 +125,7 @@ private fun VerifyAndDisplayNIP05OrStatusLine( Column(modifier = columnModifier) { Row(verticalAlignment = Alignment.CenterVertically) { if (nip05 != null) { - val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex) + val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex, accountViewModel) if (nip05Verified.value != true) { DisplayNIP05(nip05, nip05Verified) @@ -358,12 +327,12 @@ private fun NIP05VerifiedSymbol(nip05Verified: MutableState, modifier: } @Composable -fun DisplayNip05ProfileStatus(user: User) { +fun DisplayNip05ProfileStatus(user: User, accountViewModel: AccountViewModel) { val uri = LocalUriHandler.current user.nip05()?.let { nip05 -> if (nip05.split("@").size <= 2) { - val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex) + val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel) Row(verticalAlignment = Alignment.CenterVertically) { NIP05VerifiedSymbol(nip05Verified, Size16Modifier) var domainPadStart = 5.dp diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 5300f38a4..4194e9d45 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -21,6 +21,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.amethyst.service.Nip05Verifier import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.components.UrlPreviewState @@ -35,6 +36,8 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.SealedGossipEvent +import com.vitorpamplona.quartz.events.UserMetadata +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet @@ -494,6 +497,36 @@ class AccountViewModel(val account: Account) : ViewModel() { } } + fun verifyNip05(userMetadata: UserMetadata, pubkeyHex: String, onResult: (Boolean) -> Unit) { + val nip05 = userMetadata.nip05?.ifBlank { null } ?: return + + viewModelScope.launch(Dispatchers.IO) { + Nip05Verifier().verifyNip05( + nip05, + onSuccess = { + // Marks user as verified + if (it == pubkeyHex) { + userMetadata.nip05Verified = true + userMetadata.nip05LastVerificationTime = TimeUtils.now() + + onResult(userMetadata.nip05Verified) + } else { + userMetadata.nip05Verified = false + userMetadata.nip05LastVerificationTime = 0 + + onResult(userMetadata.nip05Verified) + } + }, + onError = { + userMetadata.nip05LastVerificationTime = 0 + userMetadata.nip05Verified = false + + onResult(userMetadata.nip05Verified) + } + ) + } + } + class Factory(val account: Account) : ViewModelProvider.Factory { override fun create(modelClass: Class): AccountViewModel { return AccountViewModel(account) as AccountViewModel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 0654d3be9..166a1ef4a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -920,7 +920,7 @@ private fun DrawAdditionalInfo( DisplayBadges(baseUser, nav) - DisplayNip05ProfileStatus(user) + DisplayNip05ProfileStatus(user, accountViewModel) val website = user.info?.website if (!website.isNullOrEmpty()) { From 53b0db61ea8e7c58eeeeedb0d5713167bf376c5d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 17:32:19 -0400 Subject: [PATCH 19/46] Moves isAcceptableNote calculations to viewModel --- .../amethyst/ui/note/NoteCompose.kt | 4 +-- .../ui/screen/loggedIn/AccountViewModel.kt | 30 ++++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index a66f92d06..2f7d3db2a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -441,9 +441,7 @@ fun WatchForReports( val noteReportsState by note.live().reports.observeAsState() LaunchedEffect(key1 = noteReportsState, key2 = userFollowsState) { - launch(Dispatchers.Default) { - accountViewModel.isNoteAcceptable(note, onChange) - } + accountViewModel.isNoteAcceptable(note, onChange) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 4194e9d45..0b4a4ede2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -411,24 +411,26 @@ class AccountViewModel(val account: Account) : ViewModel() { } fun isNoteAcceptable(note: Note, onReady: (Boolean, Boolean, ImmutableSet) -> Unit) { - val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex - val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true + viewModelScope.launch { + val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex + val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true - if (isFromLoggedIn || isFromLoggedInFollow) { - // No need to process if from trusted people - onReady(true, true, persistentSetOf()) - } else { - val newCanPreview = !note.hasAnyReports() - - val newIsAcceptable = account.isAcceptable(note) - - if (newCanPreview && newIsAcceptable) { - // No need to process reports if nothing is wrong + if (isFromLoggedIn || isFromLoggedInFollow) { + // No need to process if from trusted people onReady(true, true, persistentSetOf()) } else { - val newRelevantReports = account.getRelevantReports(note) + val newCanPreview = !note.hasAnyReports() - onReady(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet()) + val newIsAcceptable = account.isAcceptable(note) + + if (newCanPreview && newIsAcceptable) { + // No need to process reports if nothing is wrong + onReady(true, true, persistentSetOf()) + } else { + val newRelevantReports = account.getRelevantReports(note) + + onReady(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet()) + } } } } From b7c9b14b1f19b16da0682a0fc66d703e17ac3750 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 17:47:01 -0400 Subject: [PATCH 20/46] Allowing any BaseTextNote on the reply window --- .../java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 2f7d3db2a..f0efd1b96 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -2397,9 +2397,9 @@ private fun ReplyRow( ) { val noteEvent = note.event - val showReply by remember { + val showReply by remember(note) { derivedStateOf { - noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser()) + noteEvent is BaseTextNoteEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser()) } } From ffb81c6246d491c4c0299aaa4b95763022895ba2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Aug 2023 17:51:38 -0400 Subject: [PATCH 21/46] v0.75.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e4ceff795..5d897c8dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 281 - versionName "0.75.2" + versionCode 282 + versionName "0.75.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From f7774700db5bcf7f8e3cbff9acb2ac41d5bfe19d Mon Sep 17 00:00:00 2001 From: Zoltan <34719275+ZsZolee@users.noreply.github.com> Date: Sat, 26 Aug 2023 13:22:27 +0200 Subject: [PATCH 22/46] Additional Hungarian translation --- app/src/main/res/values-hu/strings.xml | 51 ++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 9098a2aee..f68750610 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -356,7 +356,7 @@ \n4. Ha szükséges az Orbot-ban változtasd meg a portot \n5. Ezen a felületen állítsd be a Socks portot \n6. Kattíts az Aktiválás gombra, hogy az Orbot-ot átjátszóként használd - + ít Orbot Socks portja Érvénytelen Port szám Használd az Orbot-ot @@ -406,6 +406,7 @@ A ZapGyűjtés %1$s-nál. %2$s sats kell a célig Olvassás a csomópontból Írás a csomópontra + Hiba történt a csomópont információ szerzés közben a %1$s -tól Tulajdonos Verzió Szoftver @@ -454,8 +455,8 @@ Érzékeny tartalom Megjelenítés előtt egy érzékeny tartalom figyelmeztetés jelenik meg. Beállítások - Mindig - Csak WIFI + Mindig + Csak WIFI Rendszer Világos Sötét @@ -476,4 +477,48 @@ A Nostr cím ellenőrzésre került A Nostr cím ellenőrzése sikeretelen A Nostr cím ellenőrzése + Mind kijelölése/kijelölés visszavonása + Alapértelmezett + A folytatáshoz válassz egy csomópontot + + Zap-ek továbbítása: + A funkciót támogató kliensek a Zap-eket az Ön tárcája helyett, az alábbi LN-címre vagy felhasználói profilra továbbítják + + A Hely megjelenítése mint + A bejegyzéshez az Ön tartózkodási helyének Geohash-ét hozzáadja. A közönség tudni fogja, hogy az aktuális helytől 5 km-en (3 mérföldön) belül van + + A kényes tartalom miatt, azon megjelenítése előtt figyelmeztetést ad. Ez ideális minden Felnőtt tartalomhoz vagy olyan tartalomhoz, amelyet egyesek sértőnek vagy zavarónak találhatnak + + Új funkció + Az Amethystnek ennek a módnak az aktiválásához NIP-24 üzenetet kell küldenie (GiftWrapped, Zárolt direkt és csoportos üzeneteket). A NIP-24 új, és a legtöbb kliens még nem implementálta. Győződj meg arról, hogy a fogadó fél kompatibilis klienst használ. + Aktiválás + + Publikus + Privát + Címzett + Téma + A beszélgetés témája + "@Felhasználó1, @Felhasználó2, @UFelhasználó3" + + A csoport tagjai + Magyarázat a csoport tagjainak + Az új célok érdekében, a név megváltoztatása. + + Az applikáció felülete + Sötét, Világos vagy Rendszer által használt téma + Képek és GIF-ek automatikus betöltése + A videók és a GIF-ek automatikus lejátszása + URL előnézetek megjelenítése + Mikor kell a képeket betölteni + + Az URL vágólapra másolása + A bejegyzésazonosító vágólapra másolása + + Létrehozva + Szabályok + + Állapotod változtatása + + Hiba a hibaüzenet elemzésekor + A szavazatokat a Zap-ek összegével súlyozzuk. Beállíthatsz egy minimális összeget, hogy a kéretlen leveleket elkerüld, és egy maximális összeget annak elkerülésére, hogy a szavazás feletti irányítást a nagy Zapperek vegyék át. Mindkét mezőben ugyanazt az összeget használd, hogy minden szavazat azonos értéket kapjon. Bármilyen összeg elfogadásához, hagyjd üresen. From accb115a20b31d96874b1988f577ff4745d31a30 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 26 Aug 2023 10:36:44 -0400 Subject: [PATCH 23/46] Making statuses filter just one with the minimum of all observing users. --- .../service/NostrSingleUserDataSource.kt | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index cb835c6cf..2b99cb20b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -30,16 +30,28 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") { fun createUserStatusFilter(): List? { if (usersToWatch.isEmpty()) return null - return usersToWatch.map { + val minLatestEOSEs = mutableMapOf() + usersToWatch.forEach { + it.latestEOSEs.forEach { + val minEose = minLatestEOSEs[it.key] + if (minEose == null) { + minLatestEOSEs.put(it.key, EOSETime(it.value.time)) + } else if (it.value.time < minEose.time) { + minEose.time = it.value.time + } + } + } + + return listOf( TypedFilter( types = COMMON_FEED_TYPES, filter = JsonFilter( kinds = listOf(StatusEvent.kind), - authors = listOf(it.pubkeyHex), - since = it.latestEOSEs + authors = usersToWatch.map { it.pubkeyHex }, + since = minLatestEOSEs ) ) - } + ) } fun createUserReportFilter(): List? { @@ -75,7 +87,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") { } override fun updateChannelFilters() { - userChannel.typedFilters = listOfNotNull(createUserReportFilter(), createUserStatusFilter()).flatten().ifEmpty { null } + userChannel.typedFilters = listOfNotNull(createUserStatusFilter(), createUserReportFilter()).flatten().ifEmpty { null } userChannelOnce.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null } } From 99e545f0b8748cdadf5d2e059a46eeb9c88d0360 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 26 Aug 2023 11:32:37 -0400 Subject: [PATCH 24/46] Adds polls to the hashtag screen --- .../java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt | 2 ++ .../java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt index 1555feec5..1d5033d2b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent +import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.TextNoteEvent @@ -34,6 +35,7 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil it.event is LongTextNoteEvent || it.event is ChannelMessageEvent || it.event is PrivateDmEvent || + it.event is PollNoteEvent || it.event is AudioHeaderEvent ) && it.event?.isTaggedGeoHash(myTag) == true diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index 9319908ec..031162e92 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent +import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.TextNoteEvent @@ -34,6 +35,7 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil it.event is LongTextNoteEvent || it.event is ChannelMessageEvent || it.event is PrivateDmEvent || + it.event is PollNoteEvent || it.event is AudioHeaderEvent ) && it.event?.isTaggedHash(myTag) == true From 57c33ae06a29aff53299b9aeda12dc4d62098d4e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 26 Aug 2023 12:59:41 -0400 Subject: [PATCH 25/46] Improves User filters by merging lastEOSEs into a single filter. --- .../service/NostrSingleUserDataSource.kt | 108 +++++++++++------- 1 file changed, 65 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index 2b99cb20b..a4fc612d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -12,61 +12,60 @@ import com.vitorpamplona.quartz.events.StatusEvent object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") { var usersToWatch = setOf() - fun createUserFilter(): List? { + fun createUserMetadataFilter(): List? { if (usersToWatch.isEmpty()) return null - return usersToWatch.filter { it.info?.latestMetadata == null }.map { - TypedFilter( - types = COMMON_FEED_TYPES, - filter = JsonFilter( - kinds = listOf(MetadataEvent.kind), - authors = listOf(it.pubkeyHex), - limit = 1 - ) - ) - } - } - - fun createUserStatusFilter(): List? { - if (usersToWatch.isEmpty()) return null - - val minLatestEOSEs = mutableMapOf() - usersToWatch.forEach { - it.latestEOSEs.forEach { - val minEose = minLatestEOSEs[it.key] - if (minEose == null) { - minLatestEOSEs.put(it.key, EOSETime(it.value.time)) - } else if (it.value.time < minEose.time) { - minEose.time = it.value.time - } - } - } + val firstTimers = usersToWatch.filter { it.info?.latestMetadata == null }.map { it.pubkeyHex } return listOf( TypedFilter( types = COMMON_FEED_TYPES, filter = JsonFilter( - kinds = listOf(StatusEvent.kind), - authors = usersToWatch.map { it.pubkeyHex }, - since = minLatestEOSEs + kinds = listOf(MetadataEvent.kind, StatusEvent.kind), + authors = firstTimers, + limit = 10 * firstTimers.size ) ) ) } - fun createUserReportFilter(): List? { + fun createUserMetadataFilter(minLatestEOSEs: Map): TypedFilter? { if (usersToWatch.isEmpty()) return null - return usersToWatch.map { - TypedFilter( - types = COMMON_FEED_TYPES, - filter = JsonFilter( - kinds = listOf(ReportEvent.kind), - tags = mapOf("p" to listOf(it.pubkeyHex)), - since = it.latestEOSEs - ) + return TypedFilter( + types = COMMON_FEED_TYPES, + filter = JsonFilter( + kinds = listOf(StatusEvent.kind), + authors = usersToWatch.map { it.pubkeyHex }, + since = minLatestEOSEs ) - } + ) + } + + fun createUserStatusFilter(minLatestEOSEs: Map): TypedFilter? { + if (usersToWatch.isEmpty()) return null + + return TypedFilter( + types = COMMON_FEED_TYPES, + filter = JsonFilter( + kinds = listOf(StatusEvent.kind), + authors = usersToWatch.map { it.pubkeyHex }, + since = minLatestEOSEs + ) + ) + } + + fun createUserReportFilter(minLatestEOSEs: Map): TypedFilter? { + if (usersToWatch.isEmpty()) return null + + return TypedFilter( + types = COMMON_FEED_TYPES, + filter = JsonFilter( + kinds = listOf(ReportEvent.kind), + tags = mapOf("p" to usersToWatch.map { it.pubkeyHex }), + since = minLatestEOSEs + ) + ) } val userChannel = requestNewChannel() { time, relayUrl -> @@ -80,15 +79,38 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") { } } - val userChannelOnce = requestNewChannel() { time, relayUrl -> + val userChannelFirstTimers = requestNewChannel() { time, relayUrl -> // Many relays operate with limits in the amount of filters. // As information comes, the filters will be rotated to get more data. invalidateFilters() } override fun updateChannelFilters() { - userChannel.typedFilters = listOfNotNull(createUserStatusFilter(), createUserReportFilter()).flatten().ifEmpty { null } - userChannelOnce.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null } + val minLatestEOSEs = mutableMapOf() + val neverGottenAnEOSE = mutableSetOf() + usersToWatch.forEach { + if (it.latestEOSEs.isEmpty()) { // first time + neverGottenAnEOSE.add(it.pubkeyHex) + } else { + it.latestEOSEs.forEach { + val minEose = minLatestEOSEs[it.key] + if (minEose == null) { + minLatestEOSEs.put(it.key, EOSETime(it.value.time)) + } else if (it.value.time < minEose.time) { + minEose.time = it.value.time + } + } + } + } + + userChannel.typedFilters = listOfNotNull( + createUserMetadataFilter(minLatestEOSEs), + createUserStatusFilter(minLatestEOSEs), + createUserReportFilter(minLatestEOSEs) + ).ifEmpty { null } + userChannelFirstTimers.typedFilters = listOfNotNull( + createUserMetadataFilter() + ).flatten().ifEmpty { null } } fun add(user: User) { From 2be04bfbe27668602f92cdb599c2f0478a2f7510 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 26 Aug 2023 13:00:53 -0400 Subject: [PATCH 26/46] Better error handling when Zaps fail --- .../service/lnurl/LightningAddressResolver.kt | 2 +- .../amethyst/ui/navigation/AppNavigation.kt | 1 + .../amethyst/ui/navigation/Routes.kt | 7 ++- .../amethyst/ui/note/ChannelCardCompose.kt | 4 +- .../ui/note/ChatroomMessageCompose.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 15 +++++- .../amethyst/ui/note/PollNote.kt | 37 +++++++++---- .../amethyst/ui/note/ReactionsRow.kt | 21 ++++++-- .../amethyst/ui/note/ZapCustomDialog.kt | 54 ++++++++++++++++++- .../ui/screen/loggedIn/ChannelScreen.kt | 4 +- .../ui/screen/loggedIn/ChatroomScreen.kt | 17 +++++- .../ui/screen/loggedIn/VideoScreen.kt | 2 +- app/src/main/res/values/strings.xml | 4 ++ 13 files changed, 143 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 61e8d3d17..6adb42fd7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -57,7 +57,7 @@ class LightningAddressResolver() { if (it.isSuccessful) { onSuccess(it.body.string()) } else { - onError("Could not resolve $lnaddress. Error: ${it.code}. Check if the server up and if the lightning address $lnaddress is correct") + 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 up and if the lightning address is correct") } } } catch (e: Exception) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 6775b9562..df4b33aca 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -215,6 +215,7 @@ fun AppNavigation( composable(route.route, route.arguments, content = { ChatroomScreen( roomId = it.arguments?.getString("id"), + draftMessage = it.arguments?.getString("message"), accountViewModel = accountViewModel, nav = nav ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 9e4ff9981..f242c3815 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -120,9 +120,12 @@ sealed class Route( ) object Room : Route( - route = "Room/{id}", + route = "Room/{id}?message={message}", icon = R.drawable.ic_moments, - arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList() + arguments = listOf( + navArgument("id") { type = NavType.StringType }, + navArgument("message") { type = NavType.StringType; nullable = true; defaultValue = null } + ).toImmutableList() ) object RoomByAuthor : Route( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 5df050bf0..9c45a6a04 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -625,7 +625,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n Spacer(modifier = StdHorzSpacer) LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav) Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel) + ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav) } description?.let { @@ -739,7 +739,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou Spacer(modifier = StdHorzSpacer) LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav) Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel) + ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav) } description?.let { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index ab7081152..dceec11dd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -566,7 +566,7 @@ private fun StatusRow( Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) { LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav) Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel) + ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav) Spacer(modifier = DoubleHorzSpacer) ReplyReaction( baseNote = baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index f0efd1b96..56128b091 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -165,6 +165,7 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent +import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent @@ -782,9 +783,9 @@ private fun ShortCommunityActionOptions( nav: (String) -> Unit ) { Spacer(modifier = StdHorzSpacer) - LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav) + LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav) Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel) + ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav) WatchAddressableNoteFollows(note, accountViewModel) { isFollowing -> if (!isFollowing) { @@ -1236,6 +1237,16 @@ fun routeFor(note: Note, loggedIn: User): String? { return null } +fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String { + val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex)) + accountViewModel.account.userProfile().createChatroom(withKey) + return if (draftMessage != null) { + "Room/${withKey.hashCode()}?message=$draftMessage" + } else { + "Room/${withKey.hashCode()}" + } +} + fun routeFor(note: Channel): String { return "Channel/${note.idHex}" } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index c78a3bd83..13253cfb2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -130,8 +130,7 @@ private fun OptionNote( ZapVote( baseNote, poolOption, - accountViewModel, - pollViewModel, + pollViewModel = pollViewModel, nonClickablePrepend = { RenderOptionAfterVote( poolOption.descriptor, @@ -145,18 +144,21 @@ private fun OptionNote( ) }, clickablePrepend = { - } + }, + accountViewModel = accountViewModel, + nav = nav ) } else { ZapVote( baseNote, poolOption, - accountViewModel, - pollViewModel, + pollViewModel = pollViewModel, nonClickablePrepend = {}, clickablePrepend = { RenderOptionBeforeVote(poolOption.descriptor, canPreview, tags, backgroundColor, accountViewModel, nav) - } + }, + accountViewModel = accountViewModel, + nav = nav ) } } @@ -267,11 +269,12 @@ private fun RenderOptionBeforeVote( fun ZapVote( baseNote: Note, poolOption: PollOption, - accountViewModel: AccountViewModel, - pollViewModel: PollNoteViewModel, modifier: Modifier = Modifier, + pollViewModel: PollNoteViewModel, nonClickablePrepend: @Composable () -> Unit, - clickablePrepend: @Composable () -> Unit + clickablePrepend: @Composable () -> Unit, + accountViewModel: AccountViewModel, + nav: (String) -> Unit ) { val isLoggedUser by remember { derivedStateOf { @@ -281,6 +284,7 @@ fun ZapVote( var wantsToZap by remember { mutableStateOf(false) } var zappingProgress by remember { mutableStateOf(0f) } + var showErrorMessageDialog by remember { mutableStateOf(null) } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -382,7 +386,7 @@ fun ZapVote( onError = { scope.launch { zappingProgress = 0f - Toast.makeText(context, it, Toast.LENGTH_SHORT).show() + showErrorMessageDialog = it } }, onProgress = { @@ -393,6 +397,19 @@ fun ZapVote( ) } + if (showErrorMessageDialog != null) { + ErrorMessageDialog( + title = stringResource(id = R.string.error_dialog_zap_error), + textContent = showErrorMessageDialog ?: "", + onClickStartMessage = { + baseNote.author?.let { + nav(routeToMessage(it, showErrorMessageDialog, accountViewModel)) + } + }, + onDismiss = { showErrorMessageDialog = null } + ) + } + clickablePrepend() if (poolOption.zappedByLoggedIn) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index ff7d3bc46..a7fbccffc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -211,7 +211,7 @@ private fun InnerReactionRow( ) { val (value, elapsed) = measureTimedValue { Row(verticalAlignment = CenterVertically) { - ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel) + ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav = nav) } } Log.d("Rendering Metrics", "Reaction Zaps: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed") @@ -909,11 +909,13 @@ fun ZapReaction( grayTint: Color, accountViewModel: AccountViewModel, iconSize: Dp = 20.dp, - animationSize: Dp = 14.dp + animationSize: Dp = 14.dp, + nav: (String) -> Unit ) { var wantsToZap by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) } var wantsToSetCustomZap by remember { mutableStateOf(false) } + var showErrorMessageDialog by remember { mutableStateOf(null) } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -965,7 +967,7 @@ fun ZapReaction( onError = { scope.launch { zappingProgress = 0f - Toast.makeText(context, it, Toast.LENGTH_SHORT).show() + showErrorMessageDialog = it } }, onProgress = { @@ -976,6 +978,19 @@ fun ZapReaction( ) } + if (showErrorMessageDialog != null) { + ErrorMessageDialog( + title = stringResource(id = R.string.error_dialog_zap_error), + textContent = showErrorMessageDialog ?: "", + onClickStartMessage = { + baseNote.author?.let { + nav(routeToMessage(it, showErrorMessageDialog, accountViewModel)) + } + }, + onDismiss = { showErrorMessageDialog = null } + ) + } + if (wantsToChangeZapAmount) { UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, accountViewModel = accountViewModel) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 80cbd63da..591553867 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -4,11 +4,14 @@ import android.widget.Toast 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.outlined.Done import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType @@ -111,8 +114,7 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas onError = { zappingProgress = 0f scope.launch { - Toast - .makeText(context, it, Toast.LENGTH_SHORT).show() + Toast.makeText(context, it, Toast.LENGTH_SHORT).show() } }, onProgress = { @@ -225,3 +227,51 @@ fun ZapButton(isActive: Boolean, onPost: () -> Unit) { Text(text = "⚡Zap ", color = Color.White) } } + +@Composable +fun ErrorMessageDialog( + title: String, + textContent: String, + buttonColors: ButtonColors = ButtonDefaults.buttonColors(), + onClickStartMessage: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(title) + }, + text = { + Text(textContent) + }, + buttons = { + Row( + modifier = Modifier + .padding(all = 8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton(onClick = onClickStartMessage) { + Icon( + painter = painterResource(R.drawable.ic_dm), + contentDescription = null + ) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.error_dialog_talk_to_user)) + } + Button(onClick = onDismiss, colors = buttonColors) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Done, + contentDescription = null + ) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.error_dialog_button_ok)) + } + } + } + } + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 724c1d574..46fe8e705 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -893,7 +893,7 @@ private fun ShortChannelActionOptions( Spacer(modifier = StdHorzSpacer) LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav) Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel) + ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav) Spacer(modifier = StdHorzSpacer) } } @@ -968,7 +968,7 @@ private fun LiveChannelActionOptions( LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav) Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel) + ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav = nav) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index eed164283..6b51062c5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -101,6 +101,7 @@ import kotlinx.coroutines.withContext @Composable fun ChatroomScreen( roomId: String?, + draftMessage: String? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { @@ -110,6 +111,7 @@ fun ChatroomScreen( it?.let { PrepareChatroomViewModels( room = it, + draftMessage = draftMessage, accountViewModel = accountViewModel, nav = nav ) @@ -120,6 +122,7 @@ fun ChatroomScreen( @Composable fun ChatroomScreenByAuthor( authorPubKeyHex: String?, + draftMessage: String? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { @@ -129,6 +132,7 @@ fun ChatroomScreenByAuthor( it?.let { PrepareChatroomViewModels( room = it, + draftMessage = draftMessage, accountViewModel = accountViewModel, nav = nav ) @@ -166,7 +170,12 @@ fun LoadRoomByAuthor(authorPubKeyHex: String, accountViewModel: AccountViewModel } @Composable -fun PrepareChatroomViewModels(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun PrepareChatroomViewModels( + room: ChatroomKey, + draftMessage: String?, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { val feedViewModel: NostrChatroomFeedViewModel = viewModel( key = room.hashCode().toString() + "ChatroomViewModels", factory = NostrChatroomFeedViewModel.Factory( @@ -193,6 +202,12 @@ fun PrepareChatroomViewModels(room: ChatroomKey, accountViewModel: AccountViewMo } } + if (draftMessage != null) { + LaunchedEffect(key1 = draftMessage) { + newPostModel.message = TextFieldValue(draftMessage) + } + } + ChatroomScreen( room = room, feedViewModel = feedViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 1ce0efd78..e42350ae1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -467,7 +467,7 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St wantsToQuote = baseNote } LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp) - ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp) + ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp, nav = nav) ViewCountReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colors.onBackgroundColorFilter) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1ecc33a2..567000a05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -554,4 +554,8 @@ Error parsing error message Votes are weighted by the zap amount. You can set a minimum amount to avoid spammers and a maximum amount to avoid a large zappers taking over the poll. Use the same amount in both fields to make sure every vote is valued the same amount. Leave it empty to accept any amount. + + Unable to send zap + Message the User + Ok From de1f13d64ccef68206cf87051ec59ca200d7dc11 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 26 Aug 2023 13:04:35 -0400 Subject: [PATCH 27/46] Faster crossfade animation between screens --- .../amethyst/ui/navigation/AppNavigation.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index df4b33aca..167bbaf85 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -3,6 +3,9 @@ package com.vitorpamplona.amethyst.ui.navigation import android.content.Context import android.content.ContextWrapper import android.content.Intent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -80,7 +83,12 @@ fun AppNavigation( } } - NavHost(navController, startDestination = Route.Home.route) { + NavHost( + navController, + startDestination = Route.Home.route, + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { Route.Home.let { route -> composable(route.route, route.arguments, content = { it -> val nip47 = it.arguments?.getString("nip47") From d2d26e4e57ed57f9f900ccae90fa8d3a579836cf Mon Sep 17 00:00:00 2001 From: Anthony Robin Date: Sun, 27 Aug 2023 07:47:51 +0200 Subject: [PATCH 28/46] Update french translations Update french translations according to newly added english ones. --- app/src/main/res/values-fr/strings.xml | 45 +++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3cc7fe6d9..0c7928d6e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -95,7 +95,7 @@ Chargement… L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats "Répondre ici.. " - Copie l\'ID de note dans le presse-papiers pour le partage + Copie l\'ID de note dans le presse-papiers pour le partage sur Nostr Copier l\'ID de la chaîne (Note) dans le presse-papiers Modifie les métadonnées du canal Joindre @@ -474,9 +474,9 @@ Préférences de l\'application Langage Thème - Chargement automatique des images/gifs - Démarrage automatique des vidéos - Prévisualisation automatique des liens + Prévisualisation des images + Lecture vidéo + Prévisualisation des URLs Charger l\'image Spammeurs @@ -499,4 +499,41 @@ Ajoute un Geohash de votre emplacement au message. Le public saura que vous êtes à moins de 5km de l\'emplacement actuel Ajoute un avertissement de contenu sensible avant de montrer votre contenu. C\'est idéal pour tout contenu NSFW ou contenu que certaines personnes peuvent trouver offensant ou dérangeant + + Nouvelle Fonctionnalité + Pour activer ce mode, Amethyst doit envoyer un message NIP-24 (GiftWrapped, Sealed Direct et Group Messages). Le protocole NIP-24 est nouveau et la plupart des clients ne l\'ont pas encore mis en oeuvre. Assurez-vous que le destinataire utilise un client compatible. + Activer + + Public + Privé + À + Sujet + Sujet de la conversation + "@Utilisateur1, @Utilisateur2, @Utilisateur3" + + Membres de ce groupe + Explication aux membres + Changement de nom pour les nouveaux objectifs. + + Pour l\'interface de l\'App + Sombre, Clair ou thème Système + Charger automatiquement les images et les GIFs + Lire automatiquement les vidéos et les GIFs + Afficher la prévisualisation d\'URL + Quand charger les images + + Copier l\'URL dans le presse-papiers + Copier l\'ID de la note dans le presse-papiers + + Créé le + Règles + + Mettez à jour votre statut + + Erreur d\'analyse du message d\'erreur + Les votes sont pondérés par le montant du zap. Vous pouvez définir un montant minimum pour éviter les spammeurs et un montant maximum pour éviter qu\'un grand nombre de zappeurs ne prenne le contrôle du sondage. Utilisez le même montant dans les deux champs pour vous assurer que chaque vote a la même valeur. Laissez le champ vide pour accepter n\'importe quel montant. + + Impossible d\'envoyer un zap + Contacter l\'Utilisateur + Ok From 43ac1bf555336d29535eaf937f5e4121258d1424 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 12:10:12 -0400 Subject: [PATCH 29/46] Allows cleaning up on App Pause to perform separately from memory pruning functions --- .../main/java/com/vitorpamplona/amethyst/ServiceManager.kt | 6 +++++- .../main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index a33cc2594..f1d896a91 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -115,7 +115,11 @@ object ServiceManager { Client.disconnect() } - fun cleanUp() { + fun cleanObservers() { + LocalCache.cleanObservers() + } + + fun trimMemory() { LocalCache.cleanObservers() val accounts = LocalPreferences.allLocalAccountNPubs().mapNotNull { decodePublicKeyAsHexOrNull(it) }.toSet() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index bb82a3357..d86fd04b3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -103,6 +103,7 @@ class MainActivity : AppCompatActivity() { } override fun onPause() { + ServiceManager.cleanObservers() // if (BuildConfig.DEBUG) { debugState(this) // } @@ -127,7 +128,7 @@ class MainActivity : AppCompatActivity() { super.onTrimMemory(level) println("Trim Memory $level") GlobalScope.launch(Dispatchers.Default) { - ServiceManager.cleanUp() + ServiceManager.trimMemory() } } From d189dce7f002ebaf3579e4a0b8c5e920bd315864 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 12:16:13 -0400 Subject: [PATCH 30/46] Extracts chatroom from user file --- .../vitorpamplona/amethyst/model/Chatroom.kt | 63 +++++++++++++++++++ .../com/vitorpamplona/amethyst/model/User.kt | 56 ----------------- 2 files changed, 63 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt new file mode 100644 index 000000000..e23b02956 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt @@ -0,0 +1,63 @@ +package com.vitorpamplona.amethyst.model + +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.utils.TimeUtils + +@Stable +class Chatroom() { + var roomMessages: Set = setOf() + var subject: String? = null + var subjectCreatedAt: Long? = null + + @Synchronized + fun addMessageSync(msg: Note) { + checkNotInMainThread() + + if (msg !in roomMessages) { + roomMessages = roomMessages + msg + + val newSubject = msg.event?.subject() + + if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) { + subject = newSubject + subjectCreatedAt = msg.createdAt() + } + } + } + + @Synchronized + fun removeMessageSync(msg: Note) { + checkNotInMainThread() + + if (msg !in roomMessages) { + roomMessages = roomMessages + msg + + roomMessages.filter { it.event?.subject() != null }.sortedBy { it.createdAt() }.lastOrNull()?.let { + subject = it.event?.subject() + subjectCreatedAt = it.createdAt() + } + } + } + + fun senderIntersects(keySet: Set): Boolean { + return roomMessages.any { it.author?.pubkeyHex in keySet } + } + + fun pruneMessagesToTheLatestOnly(): Set { + val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + + val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) { + // Recent messages, keep last 100 + sorted.take(100).toSet() + } else { + // Old messages, keep the last one. + sorted.take(1).toSet() + } + sorted.filter { it.liveSet?.isInUse() ?: false } + + val toRemove = roomMessages.minus(toKeep) + roomMessages = toKeep + return toRemove + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index d46bdcc76..1941864ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -440,62 +440,6 @@ data class RelayInfo( var counter: Long ) -@Stable -class Chatroom() { - var roomMessages: Set = setOf() - var subject: String? = null - var subjectCreatedAt: Long? = null - - @Synchronized - fun addMessageSync(msg: Note) { - checkNotInMainThread() - - if (msg !in roomMessages) { - roomMessages = roomMessages + msg - - val newSubject = msg.event?.subject() - - if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) { - subject = newSubject - subjectCreatedAt = msg.createdAt() - } - } - } - - @Synchronized - fun removeMessageSync(msg: Note) { - checkNotInMainThread() - - if (msg !in roomMessages) { - roomMessages = roomMessages + msg - - roomMessages.filter { it.event?.subject() != null }.sortedBy { it.createdAt() }.lastOrNull()?.let { - subject = it.event?.subject() - subjectCreatedAt = it.createdAt() - } - } - } - - fun senderIntersects(keySet: Set): Boolean { - return roomMessages.any { it.author?.pubkeyHex in keySet } - } - - fun pruneMessagesToTheLatestOnly(): Set { - val sorted = roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() - - val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) { - // Recent messages, keep last 100 - sorted.take(100).toSet() - } else { - // Old messages, keep the last one. - sorted.take(1).toSet() - } + sorted.filter { it.liveSet?.isInUse() ?: false } - - val toRemove = roomMessages.minus(toKeep) - roomMessages = toKeep - return toRemove - } -} class UserLiveData(val user: User) : LiveData(UserState(user)) { // Refreshes observers in batches. From 8e622bfbb1599d904f55086179fa12108232ec6a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 12:16:36 -0400 Subject: [PATCH 31/46] Updates to debugging logs --- .../com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index b2976ada3..406eeca5c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -754,9 +754,9 @@ fun debugState(context: Context) { Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB") Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB") - Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size) - Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size) - Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size) + Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + " / " + LocalCache.notes.size) + Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + " / " + LocalCache.addressables.size) + Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + " / " + LocalCache.users.size) Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB") From 6a64ebcc5b5ba05f1d8a500d9228ad2cda25cd1f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 12:18:11 -0400 Subject: [PATCH 32/46] migrates to a double layer observer structure for the memory cache --- .../amethyst/model/LocalCache.kt | 4 +- .../com/vitorpamplona/amethyst/model/Note.kt | 129 +++++++++++------- .../com/vitorpamplona/amethyst/model/User.kt | 120 +++++++++------- .../amethyst/ui/note/ReactionsRow.kt | 29 ++-- 4 files changed, 165 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 4e88c8726..602fb8a89 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -360,7 +360,7 @@ object LocalCache { if (event.createdAt > (note.createdAt() ?: 0)) { note.loadEvent(event, author, emptyList()) - author.liveSet?.statuses?.invalidateData() + author.liveSet?.innerStatuses?.invalidateData() refreshObservers(note) } @@ -650,7 +650,7 @@ object LocalCache { mentions.forEach { // doesn't add to reports, but triggers recounts - it.liveSet?.reports?.invalidateData() + it.liveSet?.innerReports?.invalidateData() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 78b690678..542a8d8c9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -3,8 +3,8 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.distinctUntilChanged -import androidx.lifecycle.map import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji @@ -22,6 +22,7 @@ import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.toNote import com.vitorpamplona.quartz.events.* import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import java.math.BigDecimal import java.time.Instant @@ -126,7 +127,7 @@ open class Note(val idHex: String) { this.author = author this.replyTo = replyTo - liveSet?.metadata?.invalidateData() + liveSet?.innerMetadata?.invalidateData() } } @@ -174,21 +175,21 @@ open class Note(val idHex: String) { fun addReply(note: Note) { if (note !in replies) { replies = replies + note - liveSet?.replies?.invalidateData() + liveSet?.innerReplies?.invalidateData() } } fun removeReply(note: Note) { if (note in replies) { replies = replies - note - liveSet?.replies?.invalidateData() + liveSet?.innerReplies?.invalidateData() } } fun removeBoost(note: Note) { if (note in boosts) { boosts = boosts - note - liveSet?.boosts?.invalidateData() + liveSet?.innerBoosts?.invalidateData() } } @@ -211,11 +212,11 @@ open class Note(val idHex: String) { relays = listOf() lastReactionsDownloadTime = emptyMap() - liveSet?.replies?.invalidateData() - liveSet?.reactions?.invalidateData() - liveSet?.boosts?.invalidateData() - liveSet?.reports?.invalidateData() - liveSet?.zaps?.invalidateData() + liveSet?.innerReplies?.invalidateData() + liveSet?.innerReactions?.invalidateData() + liveSet?.innerBoosts?.invalidateData() + liveSet?.innerReports?.invalidateData() + liveSet?.innerZaps?.invalidateData() return toBeRemoved } @@ -234,7 +235,7 @@ open class Note(val idHex: String) { reactions = reactions + Pair(reaction, newList) } - liveSet?.reactions?.invalidateData() + liveSet?.innerReactions?.invalidateData() } } } @@ -246,7 +247,7 @@ open class Note(val idHex: String) { if (author in reports.keys && reports[author]?.contains(deleteNote) == true) { reports[author]?.let { reports = reports + Pair(author, it.minus(deleteNote)) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } } } @@ -254,28 +255,28 @@ open class Note(val idHex: String) { fun removeZap(note: Note) { if (zaps[note] != null) { zaps = zaps.minus(note) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } else if (zaps.containsValue(note)) { zaps = zaps.filterValues { it != note } - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } fun removeZapPayment(note: Note) { if (zapPayments[note] != null) { zapPayments = zapPayments.minus(note) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } else if (zapPayments.containsValue(note)) { val toRemove = zapPayments.filterValues { it == note } zapPayments = zapPayments.minus(toRemove.keys) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } fun addBoost(note: Note) { if (note !in boosts) { boosts = boosts + note - liveSet?.boosts?.invalidateData() + liveSet?.innerBoosts?.invalidateData() } } @@ -297,12 +298,12 @@ open class Note(val idHex: String) { if (zapRequest !in zaps.keys) { val inserted = innerAddZap(zapRequest, zap) if (inserted) { - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } else if (zaps[zapRequest] == null) { val inserted = innerAddZap(zapRequest, zap) if (inserted) { - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } } @@ -325,12 +326,12 @@ open class Note(val idHex: String) { if (zapPaymentRequest !in zapPayments.keys) { val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment) if (inserted) { - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } else if (zapPayments[zapPaymentRequest] == null) { val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment) if (inserted) { - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } } @@ -341,10 +342,10 @@ open class Note(val idHex: String) { if (reaction !in reactions.keys) { reactions = reactions + Pair(reaction, listOf(note)) - liveSet?.reactions?.invalidateData() + liveSet?.innerReactions?.invalidateData() } else if (reactions[reaction]?.contains(note) == false) { reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note) - liveSet?.reactions?.invalidateData() + liveSet?.innerReactions?.invalidateData() } } @@ -353,17 +354,17 @@ open class Note(val idHex: String) { if (author !in reports.keys) { reports = reports + Pair(author, listOf(note)) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } else if (reports[author]?.contains(note) == false) { reports = reports + Pair(author, (reports[author] ?: emptySet()) + note) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } } fun addRelay(relay: Relay) { if (relay.url !in relays) { relays = relays + relay.url - liveSet?.relays?.invalidateData() + liveSet?.innerRelays?.invalidateData() } } @@ -653,40 +654,52 @@ open class Note(val idHex: String) { @Stable class NoteLiveSet(u: Note) { // Observers line up here. - val metadata: NoteLiveData = NoteLiveData(u) + val innerMetadata = NoteBundledRefresherLiveData(u) + val innerReactions = NoteBundledRefresherLiveData(u) + val innerBoosts = NoteBundledRefresherLiveData(u) + val innerReplies = NoteBundledRefresherLiveData(u) + val innerReports = NoteBundledRefresherLiveData(u) + val innerRelays = NoteBundledRefresherLiveData(u) + val innerZaps = NoteBundledRefresherLiveData(u) - val authorChanges = metadata.map { + val metadata = innerMetadata.map { it } + val reactions = innerReactions.map { it } + val boosts = innerBoosts.map { it } + val replies = innerReplies.map { it } + val reports = innerReports.map { it } + val relays = innerRelays.map { it } + val zaps = innerZaps.map { it } + + val authorChanges = innerMetadata.map { it.note.author } - val hasEvent = metadata.map { + + val hasEvent = innerMetadata.map { it.note.event != null }.distinctUntilChanged() - val reactions: NoteLiveData = NoteLiveData(u) - val boosts: NoteLiveData = NoteLiveData(u) - val replies: NoteLiveData = NoteLiveData(u) - val reports: NoteLiveData = NoteLiveData(u) - val relays: NoteLiveData = NoteLiveData(u) - val zaps: NoteLiveData = NoteLiveData(u) - - val hasReactions = zaps.combineWith(boosts, reactions) { zapState, boostState, reactionState -> + val hasReactions = innerZaps.combineWith(innerBoosts, innerReactions) { zapState, boostState, reactionState -> zapState?.note?.zaps?.isNotEmpty() ?: false || boostState?.note?.boosts?.isNotEmpty() ?: false || reactionState?.note?.reactions?.isNotEmpty() ?: false }.distinctUntilChanged() - val replyCount = replies.map { + val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged() - val reactionCount = reactions.map { + val reactionCount = innerReactions.map { it.note.reactions.values.sumOf { it.size } }.distinctUntilChanged() - val boostCount = boosts.map { + val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged() + val boostList = innerBoosts.map { + it.note.boosts.toImmutableList() + }.distinctUntilChanged() + fun isInUse(): Boolean { return metadata.hasObservers() || reactions.hasObservers() || @@ -694,21 +707,28 @@ class NoteLiveSet(u: Note) { replies.hasObservers() || reports.hasObservers() || relays.hasObservers() || - zaps.hasObservers() + zaps.hasObservers() || + authorChanges.hasObservers() || + hasEvent.hasObservers() || + hasReactions.hasObservers() || + replyCount.hasObservers() || + reactionCount.hasObservers() || + boostCount.hasObservers() || + boostList.hasObservers() } fun destroy() { - metadata.destroy() - reactions.destroy() - boosts.destroy() - replies.destroy() - reports.destroy() - relays.destroy() - zaps.destroy() + innerMetadata.destroy() + innerReactions.destroy() + innerBoosts.destroy() + innerReplies.destroy() + innerReports.destroy() + innerRelays.destroy() + innerZaps.destroy() } } -class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { +class NoteBundledRefresherLiveData(val note: Note) : LiveData(NoteState(note)) { // Refreshes observers in batches. private val bundler = BundledUpdate(500, Dispatchers.IO) @@ -730,6 +750,17 @@ class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { } } + fun map( + transform: (NoteState) -> Y + ): NoteLoadingLiveData { + val initialValue = this.value?.let { transform(it) } + val result = NoteLoadingLiveData(note, initialValue) + result.addSource(this) { x -> result.value = transform(x) } + return result + } +} + +class NoteLoadingLiveData(val note: Note, initialValue: Y?) : MediatorLiveData(initialValue) { override fun onActive() { super.onActive() if (note is AddressableNote) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 1941864ad..85d608bae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource @@ -23,7 +24,6 @@ import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.toImmutableListOfLists -import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.Dispatchers import java.math.BigDecimal @@ -101,7 +101,7 @@ class User(val pubkeyHex: String) { if (event.id == latestBookmarkList?.id) return latestBookmarkList = event - liveSet?.bookmarks?.invalidateData() + liveSet?.innerBookmarks?.invalidateData() } fun updateContactList(event: ContactListEvent) { @@ -111,18 +111,18 @@ class User(val pubkeyHex: String) { latestContactList = event // Update following of the current user - liveSet?.follows?.invalidateData() + liveSet?.innerFollows?.invalidateData() // Update Followers of the past user list // Update Followers of the new contact list (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { - LocalCache.users[it]?.liveSet?.followers?.invalidateData() + LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData() } (latestContactList)?.unverifiedFollowKeySet()?.forEach { - LocalCache.users[it]?.liveSet?.followers?.invalidateData() + LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData() } - liveSet?.relays?.invalidateData() + liveSet?.innerRelays?.invalidateData() } fun addReport(note: Note) { @@ -130,10 +130,10 @@ class User(val pubkeyHex: String) { if (author !in reports.keys) { reports = reports + Pair(author, setOf(note)) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } else if (reports[author]?.contains(note) == false) { reports = reports + Pair(author, (reports[author] ?: emptySet()) + note) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } } @@ -143,7 +143,7 @@ class User(val pubkeyHex: String) { if (author in reports.keys && reports[author]?.contains(deleteNote) == true) { reports[author]?.let { reports = reports + Pair(author, it.minus(deleteNote)) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } } } @@ -151,20 +151,20 @@ class User(val pubkeyHex: String) { fun addZap(zapRequest: Note, zap: Note?) { if (zapRequest !in zaps.keys) { zaps = zaps + Pair(zapRequest, zap) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } else if (zapRequest in zaps.keys && zaps[zapRequest] == null) { zaps = zaps + Pair(zapRequest, zap) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } fun removeZap(zapRequestOrZapEvent: Note) { if (zapRequestOrZapEvent in zaps.keys) { zaps = zaps.minus(zapRequestOrZapEvent) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } else if (zapRequestOrZapEvent in zaps.values) { zaps = zaps.filter { it.value != zapRequestOrZapEvent } - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } @@ -218,7 +218,7 @@ class User(val pubkeyHex: String) { val privateChatroom = getOrCreatePrivateChatroom(room) if (msg !in privateChatroom.roomMessages) { privateChatroom.addMessageSync(msg) - liveSet?.messages?.invalidateData() + liveSet?.innerMessages?.invalidateData() } } @@ -226,7 +226,7 @@ class User(val pubkeyHex: String) { val privateChatroom = getOrCreatePrivateChatroom(user) if (msg !in privateChatroom.roomMessages) { privateChatroom.addMessageSync(msg) - liveSet?.messages?.invalidateData() + liveSet?.innerMessages?.invalidateData() } } @@ -240,7 +240,7 @@ class User(val pubkeyHex: String) { val privateChatroom = getOrCreatePrivateChatroom(user) if (msg in privateChatroom.roomMessages) { privateChatroom.removeMessageSync(msg) - liveSet?.messages?.invalidateData() + liveSet?.innerMessages?.invalidateData() } } @@ -255,7 +255,7 @@ class User(val pubkeyHex: String) { here.counter++ } - liveSet?.relayInfo?.invalidateData() + liveSet?.innerRelayInfo?.invalidateData() } fun updateUserInfo(newUserInfo: UserMetadata, latestMetadata: MetadataEvent) { @@ -280,7 +280,7 @@ class User(val pubkeyHex: String) { } } - liveSet?.metadata?.invalidateData() + liveSet?.innerMetadata?.invalidateData() } fun isFollowing(user: User): Boolean { @@ -382,54 +382,70 @@ class User(val pubkeyHex: String) { @Stable class UserLiveSet(u: User) { - // UI Observers line up here. - val follows: UserLiveData = UserLiveData(u) - val followers: UserLiveData = UserLiveData(u) - val reports: UserLiveData = UserLiveData(u) - val messages: UserLiveData = UserLiveData(u) - val relays: UserLiveData = UserLiveData(u) - val relayInfo: UserLiveData = UserLiveData(u) - val metadata: UserLiveData = UserLiveData(u) - val zaps: UserLiveData = UserLiveData(u) - val bookmarks: UserLiveData = UserLiveData(u) - val statuses: UserLiveData = UserLiveData(u) + val innerMetadata = UserBundledRefresherLiveData(u) - val profilePictureChanges = metadata.map { + // UI Observers line up here. + val innerFollows = UserBundledRefresherLiveData(u) + val innerFollowers = UserBundledRefresherLiveData(u) + val innerReports = UserBundledRefresherLiveData(u) + val innerMessages = UserBundledRefresherLiveData(u) + val innerRelays = UserBundledRefresherLiveData(u) + val innerRelayInfo = UserBundledRefresherLiveData(u) + val innerZaps = UserBundledRefresherLiveData(u) + val innerBookmarks = UserBundledRefresherLiveData(u) + val innerStatuses = UserBundledRefresherLiveData(u) + + // UI Observers line up here. + val metadata = innerMetadata.map { it } + val follows = innerFollows.map { it } + val followers = innerFollowers.map { it } + val reports = innerReports.map { it } + val messages = innerMessages.map { it } + val relays = innerRelays.map { it } + val relayInfo = innerRelayInfo.map { it } + val zaps = innerZaps.map { it } + val bookmarks = innerBookmarks.map { it } + val statuses = innerStatuses.map { it } + + val profilePictureChanges = innerMetadata.map { it.user.profilePicture() }.distinctUntilChanged() - val nip05Changes = metadata.map { + val nip05Changes = innerMetadata.map { it.user.nip05() }.distinctUntilChanged() - val userMetadataInfo = metadata.map { + val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged() fun isInUse(): Boolean { - return follows.hasObservers() || + return metadata.hasObservers() || + follows.hasObservers() || followers.hasObservers() || reports.hasObservers() || messages.hasObservers() || relays.hasObservers() || relayInfo.hasObservers() || - metadata.hasObservers() || zaps.hasObservers() || + bookmarks.hasObservers() || statuses.hasObservers() || - bookmarks.hasObservers() + profilePictureChanges.hasObservers() || + nip05Changes.hasObservers() || + userMetadataInfo.hasObservers() } fun destroy() { - follows.destroy() - followers.destroy() - reports.destroy() - messages.destroy() - relays.destroy() - relayInfo.destroy() - metadata.destroy() - zaps.destroy() - bookmarks.destroy() - statuses.destroy() + innerMetadata.destroy() + innerFollows.destroy() + innerFollowers.destroy() + innerReports.destroy() + innerMessages.destroy() + innerRelays.destroy() + innerRelayInfo.destroy() + innerZaps.destroy() + innerBookmarks.destroy() + innerStatuses.destroy() } } @@ -440,8 +456,7 @@ data class RelayInfo( var counter: Long ) - -class UserLiveData(val user: User) : LiveData(UserState(user)) { +class UserBundledRefresherLiveData(val user: User) : LiveData(UserState(user)) { // Refreshes observers in batches. private val bundler = BundledUpdate(500, Dispatchers.IO) @@ -462,6 +477,17 @@ class UserLiveData(val user: User) : LiveData(UserState(user)) { } } + fun map( + transform: (UserState) -> Y + ): UserLoadingLiveData { + val initialValue = this.value?.let { transform(it) } + val result = UserLoadingLiveData(user, initialValue) + result.addSource(this) { x -> result.value = transform(x) } + return result + } +} + +class UserLoadingLiveData(val user: User, initialValue: Y?) : MediatorLiveData(initialValue) { override fun onActive() { super.onActive() NostrSingleUserDataSource.add(user) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index a7fbccffc..8bbf1d157 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -319,15 +319,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc @Composable private fun WatchReactionsZapsBoostsAndDisplayIfExists(baseNote: Note, content: @Composable () -> Unit) { - val hasReactions by baseNote.live().zaps.combineWith( - liveData1 = baseNote.live().boosts, - liveData2 = baseNote.live().reactions, - block = { zapsState, boostsState, reactionsState -> - zapsState?.note?.zaps?.isNotEmpty() == true || - boostsState?.note?.boosts?.isNotEmpty() == true || - reactionsState?.note?.reactions?.isNotEmpty() == true - } - ).observeAsState( + val hasReactions by baseNote.live().hasReactions.observeAsState( baseNote.zaps.isNotEmpty() || baseNote.boosts.isNotEmpty() || baseNote.reactions.isNotEmpty() @@ -420,17 +412,16 @@ private fun WatchBoostsAndRenderGallery( nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - val boostsState by baseNote.live().boosts.observeAsState() - val boostsEvents by remember(boostsState) { - derivedStateOf { baseNote.boosts.toImmutableList() } - } + val boostsEvents by baseNote.live().boostList.observeAsState() - if (boostsEvents.isNotEmpty()) { - RenderBoostGallery( - boostsEvents, - nav, - accountViewModel - ) + boostsEvents?.let { + if (it.isNotEmpty()) { + RenderBoostGallery( + it, + nav, + accountViewModel + ) + } } } From 23a4d80ddbcebce671442af754abfa721d57c78e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 12:25:53 -0400 Subject: [PATCH 33/46] v0.75.4 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5d897c8dc..25cb7d222 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 282 - versionName "0.75.3" + versionCode 283 + versionName "0.75.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From b569c67d3b71405e4f78a9ebd40c78e5ddabd241 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 15:33:18 -0400 Subject: [PATCH 34/46] Fixes Hungarian labels --- app/src/main/res/values-hu/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f68750610..d985f4c11 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -455,8 +455,8 @@ Érzékeny tartalom Megjelenítés előtt egy érzékeny tartalom figyelmeztetés jelenik meg. Beállítások - Mindig - Csak WIFI + Mindig + Csak WIFI Rendszer Világos Sötét From da1d4018cf659f14acb818fc07397162ac1a9953 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 15:52:35 -0400 Subject: [PATCH 35/46] Fixes crash when more than 1 status --- .../vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index db9e3fcad..d36dd4b72 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -161,7 +161,7 @@ fun RotateStatuses( LaunchedEffect(Unit) { while (true) { delay(10.seconds) - indexToDisplay = ((indexToDisplay + 1) % (statuses.size + 1)) + indexToDisplay = ((indexToDisplay + 1) % (statuses.size)) } } } From 0b41110938e6f1012c699a72c8b17d3404cae8cc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 15:53:41 -0400 Subject: [PATCH 36/46] Fixes formatting --- .../vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index d36dd4b72..221175b16 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -161,7 +161,7 @@ fun RotateStatuses( LaunchedEffect(Unit) { while (true) { delay(10.seconds) - indexToDisplay = ((indexToDisplay + 1) % (statuses.size)) + indexToDisplay = (indexToDisplay + 1) % statuses.size } } } From 536bdfe43a7db27284ab6b4c40da4403efcd5490 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 16:32:35 -0400 Subject: [PATCH 37/46] - When All Follows, the live tab will load any event with a follow in the participant list. - Removes prefix filters --- .../service/NostrDiscoveryDataSource.kt | 99 +++++++++++-------- .../amethyst/service/NostrHomeDataSource.kt | 9 +- .../amethyst/service/NostrVideoDataSource.kt | 8 +- 3 files changed, 60 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index d58f40853..ed4c02364 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -18,53 +18,66 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { val latestEOSEs = EOSEAccount() - fun createLiveStreamFilter(): TypedFilter { - val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) + fun createLiveStreamFilter(): List { + val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList() - val followKeys = follows?.map { - it.substring(0, 8) - } - - return TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = JsonFilter( - authors = followKeys, - kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind), - limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList - ) + return listOfNotNull( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = JsonFilter( + authors = follows, + kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind), + limit = 300, + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + ) + ), + follows?.let { + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = JsonFilter( + tags = mapOf("p" to it), + kinds = listOf(LiveActivitiesEvent.kind), + limit = 100, + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + ) + ) + } ) } - fun createPublicChatFilter(): TypedFilter { - val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) + fun createPublicChatFilter(): List { + val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList() + val followChats = account.selectedChatsFollowList().toList() - val followKeys = follows?.map { - it.substring(0, 8) - } - - return TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = JsonFilter( - authors = followKeys, - kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind), - limit = 300, - since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + return listOf( + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = JsonFilter( + authors = follows, + kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind), + limit = 300, + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + ) + ), + TypedFilter( + types = setOf(FeedType.PUBLIC_CHATS), + filter = JsonFilter( + ids = followChats, + kinds = listOf(ChannelCreateEvent.kind), + limit = 300, + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList + ) ) ) } fun createCommunitiesFilter(): TypedFilter { - val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) - - val followKeys = follows?.map { - it.substring(0, 8) - } + val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList() return TypedFilter( types = setOf(FeedType.GLOBAL), filter = JsonFilter( - authors = followKeys, + authors = follows, kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind), limit = 300, since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList @@ -197,16 +210,16 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { } override fun updateChannelFilters() { - discoveryFeedChannel.typedFilters = listOfNotNull( - createLiveStreamFilter(), - createPublicChatFilter(), - createCommunitiesFilter(), - createLiveStreamTagsFilter(), - createPublicChatsTagsFilter(), - createCommunitiesTagsFilter(), - createCommunitiesGeohashesFilter(), - createPublicChatsGeohashesFilter(), - createLiveStreamGeohashesFilter() + discoveryFeedChannel.typedFilters = createLiveStreamFilter().plus(createPublicChatFilter()).plus( + listOfNotNull( + createLiveStreamTagsFilter(), + createLiveStreamGeohashesFilter(), + createCommunitiesFilter(), + createPublicChatsTagsFilter(), + createCommunitiesTagsFilter(), + createCommunitiesGeohashesFilter(), + createPublicChatsGeohashesFilter() + ) ).ifEmpty { null } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 2c49f5fad..183fbb935 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -54,13 +54,8 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { } fun createFollowAccountsFilter(): TypedFilter { - val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) - - val followKeys = follows?.map { - it.substring(0, 8) - } - - val followSet = followKeys?.plus(account.userProfile().pubkeyHex.substring(0, 8)) + val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() + val followSet = follows.plus(account.userProfile().pubkeyHex).toList() return TypedFilter( types = setOf(FeedType.FOLLOWS), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt index 1ca8bd199..2fcd5ecb0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt @@ -14,16 +14,12 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") { val latestEOSEs = EOSEAccount() fun createContextualFilter(): TypedFilter? { - val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList) - - val followKeys = follows?.map { - it.substring(0, 6) - } + val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)?.toList() return TypedFilter( types = setOf(FeedType.GLOBAL), filter = JsonFilter( - authors = followKeys, + authors = follows, kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind), limit = 200, since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList From 3c24c011e3ece600985c0faba0cd5b0dd9af60c1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 16:45:12 -0400 Subject: [PATCH 38/46] Avoids rebuilding the channel object in DataSource. --- .../vitorpamplona/amethyst/model/Channel.kt | 12 ++------ .../service/NostrSingleChannelDataSource.kt | 28 ++++++++++--------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 70bc4a706..92d9d7759 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -164,20 +164,12 @@ class ChannelLiveData(val channel: Channel) : LiveData(ChannelStat override fun onActive() { super.onActive() - if (channel is PublicChatChannel) { - NostrSingleChannelDataSource.add(channel.idHex) - } else { - NostrSingleChannelDataSource.add(channel.idHex) - } + NostrSingleChannelDataSource.add(channel) } override fun onInactive() { super.onInactive() - if (channel is PublicChatChannel) { - NostrSingleChannelDataSource.remove(channel.idHex) - } else { - NostrSingleChannelDataSource.remove(channel.idHex) - } + NostrSingleChannelDataSource.remove(channel) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt index 1315a614f..b72da4fe8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt @@ -1,7 +1,7 @@ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.amethyst.service.relays.FeedType @@ -11,10 +11,10 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") { - private var channelsToWatch = setOf() + private var channelsToWatch = setOf() - private fun createRepliesAndReactionsFilter(): TypedFilter? { - val reactionsToWatch = channelsToWatch.map { it } + private fun createMetadataChangeFilter(): TypedFilter? { + val reactionsToWatch = channelsToWatch.filter { it is PublicChatChannel }.map { it.idHex } if (reactionsToWatch.isEmpty()) { return null @@ -32,7 +32,6 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") { fun createLoadEventsIfNotLoadedFilter(): TypedFilter? { val directEventsToLoad = channelsToWatch - .mapNotNull { LocalCache.checkGetOrCreateChannel(it) } .filter { it.notes.isEmpty() && it is PublicChatChannel } val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet() @@ -53,7 +52,6 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") { fun createLoadStreamingIfNotLoadedFilter(): List? { val directEventsToLoad = channelsToWatch - .mapNotNull { LocalCache.checkGetOrCreateChannel(it) } .filterIsInstance() .filter { it.info == null } @@ -81,7 +79,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") { val singleChannelChannel = requestNewChannel() override fun updateChannelFilters() { - val reactions = createRepliesAndReactionsFilter() + val reactions = createMetadataChangeFilter() val missing = createLoadEventsIfNotLoadedFilter() val missingStreaming = createLoadStreamingIfNotLoadedFilter() @@ -90,13 +88,17 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") { ).ifEmpty { null } } - fun add(eventId: String) { - channelsToWatch = channelsToWatch.plus(eventId) - invalidateFilters() + fun add(eventId: Channel) { + if (eventId !in channelsToWatch) { + channelsToWatch = channelsToWatch.plus(eventId) + invalidateFilters() + } } - fun remove(eventId: String) { - channelsToWatch = channelsToWatch.minus(eventId) - invalidateFilters() + fun remove(eventId: Channel) { + if (eventId in channelsToWatch) { + channelsToWatch = channelsToWatch.minus(eventId) + invalidateFilters() + } } } From d116159ed92ee9f7371944ea78adc11b7bc52794 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 16:49:31 -0400 Subject: [PATCH 39/46] v0.75.5 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 25cb7d222..8e1bc33c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 283 - versionName "0.75.4" + versionCode 284 + versionName "0.75.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From e016e68e422fce0c709b6b307cbcc7e88e12b3e6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 17:11:49 -0400 Subject: [PATCH 40/46] removing unused sentence. --- app/src/main/res/values-hu/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index d985f4c11..e746df58b 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -406,7 +406,6 @@ A ZapGyűjtés %1$s-nál. %2$s sats kell a célig Olvassás a csomópontból Írás a csomópontra - Hiba történt a csomópont információ szerzés közben a %1$s -tól Tulajdonos Verzió Szoftver From 1ab84635786df2965586e5cd0adc28c89ec6e336 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 17:13:29 -0400 Subject: [PATCH 41/46] v0.75.6 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8e1bc33c2..2e670e2ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 284 - versionName "0.75.5" + versionCode 285 + versionName "0.75.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 0665c552aa1e3fb59e0e1641be641e080b54ce6e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 18:11:14 -0400 Subject: [PATCH 42/46] BugFix for a crash when receiving a new notification --- .../vitorpamplona/amethyst/model/Account.kt | 18 ++++++++++-------- .../notifications/EventNotificationConsumer.kt | 6 +++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 7b556e08d..04851d456 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -101,14 +101,16 @@ class Account( val showSensitiveContent: Boolean? ) - val liveHiddenUsers: LiveData = live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent -> - val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey) - LiveHiddenUsers( - hiddenUsers = liveBlockedUsers ?: persistentSetOf(), - spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), - showSensitiveContent = showSensitiveContent - ) - }.distinctUntilChanged() + val liveHiddenUsers: LiveData by lazy { + live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent -> + val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey) + LiveHiddenUsers( + hiddenUsers = liveBlockedUsers ?: persistentSetOf(), + spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), + showSensitiveContent = showSensitiveContent + ) + }.distinctUntilChanged() + } var userProfileCache: User? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index d73c22b10..5ca2e3af8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -23,7 +23,7 @@ import kotlinx.collections.immutable.persistentSetOf class EventNotificationConsumer(private val applicationContext: Context) { - fun consume(event: Event) { + suspend fun consume(event: Event) { if (LocalCache.notes[event.id] == null) { if (LocalCache.justVerify(event)) { LocalCache.justConsume(event, null) @@ -40,7 +40,7 @@ class EventNotificationConsumer(private val applicationContext: Context) { } } - fun unwrapAndConsume(event: Event, account: Account): Event? { + suspend fun unwrapAndConsume(event: Event, account: Account): Event? { if (!LocalCache.justVerify(event)) return null return when (event) { @@ -65,7 +65,7 @@ class EventNotificationConsumer(private val applicationContext: Context) { } } - private fun unwrapAndNotify(giftWrap: GiftWrapEvent) { + private suspend fun unwrapAndNotify(giftWrap: GiftWrapEvent) { val giftWrapNote = LocalCache.notes[giftWrap.id] ?: return LocalPreferences.allSavedAccounts().forEach { From bc5500731dcac74b9fe2605d8ca9b81a1923a7b9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 18:12:41 -0400 Subject: [PATCH 43/46] v0.75.7 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2e670e2ec..0132b04c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 285 - versionName "0.75.6" + versionCode 286 + versionName "0.75.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 6c54082a125b3059b0e7dd7bdb4ea9c9bd79c8a0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 19:16:58 -0400 Subject: [PATCH 44/46] clears the index cache when changing status fields. --- .../vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 221175b16..81a0d49c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -151,7 +151,7 @@ fun RotateStatuses( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - var indexToDisplay by remember { + var indexToDisplay by remember(statuses) { mutableIntStateOf(0) } From e1738a25a1244ee81d652a777941a4b72a454bd7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 19:17:58 -0400 Subject: [PATCH 45/46] Do a warning log and not an error log for verification fails (it usually happens in stringified JSON inside .content) --- quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 6cbaff6ea..5aa91ebf3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -218,7 +218,7 @@ open class Event( return try { hasCorrectIDHash() && hasVerifedSignature() } catch (e: Exception) { - Log.e("Event", "Event $id does not have a valid signature: ${toJson()}", e) + Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e) false } } From c69969947ba550f3a7fd20e569041d48f1ffae95 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 19:33:08 -0400 Subject: [PATCH 46/46] v0.75.8: Fixes status's cache update crash. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0132b04c7..cf1181170 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 286 - versionName "0.75.7" + versionCode 287 + versionName "0.75.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables {