diff --git a/PRIVACY.md b/PRIVACY.md index a63c9a2186..cfed3b5e56 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -4,33 +4,39 @@ Effective as of Jun 12, 2023 -The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open-source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them. +The Amethyst app for Android does not collect or process any personal information from its users. -The app may collect a per-device token, your public key, and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts are only stored locally on the device when it's required for the functionality and performance of Amethyst. This data is strictly confidential and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app. +The app is used to browse third-party Nostr servers (called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open-source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them. -Amethyst offers a few options to upload pictures and videos in order to post them online. You can choose the server at your discretion. Similar to relays, such services are independent of the app and have their own privacy policy and terms of use. +The app may collect a per-device token, your public key, and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts is only stored locally on the device when it's required for the functionality and performance of Amethyst. This data is strictly confidential and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app. -### Privacy with Relays services +Amethyst offers several options for uploading pictures and videos to post online. You can choose the server at your discretion. Similar to relays, such services are independent of the app and have their own privacy policy and terms of use. -Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online. +### Privacy with Relay services + +Your Internet Protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g., a VPN) from trackers online. The relay can also see which public keys you are using and what information you are requesting from the network. Your public key is tied to your IP address and your relay filters. Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of the content inside Private Zaps and Private DMs. -While the content of direct messages (DMs) is only visible to you, and your DM Nostr counterparty, everyone can see when you and your counterparty are DM-ing each other. Image uploads in the DM screen use one of the chosen image servers and simply paste the image link on the DM text. Your uploaded pictures are available to anyone with that direct link. +While the content of direct messages (DMs) is only visible to you and your DM Nostr counterparty, everyone can see when you and your counterparty are DM-ing each other. Image uploads in the DM screen use one of the chosen image servers and simply paste the image link into the DM text. Your uploaded pictures are available to anyone with that direct link. ### Visibility & Permanence of Your Content on Nostr Relays #### Information Visibility Content that you share can be shared with other relays by any user of the network. -Information that you share is publicly visible to anyone reading from relays that have your information. Your information may also be visible to Nostr users who do not share relays with you. +The information you share is publicly visible to anyone reading from relays that have access to your information. Your information may also be visible to Nostr users who do not share relays with you. #### Information Permanence Information shared on Nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted. +## Child safety standards + +Amethyst does not knowingly collect information from children. The app has no age verification because it collects no personal information from anyone. The application is 17+. We rely on Google Play's age verification to make sure the user downloading the app is an adult. Since we do not control which relays the user connects to, there is no content moderation beyond the standard block post, block account, and report post and/or account that will hide the content from the user. + ## Terms of Use ### For versions downloaded from Google's Play Store diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 82e5ae67be..5c72709ddc 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -21,7 +21,7 @@ def getCurrentBranch() { def generateVersionName(String baseVersion) { def currentBranch = getCurrentBranch() - if (currentBranch == "main" || currentBranch == "master") { + if (currentBranch == "main" || currentBranch == "master" || currentBranch == "unknown" || currentBranch == "HEAD") { return baseVersion } else { // Clean branch name for version (replace special characters) @@ -44,9 +44,9 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 418 - versionName = generateVersionName("0.94.3") - buildConfigField "String", "RELEASE_NOTES_ID", "\"fd42b23b9ef792059b1c1a89555443abbb11578f4b3c8430b452559eec7325f3\"" + versionCode = 424 + versionName = generateVersionName("1.00.5") + buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index f830850aac..2367c9f751 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -54,9 +54,9 @@ import com.halilibo.richtext.ui.material3.RichText import com.halilibo.richtext.ui.resolveDefaults import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler +import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.checkifItNeedsToRequestNotificationPermission import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 9101a08fbf..70e36a3f52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -82,7 +82,7 @@ class Amethyst : Application() { // Service that will run at all times to receive events from Pokey val pokeyReceiver = PokeyReceiver() - // creates okHttpClients based on the conditions of the connection and tor status + // manages all the other connections separately from relays. val okHttpClients = DualHttpClientManager( userAgent = appAgent, @@ -92,12 +92,36 @@ class Amethyst : Application() { scope = applicationIOScope, ) + // manages all relay connections + val okHttpClientForRelays = + DualHttpClientManager( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + keyCache = keyCache, + scope = applicationIOScope, + ) + + // manages all relay connections + val okHttpClientForRelaysForDms = + DualHttpClientManager( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + keyCache = keyCache, + scope = applicationIOScope, + ) + val torProxySettingsAnchor = ProxySettingsAnchor() // Connects the NostrClient class with okHttp val websocketBuilder = OkHttpWebSocket.Builder { url -> - okHttpClients.getHttpClient(torProxySettingsAnchor.useProxy(url)) + if (torProxySettingsAnchor.isDM(url)) { + okHttpClientForRelaysForDms.getHttpClient(torProxySettingsAnchor.useProxy(url)) + } else { + okHttpClientForRelays.getHttpClient(torProxySettingsAnchor.useProxy(url)) + } } // Caches all events in Memory diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index e14358b345..8824169045 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -214,6 +214,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal import java.util.Locale +import kotlin.coroutines.cancellation.CancellationException @OptIn(DelicateCoroutinesApi::class) @Stable @@ -1158,11 +1159,25 @@ class Account( return event } - suspend fun createAndSendDraft( + suspend fun createAndSendDraftIgnoreErrors( draftTag: String, template: EventTemplate, broadcast: Set = emptySet(), ) { + try { + createAndSendDraftInner(draftTag, template, broadcast) + } catch (e: Exception) { + if (e is CancellationException) throw e + } + } + + suspend fun createAndSendDraftInner( + draftTag: String, + template: EventTemplate, + broadcast: Set = emptySet(), + ) { + if (!isWriteable()) return + val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) @@ -1180,7 +1195,17 @@ class Account( } } - suspend fun deleteDraft(draftTag: String) { + suspend fun deleteDraftIgnoreErrors(draftTag: String) { + try { + deleteDraftInner(draftTag) + } catch (e: Exception) { + if (e is CancellationException) throw e + } + } + + suspend fun deleteDraftInner(draftTag: String) { + if (!isWriteable()) return + val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() val deletedDraft = DraftWrapEvent.createDeletedEvent(draftTag, signer) @@ -1307,7 +1332,7 @@ class Account( } if (draftTag != null) { - createAndSendDraft(draftTag, template) + createAndSendDraftIgnoreErrors(draftTag, template) } else { val it = signer.sign(template) cache.justConsumeMyOwnEvent(it) @@ -1352,7 +1377,7 @@ class Account( val broadcastNotes = mapEntitiesToNotes(quotes).toSet() if (draftTag != null) { - createAndSendDraft(draftTag, template) + createAndSendDraftIgnoreErrors(draftTag, template) } else { val it = signer.sign(template) cache.justConsumeMyOwnEvent(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index 4f6fe71288..13df157b6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState @@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest +import kotlin.collections.ifEmpty class FollowsPerOutboxRelay( kind3Follows: FollowListState, @@ -68,7 +70,11 @@ class FollowsPerOutboxRelay( mapOfSet { relayListNotes.forEach { noteState -> noteState.note.author?.pubkeyHex?.let { authorHex -> - (noteState.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.forEach { relay -> + val outboxRelayList = + getNIP65RelayList(authorHex)?.writeRelaysNorm() + ?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null } + ?: Constants.eventFinderRelays + outboxRelayList.forEach { relay -> add(relay, authorHex) } } @@ -85,7 +91,11 @@ class FollowsPerOutboxRelay( emit( mapOfSet { kind3Follows.flow.value.authors.map { authorHex -> - getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> + val outboxRelayList = + getNIP65RelayList(authorHex)?.writeRelaysNorm() + ?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null } + ?: Constants.eventFinderRelays + outboxRelayList.forEach { relay -> add(relay, authorHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt deleted file mode 100644 index 8f37e8cf34..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.model.nip65RelayList - -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest - -class OutboxRelaySetState( - usersToLoad: MutableStateFlow>, - val cache: LocalCache, - scope: CoroutineScope, -) { - fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.Companion.createAddress(pubkey) - - fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey)) - - fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow - - fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent - - fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } - - fun combineAllFlows(flows: List>): Flow> = - combine(flows) { relayListNotes: Array -> - relayListNotes.mapNotNull { - (it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() - } - }.map { - it.flatten().toSet() - } - - @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> = - usersToLoad - .transformLatest { followList -> - val flows: List> = allRelayListFlows(followList) - val relayListFlows = combineAllFlows(flows) - emitAll(relayListFlows) - }.onStart { - emit( - usersToLoad.value - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm() - }.flatten() - .toSet(), - ) - }.flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Companion.Eagerly, - emptySet(), - ) - - @OptIn(ExperimentalCoroutinesApi::class) - val flowSet: StateFlow> = - flow - .map { relayList -> - relayList.map { it.url }.toSet() - }.onStart { - emit( - usersToLoad.value - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() - }.flatten() - .toSet(), - ) - }.flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Companion.Eagerly, - emptySet(), - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt index c6b203ba1e..92f8d62365 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -64,40 +64,42 @@ class AppSpecificState( } init { - settings.backupAppSpecificData?.let { event -> - Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - LocalCache.justConsumeMyOwnEvent(event) - try { - val decrypted = signer.decrypt(event.content, event.pubKey) - val syncedSettings = JsonMapper.mapper.readValue(decrypted) - settings.syncedSettings.updateFrom(syncedSettings) - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value", e) + if (settings.isWriteable()) { + settings.backupAppSpecificData?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + LocalCache.justConsumeMyOwnEvent(event) + try { + val decrypted = signer.decrypt(event.content, event.pubKey) + val syncedSettings = JsonMapper.mapper.readValue(decrypted) + settings.syncedSettings.updateFrom(syncedSettings) + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value", e) + } } } - } - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "AppSpecificData Collector Start") - getAppSpecificDataFlow().collect { - try { - Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}") - (it.note.event as? AppSpecificDataEvent)?.let { - val decrypted = signer.decrypt(it.content, it.pubKey) - try { - val syncedSettings = JsonMapper.mapper.readValue(decrypted) - settings.updateAppSpecificData(it, syncedSettings) - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "AppSpecificData Collector Start") + getAppSpecificDataFlow().collect { + try { + Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}") + (it.note.event as? AppSpecificDataEvent)?.let { + val decrypted = signer.decrypt(it.content, it.pubKey) + try { + val syncedSettings = JsonMapper.mapper.readValue(decrypted) + settings.updateAppSpecificData(it, syncedSettings) + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) + } } + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decrypting latestAppSpecificData from Preferences", e) } - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decrypting latestAppSpecificData from Preferences", e) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt index d4f4e44fea..16cf45525b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.mapOfSet import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine class CommunityRelayLoader { @@ -81,8 +82,12 @@ class CommunityRelayLoader { ?.stateFlow } - return combine(noteMetadataFlows) { communityNotes -> - transformation(communitiesPerRelay(communityNotes, cache)) + return if (noteMetadataFlows.isEmpty()) { + MutableStateFlow(transformation(emptyMap())) + } else { + combine(noteMetadataFlows) { communityNotes -> + transformation(communitiesPerRelay(communityNotes, cache)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index 26bee7d8e6..2d992980e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model.topNavFeeds import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -28,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.mapOfSet import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine class OutboxRelayLoader { @@ -49,8 +51,9 @@ class OutboxRelayLoader { if (authorHex != null) { val relays = - (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.ifEmpty { null } - ?: cache.relayHints.hintsForKey(authorHex) + (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() + ?: cache.relayHints.hintsForKey(authorHex).ifEmpty { null } + ?: Constants.eventFinderRelays relays.forEach { add(it, authorHex) @@ -86,8 +89,12 @@ class OutboxRelayLoader { note.flow().metadata.stateFlow } - return combine(noteMetadataFlows) { outboxRelays -> - transformation(authorsPerRelay(outboxRelays, cache)) + return if (noteMetadataFlows.isEmpty()) { + MutableStateFlow(transformation(emptyMap())) + } else { + combine(noteMetadataFlows) { outboxRelays -> + transformation(authorsPerRelay(outboxRelays, cache)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt index 38c4911e50..c3fd5f88bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -86,6 +86,7 @@ class AllFollowsByOutboxTopNavFilter( } else { MutableStateFlow(emptyMap()) } + val communitiesPerRelay = if (communities != null) { CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { it } @@ -116,6 +117,7 @@ class AllFollowsByOutboxTopNavFilter( } else { emptyMap() } + val communitiesPerRelay = if (communities != null) { CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { it } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt index 596af8a4c5..508678a251 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt @@ -22,9 +22,12 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @Immutable class SingleCommunityTopNavPerRelayFilter( val community: String, val authors: Set?, -) : IFeedTopNavPerRelayFilter +) : IFeedTopNavPerRelayFilter { + val communityAddress = Address.parse(community) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index bf87bdbcbb..9489f8a065 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -73,13 +73,12 @@ class OkHttpClientFactory( userAgent: String, ): OkHttpClient { val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds - val duration = Duration.ofSeconds(seconds.toLong()) return rootClient .newBuilder() .proxy(proxy) - .readTimeout(duration) - .connectTimeout(duration) - .writeTimeout(duration) + .connectTimeout(Duration.ofSeconds(seconds.toLong())) + .readTimeout(Duration.ofSeconds(seconds.toLong() * 3)) + .writeTimeout(Duration.ofSeconds(seconds.toLong() * 3)) .addInterceptor(DefaultContentTypeInterceptor(userAgent)) .addNetworkInterceptor(logging) .addNetworkInterceptor(keyDecryptor) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt index 37af51d960..b97d329a28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt @@ -38,5 +38,6 @@ class ProxySettingsAnchor { ), ) + var isDM: (NormalizedRelayUrl) -> Boolean = { it in flow.value.value.dmRelayList } var useProxy: (NormalizedRelayUrl) -> Boolean = { flow.value.value.useTor(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt index f264bc9f53..e929a44c54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt @@ -67,10 +67,11 @@ fun GetVideoController( scope.launch { Log.d("PlaybackService", "Preparing Video ${controllerId.id} ${mediaItem.src.videoUri}") PlaybackServiceClient.prepareController( - controllerId, - mediaItem.src.videoUri, - mediaItem.src.proxyPort, - context, + mediaControllerState = controllerId, + videoUri = mediaItem.src.videoUri, + proxyPort = mediaItem.src.proxyPort, + keepPlaying = mediaItem.src.keepPlaying, + context = context, ) { controllerId -> scope.launch(Dispatchers.Main) { // checks if the player is still active after requesting to load @@ -174,10 +175,11 @@ fun GetVideoController( scope.launch(Dispatchers.Main) { Log.d("PlaybackService", "Preparing Video from Resume ${controllerId.id} ${mediaItem.src.videoUri} ") PlaybackServiceClient.prepareController( - controllerId, - mediaItem.src.videoUri, - mediaItem.src.proxyPort, - context, + mediaControllerState = controllerId, + videoUri = mediaItem.src.videoUri, + proxyPort = mediaItem.src.proxyPort, + keepPlaying = mediaItem.src.keepPlaying, + context = context, ) { controllerId -> scope.launch(Dispatchers.Main) { // checks if the player is still active after requesting to load diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt index 1a9e667062..bf5eb3450f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.composable import android.content.Context import android.view.View +import android.widget.FrameLayout import androidx.annotation.OptIn import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable @@ -49,7 +50,6 @@ fun RenderVideoPlayer( thumbData: VideoThumb?, showControls: Boolean = true, contentScale: ContentScale, - waveform: WaveformData? = null, borderModifier: Modifier, videoModifier: Modifier, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, @@ -64,6 +64,13 @@ fun RenderVideoPlayer( factory = { context: Context -> PlayerView(context).apply { player = controllerState.controller + // if we alrady know the size of the frame, this forces the player to stay in the size + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS) setBackgroundColor(Color.Transparent.toArgb()) setShutterBackgroundColor(Color.Transparent.toArgb()) @@ -101,7 +108,7 @@ fun RenderVideoPlayer( }, ) - waveform?.let { Waveform(it, controllerState, Modifier.align(Alignment.Center)) } + mediaItem.src.waveformData?.let { Waveform(it, controllerState, Modifier.align(Alignment.Center)) } if (showControls) { RenderControlButtons( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index 03aca3d3d9..0614fef0d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -55,14 +55,16 @@ fun VideoViewInner( val muted = remember(videoUri) { DEFAULT_MUTED_SETTING.value } GetMediaItem( - videoUri, - title, - artworkUri, - authorName, - nostrUriCallback, - mimeType, - aspectRatio, + videoUri = videoUri, + title = title, + artworkUri = artworkUri, + authorName = authorName, + callbackUri = nostrUriCallback, + mimeType = mimeType, + aspectRatio = aspectRatio, proxyPort = accountViewModel.proxyPortFor(videoUri), + keepPlaying = true, + waveformData = waveform, ) { mediaItem -> GetVideoController( mediaItem = mediaItem, @@ -76,7 +78,6 @@ fun VideoViewInner( thumbData = thumb, showControls = showControls, contentScale = contentScale, - waveform = waveform, borderModifier = borderModifier, videoModifier = videoModifier, onControllerVisibilityChanged = onControllerVisibilityChanged, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt index ab10d943c0..b88f35ef41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.compose.produceCachedState +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData val mediaItemCache = MediaItemCache() @@ -37,6 +38,8 @@ fun GetMediaItem( mimeType: String? = null, aspectRatio: Float? = null, proxyPort: Int? = null, + keepPlaying: Boolean = false, + waveformData: WaveformData? = null, inner: @Composable (LoadedMediaItem) -> Unit, ) { val data = @@ -50,6 +53,8 @@ fun GetMediaItem( mimeType = mimeType, aspectRatio = aspectRatio, proxyPort = proxyPort, + keepPlaying = keepPlaying, + waveformData = waveformData, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt index d4b664ff20..8b3166db2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.composable.mediaitem import androidx.compose.runtime.Immutable import androidx.media3.common.MediaItem +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData @Immutable data class MediaItemData( @@ -33,6 +34,8 @@ data class MediaItemData( val mimeType: String? = null, val aspectRatio: Float? = null, val proxyPort: Int? = null, + val keepPlaying: Boolean = true, + val waveformData: WaveformData? = null, ) @Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt index c0ef0c8659..2b0c6bd472 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt @@ -82,6 +82,12 @@ fun Waveform( pollCurrentDuration(it).collect { value -> waveformProgress.floatValue = value } } } + + LaunchedEffect(Unit) { + delay(500) + val position = mediaControllerState.controller?.let { it.currentPosition / it.duration.toFloat() } ?: 0f + waveformProgress.floatValue = position + } } private fun pollCurrentDuration(controller: MediaController) = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt index a4ec77431f..5470724558 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt @@ -20,8 +20,10 @@ */ package com.vitorpamplona.amethyst.service.playback.pip +import android.R.attr.data import android.graphics.Rect import android.os.Bundle +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData import com.vitorpamplona.quartz.utils.ensure @@ -58,6 +60,8 @@ class IntentExtras { mimeType = intent.getString("mimeType"), aspectRatio = if (ratio > 0) ratio else null, proxyPort = if (port > 0) port else null, + keepPlaying = intent.getBoolean("keepPlaying", true), + waveformData = intent.getFloatArray("wavefrontData")?.toList()?.let { WaveformData(it) }, ) } @@ -74,6 +78,8 @@ class IntentExtras { data.mimeType?.let { putString("mimeType", it) } data.aspectRatio?.let { putFloat("aspectRatio", it) } data.proxyPort?.let { putInt("proxyPort", it) } + data.keepPlaying.let { putBoolean("keepPlaying", it) } + data.waveformData?.let { putFloatArray("wavefrontData", it.wave.toFloatArray()) } bounds?.let { putInt("boundLeft", it.left) } bounds?.let { putInt("boundRight", it.right) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt index 168e03897e..bc2df36be8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import androidx.annotation.OptIn import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -42,8 +43,11 @@ import androidx.media3.ui.PlayerView import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem +import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform import com.vitorpamplona.amethyst.ui.components.getActivity +import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier @Composable fun PipVideo() { @@ -69,7 +73,7 @@ fun PipVideo() { videoData?.let { GetMediaItem(it) { mediaItem -> GetVideoController(mediaItem, false) { controller -> - PipVideo(controller) + PipVideo(controller, it.waveformData) } } } @@ -77,7 +81,10 @@ fun PipVideo() { @OptIn(UnstableApi::class) @Composable -fun PipVideo(controller: MediaControllerState) { +fun PipVideo( + controller: MediaControllerState, + waveformData: WaveformData?, +) { DisposableEffect(controller) { BackgroundMedia.switchKeepPlaying(controller) onDispose { @@ -117,5 +124,9 @@ fun PipVideo(controller: MediaControllerState) { } }, ) + + Row(VoiceHeightModifier, verticalAlignment = Alignment.CenterVertically) { + waveformData?.let { Waveform(it, controller, Modifier) } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt index 48bfad6b34..b299a3acc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt @@ -56,7 +56,7 @@ class SessionListener( class MediaSessionPool( val exoPlayerPool: ExoPlayerPool, val okHttpClient: OkHttpClient, - val reset: (MediaSession) -> Unit, + val reset: (MediaSession, Boolean) -> Unit, ) { val globalCallback = MediaSessionCallback(this) var lastCleanup = TimeUtils.now() @@ -65,7 +65,7 @@ class MediaSessionPool( private val playingMap = mutableMapOf() private val cache = - object : LruCache(SimultaneousPlaybackCalculator.max()) { // up to 10 videos in the screen at the same time + object : LruCache(10) { // up to 10 videos in the screen at the same time override fun entryRemoved( evicted: Boolean, key: String?, @@ -87,6 +87,7 @@ class MediaSessionPool( @OptIn(UnstableApi::class) fun newSession( id: String, + keepPlaying: Boolean, context: Context, ): MediaSession { val mediaSession = @@ -107,7 +108,7 @@ class MediaSessionPool( mediaSession.player.addListener(listener) - reset(mediaSession) + reset(mediaSession, keepPlaying) cache.put(mediaSession.id, SessionListener(mediaSession, listener)) @@ -167,6 +168,7 @@ class MediaSessionPool( fun getSession( id: String, + keepPlaying: Boolean, context: Context, ): MediaSession { val existingSession = playingMap.get(id) ?: cache.get(id) @@ -174,7 +176,7 @@ class MediaSessionPool( return existingSession.session } - return newSession(id, context) + return newSession(id, keepPlaying, context) } fun playingContent() = playingMap.values diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt index 0232d843c4..eb1c693f86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt @@ -44,9 +44,9 @@ class PlaybackService : MediaSessionService() { MediaSessionPool( ExoPlayerPool(ExoPlayerBuilder(okHttp)), okHttpClient = okHttp, - reset = { session -> + reset = { session, keepPlaying -> (session.player as ExoPlayer).apply { - repeatMode = Player.REPEAT_MODE_ONE + repeatMode = if (keepPlaying) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT volume = 0f } @@ -145,7 +145,8 @@ class PlaybackService : MediaSessionService() { override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { val id = controllerInfo.connectionHints.getString("id") ?: return null val proxyPort = controllerInfo.connectionHints.getInt("proxyPort") + val keepPlaying = controllerInfo.connectionHints.getBoolean("keepPlaying", true) val manager = lazyPool(proxyPort) - return manager.getSession(id, applicationContext) + return manager.getSession(id, keepPlaying, applicationContext) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackServiceClient.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackServiceClient.kt index 1018b3813c..1bd5bf6dd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackServiceClient.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackServiceClient.kt @@ -60,6 +60,7 @@ object PlaybackServiceClient { mediaControllerState: MediaControllerState, videoUri: String, proxyPort: Int? = 0, + keepPlaying: Boolean = true, context: Context, onReady: (MediaControllerState) -> Unit, ) { @@ -73,6 +74,7 @@ object PlaybackServiceClient { // link the id with the client's id to make sure it can return the // same session on background media. putString("id", mediaControllerState.id) + putBoolean("keepPlaying", keepPlaying) proxyPort?.let { putInt("proxyPort", it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 6b354e41da..812fc9e84a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -429,56 +429,47 @@ fun observeUserIsFollowing( return flow.collectAsStateWithLifecycle(user1.isFollowing(user2)) } +@SuppressLint("StateFlowValueCalledInComposition") @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable fun observeUserIsFollowingHashtag( - user: User, hashtag: String, accountViewModel: AccountViewModel, ): State { - // Subscribe in the relay for changes in the metadata of this user. - UserFinderFilterAssemblerSubscription(user, accountViewModel) - // Subscribe in the LocalCache for changes that arrive in the device val flow = - remember(user) { - user - .flow() - .follows.stateFlow - .sample(1000) - .mapLatest { userState -> - userState.user.isFollowingHashtag(hashtag) + remember(accountViewModel) { + accountViewModel.account.hashtagList.flow + .mapLatest { hashtags -> + hashtag in hashtags + }.onStart { + emit(hashtag in accountViewModel.account.hashtagList.flow.value) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } - return flow.collectAsStateWithLifecycle(user.isFollowingHashtag(hashtag)) + return flow.collectAsStateWithLifecycle(hashtag in accountViewModel.account.hashtagList.flow.value) } +@SuppressLint("StateFlowValueCalledInComposition") @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable fun observeUserIsFollowingGeohash( - user: User, geohash: String, accountViewModel: AccountViewModel, ): State { - // Subscribe in the relay for changes in the metadata of this user. - UserFinderFilterAssemblerSubscription(user, accountViewModel) - - // Subscribe in the LocalCache for changes that arrive in the device val flow = - remember(user) { - user - .flow() - .follows.stateFlow - .sample(1000) - .mapLatest { userState -> - userState.user.isFollowingGeohash(geohash) + remember(accountViewModel) { + accountViewModel.account.geohashList.flow + .mapLatest { geohashes -> + geohash in geohashes + }.onStart { + emit(geohash in accountViewModel.account.geohashList.flow.value) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } - return flow.collectAsStateWithLifecycle(user.isFollowingGeohash(geohash)) + return flow.collectAsStateWithLifecycle(geohash in accountViewModel.account.geohashList.flow.value) } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt index d147ef3fff..b419e86a58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt @@ -52,7 +52,7 @@ fun filterFindUserMetadataForKey( mapOfSet { authors.forEach { key -> val relays = - key.authorRelayList()?.writeRelaysNorm()?.ifEmpty { null } + key.authorRelayList()?.writeRelaysNorm() ?: LocalCache.relayHints.hintsForKey(key.pubkeyHex).ifEmpty { null } ?: (key.relaysBeingUsed.keys + defaultRelays).toList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index ebcd0f627a..75b68e1d60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -50,7 +50,7 @@ fun filterUserMetadataForKey( val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(it) val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) - authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } + authorHomeRelayEvent?.writeRelaysNorm() ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 643a72ace7..899c3c59c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -58,12 +58,12 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index 954e6a5a28..70ac084219 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn +package com.vitorpamplona.amethyst.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable @@ -50,22 +50,30 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP import kotlinx.collections.immutable.ImmutableList @Composable fun TextSpinner( - label: String?, + modifier: Modifier = Modifier, + label: String? = null, placeholder: String, options: ImmutableList, onSelect: (Int) -> Unit, - modifier: Modifier = Modifier, ) { - TextSpinner( + BaseTextSpinner( placeholder, options, onSelect, @@ -89,12 +97,38 @@ fun TextSpinner( onSelect: (Int) -> Unit, modifier: Modifier = Modifier, mainElement: @Composable (currentOption: String, modifier: Modifier) -> Unit, +) { + BaseTextSpinner( + placeholder = placeholder, + options = options, + onSelect = onSelect, + modifier = modifier, + mainElement = mainElement, + ) +} + +@Composable +private fun BaseTextSpinner( + placeholder: String, + options: ImmutableList, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier, + mainElement: @Composable (currentOption: String, modifier: Modifier) -> Unit, ) { val focusRequester = remember { FocusRequester() } val interactionSource = remember { MutableInteractionSource() } var optionsShowing by remember { mutableStateOf(false) } var currentText by remember(placeholder) { mutableStateOf(placeholder) } + val accessibilityDescription = + if (currentText == placeholder) { + "Dropdown menu, $placeholder, not selected" + } else { + "Dropdown menu, $currentText selected" + } + + val openDropdownLabel = stringRes(R.string.open_dropdown_menu) + Box( modifier = modifier, contentAlignment = Alignment.Center, @@ -105,13 +139,23 @@ fun TextSpinner( ) Box( modifier = - Modifier.matchParentSize().clickable( - interactionSource = interactionSource, - indication = null, - ) { - optionsShowing = true - focusRequester.requestFocus() - }, + Modifier + .matchParentSize() + .clickable( + interactionSource = interactionSource, + indication = null, + ) { + optionsShowing = true + focusRequester.requestFocus() + }.semantics { + role = Role.DropdownList + stateDescription = accessibilityDescription + onClick(label = openDropdownLabel) { + optionsShowing = true + focusRequester.requestFocus() + return@onClick true + } + }, ) } @@ -187,8 +231,17 @@ fun SpinnerSelectionDialog( } } itemsIndexed(options) { index, item -> + val optionsOfLabel = stringRes(R.string.option_of, index + 1, options.size) Row( - modifier = Modifier.fillMaxWidth().clickable { onSelect(index) }.padding(16.dp, 16.dp), + modifier = + Modifier + .fillMaxWidth() + .clickable { onSelect(index) } + .padding(16.dp, 16.dp) + .semantics { + role = Role.Button + contentDescription = optionsOfLabel + }, ) { Column { onRenderItem(item) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index c031d040a5..163011d9c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -44,6 +44,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -56,6 +61,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition import com.vitorpamplona.amethyst.ui.screen.CommunityName @@ -66,7 +72,6 @@ import com.vitorpamplona.amethyst.ui.screen.Name import com.vitorpamplona.amethyst.ui.screen.PeopleListName import com.vitorpamplona.amethyst.ui.screen.ResourceName import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer @@ -113,6 +118,13 @@ fun FeedFilterSpinner( Amethyst.instance.locationManager.setLocationPermission(locationPermissionState.status.isGranted) } + val accessibilityDescription = + if (selected != null) { + stringRes(R.string.feed_filter_selected, currentText) + } else { + stringRes(R.string.feed_filter_select_an_option, selectAnOption) + } + Box( modifier = modifier, contentAlignment = Alignment.Center, @@ -195,6 +207,13 @@ fun FeedFilterSpinner( indication = null, ) { optionsShowing = true + }.semantics { + role = Role.DropdownList + stateDescription = accessibilityDescription + onClick(label = "Open feed filter menu") { + optionsShowing = true + return@onClick true + } }, ) } @@ -202,6 +221,7 @@ fun FeedFilterSpinner( if (optionsShowing) { options.isNotEmpty().also { SpinnerSelectionDialog( + title = explainer, options = options, onDismiss = { optionsShowing = false }, onSelect = { @@ -260,12 +280,18 @@ fun RenderOption( val noteEvent = noteState.note.event val name = - if (noteEvent is PeopleListEvent) { - noteEvent.nameOrTitle() ?: option.note.dTag() - } else if (noteEvent is FollowListEvent) { - noteEvent.title() ?: option.note.dTag() - } else { - option.note.dTag() + when (noteEvent) { + is PeopleListEvent -> { + noteEvent.nameOrTitle() ?: option.note.dTag() + } + + is FollowListEvent -> { + noteEvent.title() ?: option.note.dTag() + } + + else -> { + option.note.dTag() + } } Text(text = name, color = MaterialTheme.colorScheme.onSurface) @@ -278,7 +304,7 @@ fun RenderOption( ) { val it by observeNote(option.note, accountViewModel) - Text(text = "/n/${((it?.note as? AddressableNote)?.dTag() ?: "")}", color = MaterialTheme.colorScheme.onSurface) + Text(text = "/n/${((it.note as? AddressableNote)?.dTag() ?: "")}", color = MaterialTheme.colorScheme.onSurface) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 1a99a0cb0b..4bdf1b7b56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -240,6 +240,7 @@ fun NoteCompose( WatchNoteEvent( baseNote = baseNote, accountViewModel = accountViewModel, + nav, modifier, ) { CheckHiddenFeedWatchBlockAndReport( @@ -312,7 +313,7 @@ fun AcceptableNote( } is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) else -> - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, nav) { showPopup -> CheckNewAndRenderNote( baseNote = baseNote, modifier = modifier, @@ -356,9 +357,9 @@ fun AcceptableNote( nav = nav, ) } - is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) + is BadgeDefinitionEvent -> BadgeDisplay(baseNote, accountViewModel) else -> - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote, accountViewModel, nav) { showPopup -> CheckNewAndRenderNote( baseNote = baseNote, modifier = modifier, @@ -798,7 +799,7 @@ private fun RenderNoteRow( is VideoNormalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav) is VideoShortEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav) is PictureEvent -> PictureDisplay(baseNote, true, ContentScale.FillWidth, PaddingValues(vertical = 5.dp), backgroundColor, accountViewModel, nav) - is BaseVoiceEvent -> RenderVoiceTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav) + is BaseVoiceEvent -> RenderVoiceTrack(baseNote, accountViewModel, nav) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel) is CommunityPostApprovalEvent -> { RenderPostApproval( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 698f434910..c2afd3651a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -82,7 +82,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.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.painterRes @@ -142,6 +141,7 @@ val externalLinkForNote = { note: Note -> fun LongPressToQuickAction( baseNote: Note, accountViewModel: AccountViewModel, + nav: INav, content: @Composable (() -> Unit) -> Unit, ) { val popupExpanded = remember { mutableStateOf(false) } @@ -153,7 +153,7 @@ fun LongPressToQuickAction( note = baseNote, onDismiss = { popupExpanded.value = false }, accountViewModel = accountViewModel, - nav = EmptyNav, + nav = nav, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 907f5bf9a5..13a5092570 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -81,12 +81,12 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.stringRes @@ -97,7 +97,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @Composable fun UpdateZapAmountDialog( @@ -536,7 +535,7 @@ fun authenticate( fun keyguardPrompt() { val intent = keyguardManager.createConfirmDeviceCredentialIntent( - stringRes(context, R.string.app_name_release), + stringRes(context, R.string.app_name), title, ) @@ -556,7 +555,7 @@ fun authenticate( val promptInfo = BiometricPrompt.PromptInfo .Builder() - .setTitle(stringRes(context, R.string.app_name_release)) + .setTitle(stringRes(context, R.string.app_name)) .setSubtitle(title) .setAllowedAuthenticators(authenticators) .build() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt index 30358376a4..61c3954814 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @OptIn(ExperimentalFoundationApi::class) @@ -36,6 +38,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun WatchNoteEvent( baseNote: Note, accountViewModel: AccountViewModel, + nav: INav, modifier: Modifier = Modifier, shortPreview: Boolean = false, onNoteEventFound: @Composable () -> Unit, @@ -47,6 +50,7 @@ fun WatchNoteEvent( LongPressToQuickAction( baseNote = baseNote, accountViewModel = accountViewModel, + nav = nav, ) { showPopup -> BlankNote( remember { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 7526b3e9eb..5f16c7d801 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -65,11 +65,11 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index a119bc8a94..0d3b1052ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -61,11 +61,11 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.note.CancelIcon import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.QuoteBorder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 7bfefdeca2..6c05d97144 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -317,12 +317,14 @@ open class CommentPostViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - accountViewModel.account.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { val attachments = mutableSetOf() nip95attachments.forEach { @@ -331,7 +333,7 @@ open class CommentPostViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index d5874e8246..d4b8eb9585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -45,7 +45,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -86,7 +85,6 @@ import com.vitorpamplona.amethyst.ui.theme.replyModifier import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable @@ -144,16 +142,16 @@ fun GenericCommentPostScreen( onCancel = { // uses the accountViewModel scope to avoid cancelling this // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { postViewModel.sendDraftSync() - nav.popBack() postViewModel.cancel() } + nav.popBack() }, onPost = { // uses the accountViewModel scope to avoid cancelling this // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { postViewModel.sendPostSync() nav.popBack() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 60346dd4a1..6278738351 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -54,7 +54,6 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture @@ -87,8 +86,6 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import java.util.Locale @Composable @@ -362,10 +359,10 @@ fun WatchAddressableNoteFollows( accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit, ) { - val state by accountViewModel.account.kind3FollowList.flow + val state by accountViewModel.account.communityList.flowSet .collectAsStateWithLifecycle() - onFollowChanges(state.communities.contains(note.idHex)) + onFollowChanges(state.contains(note.idHex)) } @Composable @@ -376,16 +373,9 @@ fun JoinCommunityButton( ) { Button( modifier = Modifier.padding(horizontal = 3.dp), - onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.follow(note) - } - }, + onClick = { accountViewModel.follow(note) }, shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), contentPadding = ButtonPadding, ) { Text(text = stringRes(R.string.join), color = Color.White) @@ -402,7 +392,7 @@ fun LeaveCommunityButton( Button( modifier = Modifier.padding(horizontal = 3.dp), - onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.unfollow(note) } }, + onClick = { accountViewModel.unfollow(note) }, shape = ButtonBorder, colors = ButtonDefaults.buttonColors( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index a0ace2304a..e77a7734f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -20,22 +20,56 @@ */ package com.vitorpamplona.amethyst.ui.note.types +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.OptIn +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PauseCircleOutline +import androidx.compose.material.icons.filled.PlayCircleOutline +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.PlayerView +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.playback.composable.VideoView +import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController +import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.WaveformData +import com.vitorpamplona.amethyst.service.playback.composable.controls.RenderControlButtons +import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem +import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem +import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform +import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia.isPlaying import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp +import com.vitorpamplona.amethyst.ui.theme.Size50Modifier +import com.vitorpamplona.amethyst.ui.theme.Size75Modifier +import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier +import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent @@ -43,23 +77,28 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent @Composable fun RenderVoiceTrack( note: Note, - contentScale: ContentScale, accountViewModel: AccountViewModel, nav: INav, ) { val noteEvent = note.event as? BaseVoiceEvent ?: return - VoiceHeader(noteEvent, note, contentScale, accountViewModel, nav) + VoiceHeader(noteEvent, note, accountViewModel, nav) } @Composable fun VoiceHeader( noteEvent: BaseVoiceEvent, note: Note, - contentScale: ContentScale, accountViewModel: AccountViewModel, nav: INav, ) { + val media = + remember(noteEvent) { + noteEvent.content.ifBlank { null } ?: noteEvent.iMetaTags().firstOrNull()?.url + } + + if (media == null) return + val waveform = remember(noteEvent) { noteEvent @@ -68,32 +107,40 @@ fun VoiceHeader( ?.waveform ?.let { WaveformData(it) } } - val media = - remember(noteEvent) { - noteEvent.content.ifBlank { null } ?: noteEvent.iMetaTags().firstOrNull()?.url - } - if (media == null) return + val callbackUri = remember(note) { note.toNostrUri() } - Column(modifier = Modifier.fillMaxWidth().padding(top = 5.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) { Row( verticalAlignment = Alignment.CenterVertically, ) { - VideoView( + GetMediaItem( videoUri = media, - mimeType = null, - waveform = waveform, title = noteEvent.subject(), + artworkUri = null, authorName = note.author?.toBestDisplayName(), - roundedCorner = true, - contentScale = contentScale, - accountViewModel = accountViewModel, - nostrUriCallback = note.toNostrUri(), - ) + callbackUri = callbackUri, + mimeType = null, + aspectRatio = null, + proxyPort = accountViewModel.proxyPortFor(media), + keepPlaying = false, + waveformData = waveform, + ) { mediaItem -> + GetVideoController( + mediaItem = mediaItem, + muted = false, + ) { controller -> + RenderVoicePlayer( + mediaItem = mediaItem, + controllerState = controller, + waveform = waveform, + borderModifier = MaterialTheme.colorScheme.imageModifier, + accountViewModel = accountViewModel, + ) + } + } } - val callbackUri = remember(note) { note.toNostrUri() } - if (noteEvent.hasHashtags()) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav) @@ -101,3 +148,119 @@ fun VoiceHeader( } } } + +@Composable +@OptIn(UnstableApi::class) +fun RenderVoicePlayer( + mediaItem: LoadedMediaItem, + controllerState: MediaControllerState, + waveform: WaveformData? = null, + borderModifier: Modifier, + accountViewModel: AccountViewModel, +) { + val controllerVisible = remember(controllerState) { mutableStateOf(false) } + + Box(modifier = borderModifier) { + AndroidView( + modifier = + Modifier + .fillMaxWidth() + .height(100.dp), + factory = { context: Context -> + PlayerView(context).apply { + player = controllerState.controller + // if we alrady know the size of the frame, this forces the player to stay in the size + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + + setBackgroundColor(Color.Transparent.toArgb()) + setShutterBackgroundColor(Color.Transparent.toArgb()) + + controllerAutoShow = false + useController = true + hideController() + + setControllerVisibilityListener( + PlayerView.ControllerVisibilityListener { visible -> + controllerVisible.value = visible == View.VISIBLE + }, + ) + } + }, + ) + + Row(VoiceHeightModifier, verticalAlignment = Alignment.CenterVertically) { + PlayPauseButton(controllerState) + waveform?.let { Waveform(it, controllerState, Modifier) } + } + + RenderControlButtons( + mediaItem.src, + controllerState, + controllerVisible, + Modifier.align(Alignment.TopEnd), + accountViewModel, + ) + } +} + +@Composable +fun PlayPauseButton(controllerState: MediaControllerState) { + val controller = controllerState.controller + var isPlaying by remember(controller) { + mutableStateOf(controllerState.isPlaying()) + } + + if (controller != null) { + val view = LocalView.current + // Keeps the screen on while playing and viewing videos. + DisposableEffect(key1 = controller, key2 = view) { + val listener = + object : Player.Listener { + override fun onIsPlayingChanged(playing: Boolean) { + // doesn't consider the mutex because the screen can turn off if the video + // being played in the mutex is not visible. + if (view.keepScreenOn != playing) { + view.keepScreenOn = playing + } + isPlaying = playing + } + + fun destroy() { + if (view.keepScreenOn) { + view.keepScreenOn = false + } + isPlaying = false + } + } + + controller.addListener(listener) + onDispose { + controller.removeListener(listener) + listener.destroy() + } + } + + // Play/Pause Button + IconButton( + onClick = { + if (controller.isPlaying) { + controller.pause() + } else { + controller.play() + } + }, + modifier = Size75Modifier, + ) { + Icon( + imageVector = if (isPlaying) Icons.Default.PauseCircleOutline else Icons.Default.PlayCircleOutline, + contentDescription = if (isPlaying) stringResource(R.string.pause) else stringResource(R.string.play), + modifier = Size50Modifier, + tint = Color.White, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index a563316d3c..9f7d4becb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -66,7 +66,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest @Stable class FollowListState( @@ -160,12 +159,12 @@ class FollowListState( @OptIn(ExperimentalCoroutinesApi::class) val liveKind3FollowsFlow: Flow> = - account.kind3FollowList.flow.transformLatest { + combineTransform(account.kind3FollowList.flow, account.hashtagList.flow, account.geohashList.flow, account.communityList.flow) { kind3List, hashtagList, geotagList, communityList -> checkNotInMainThread() val communities = - it.communities.mapNotNull { - LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote -> + communityList.mapNotNull { + LocalCache.getOrCreateAddressableNote(it.address).let { communityNote -> TagFeedDefinition( "Community/${communityNote.idHex}", CommunityName(communityNote), @@ -178,7 +177,7 @@ class FollowListState( } val hashtags = - it.hashtags.map { + hashtagList.map { TagFeedDefinition( "Hashtag/$it", HashtagName(it), @@ -190,7 +189,7 @@ class FollowListState( } val geotags = - it.geotags.map { + geotagList.map { TagFeedDefinition( "Geohash/$it", GeoHashName(it), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 2d9bbd153a..962a1009d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivitie import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.DiscoverCommunityFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.DiscoverNIP89FeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.DiscoverMarketplaceFeedFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal.DraftEventsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeConversationsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter @@ -68,6 +69,8 @@ class AccountFeedContentStates( val feedListOptions = FollowListState(accountViewModel.account, accountViewModel.viewModelScope) + val drafts = FeedContentState(DraftEventsFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) + suspend fun init() { notificationSummary.initializeSuspend() feedListOptions.initializeSuspend() @@ -97,6 +100,8 @@ class AccountFeedContentStates( notificationSummary.invalidateInsertData(newNotes) feedListOptions.updateFeedWith(newNotes) + + drafts.updateFeedWith(newNotes) } fun destroy() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index fa3fa2a02b..5815003018 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -222,7 +222,7 @@ class AccountViewModel( } } - if (flows != null) { + if (!flows.isNullOrEmpty()) { combine(flows) { it.any { it } } @@ -667,9 +667,7 @@ class AccountViewModel( note: Note, type: ReportType, content: String = "", - ) { - runIOCatching { account.report(note, type, content) } - } + ) = runIOCatching { account.report(note, type, content) } fun report( user: User, @@ -681,17 +679,11 @@ class AccountViewModel( } } - fun boost(note: Note) { - runIOCatching { account.boost(note) } - } + fun boost(note: Note) = runIOCatching { account.boost(note) } - fun removeEmojiPack(emojiPack: Note) { - runIOCatching { account.removeEmojiPack(emojiPack) } - } + fun removeEmojiPack(emojiPack: Note) = runIOCatching { account.removeEmojiPack(emojiPack) } - fun addEmojiPack(emojiPack: Note) { - runIOCatching { account.addEmojiPack(emojiPack) } - } + fun addEmojiPack(emojiPack: Note) = runIOCatching { account.addEmojiPack(emojiPack) } fun addMediaToGallery( hex: String, @@ -701,13 +693,9 @@ class AccountViewModel( dim: DimensionTag?, hash: String?, mimeType: String?, - ) { - runIOCatching { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) } - } + ) = runIOCatching { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) } - fun removeFromMediaGallery(note: Note) { - runIOCatching { account.removeFromGallery(note) } - } + fun removeFromMediaGallery(note: Note) = runIOCatching { account.removeFromGallery(note) } fun hashtagFollows(user: User): Note = LocalCache.getOrCreateAddressableNote(HashtagListEvent.createAddress(user.pubkeyHex)) @@ -736,13 +724,9 @@ class AccountViewModel( } } - fun delete(notes: List) { - runIOCatching { account.delete(notes) } - } + fun delete(notes: List) = runIOCatching { account.delete(notes) } - fun delete(note: Note) { - runIOCatching { account.delete(note) } - } + fun delete(note: Note) = runIOCatching { account.delete(note) } fun cachedDecrypt(note: Note): String? = account.cachedDecryptContent(note) @@ -790,61 +774,33 @@ class AccountViewModel( } } - fun follow(community: AddressableNote) { - runIOCatching { account.follow(community) } - } + fun follow(community: AddressableNote) = runIOCatching { account.follow(community) } - fun follow(channel: PublicChatChannel) { - runIOCatching { account.follow(channel) } - } + fun follow(channel: PublicChatChannel) = runIOCatching { account.follow(channel) } - fun follow(channel: EphemeralChatChannel) { - runIOCatching { account.follow(channel) } - } + fun follow(channel: EphemeralChatChannel) = runIOCatching { account.follow(channel) } - fun unfollow(community: AddressableNote) { - runIOCatching { account.unfollow(community) } - } + fun unfollow(community: AddressableNote) = runIOCatching { account.unfollow(community) } - fun unfollow(channel: PublicChatChannel) { - runIOCatching { account.unfollow(channel) } - } + fun unfollow(channel: PublicChatChannel) = runIOCatching { account.unfollow(channel) } - fun unfollow(channel: EphemeralChatChannel) { - runIOCatching { account.unfollow(channel) } - } + fun unfollow(channel: EphemeralChatChannel) = runIOCatching { account.unfollow(channel) } - fun follow(user: User) { - runIOCatching { account.follow(user) } - } + fun follow(user: User) = runIOCatching { account.follow(user) } - fun unfollow(user: User) { - runIOCatching { account.unfollow(user) } - } + fun unfollow(user: User) = runIOCatching { account.unfollow(user) } - fun followGeohash(tag: String) { - runIOCatching { account.followGeohash(tag) } - } + fun followGeohash(tag: String) = runIOCatching { account.followGeohash(tag) } - fun unfollowGeohash(tag: String) { - runIOCatching { account.unfollowGeohash(tag) } - } + fun unfollowGeohash(tag: String) = runIOCatching { account.unfollowGeohash(tag) } - fun followHashtag(tag: String) { - runIOCatching { account.followHashtag(tag) } - } + fun followHashtag(tag: String) = runIOCatching { account.followHashtag(tag) } - fun unfollowHashtag(tag: String) { - runIOCatching { account.unfollowHashtag(tag) } - } + fun unfollowHashtag(tag: String) = runIOCatching { account.unfollowHashtag(tag) } - fun showWord(word: String) { - runIOCatching { account.showWord(word) } - } + fun showWord(word: String) = runIOCatching { account.showWord(word) } - fun hideWord(word: String) { - runIOCatching { account.hideWord(word) } - } + fun hideWord(word: String) = runIOCatching { account.hideWord(word) } fun isLoggedUser(pubkeyHex: HexKey?): Boolean = account.signer.pubKey == pubkeyHex @@ -927,17 +883,14 @@ class AccountViewModel( fun updateStatus( address: Address, newStatus: String, - ) { - runIOCatching { - account.updateStatus(LocalCache.getOrCreateAddressableNote(address), newStatus) - } + ) = runIOCatching { + account.updateStatus(LocalCache.getOrCreateAddressableNote(address), newStatus) } - fun deleteStatus(address: Address) { + fun deleteStatus(address: Address) = runIOCatching { account.deleteStatus(LocalCache.getOrCreateAddressableNote(address)) } - } fun urlPreview( url: String, @@ -1506,10 +1459,6 @@ class AccountViewModel( fun dataSources() = app.sources - suspend fun deleteDraft(draftTag: String) { - account.deleteDraft(draftTag) - } - suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent) fun createTempDraftNote( @@ -1582,7 +1531,7 @@ class AccountViewModel( root: InteractiveStoryBaseEvent, readingScene: InteractiveStoryBaseEvent, ) { - viewModelScope.launch(Dispatchers.IO) { + runIOCatching { val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.address()).relayHintUrl() val readingState = getInteractiveStoryReadingState(root.addressTag()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index 8a510adab3..1102c7ec0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -97,7 +97,7 @@ fun ChatroomMessageCompose( onWantsToReply: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit, ) { - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav) { WatchBlockAndReport( note = baseNote, showHiddenWarning = false, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt index 3083a29cd7..9f4c0fa908 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt @@ -48,7 +48,7 @@ fun filterNip04DMs( val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) val outbox = - authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } + authorHomeRelayEvent?.writeRelaysNorm() ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index ec6dd17374..056787bbb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -112,7 +112,9 @@ class ChatNewMessageViewModel : draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel.runIOCatching { + sendDraftSync() + } } } } @@ -349,12 +351,14 @@ class ChatNewMessageViewModel : val version = draftTag.current innerSendPost(null) cancel() - accountViewModel.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - account.deleteDraft(draftTag.current) + account.deleteDraftIgnoreErrors(draftTag.current) } else { innerSendPost(draftTag.current) } @@ -469,7 +473,7 @@ class ChatNewMessageViewModel : } if (draftTag != null) { - accountViewModel.account.createAndSendDraft(draftTag, template) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template) } else { accountViewModel.account.sendNip17PrivateMessage(template) } @@ -486,7 +490,7 @@ class ChatNewMessageViewModel : ) if (draftTag != null) { - accountViewModel.account.createAndSendDraft(draftTag, template) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template) } else { accountViewModel.account.sendNip04PrivateMessage(template) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index 94f18b6a64..cde640df8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -175,10 +175,9 @@ fun NewGroupDMScreen( // function when the postViewModel is released accountViewModel.runIOCatching { postViewModel.sendDraftSync() - delay(100) - nav.popBack() postViewModel.cancel() } + nav.popBack() }, onPost = { // uses the accountViewModel scope to avoid cancelling this @@ -188,7 +187,6 @@ fun NewGroupDMScreen( postViewModel.room?.let { nav.nav(routeToMessage(it, null, null, null, accountViewModel)) } - postViewModel.cancel() } nav.popBack() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 61fb907dd1..bc775bac9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -106,7 +106,9 @@ open class ChannelNewMessageViewModel : draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel.runIOCatching { + sendDraftSync() + } } } } @@ -260,7 +262,7 @@ open class ChannelNewMessageViewModel : } fun sendPost(onDone: suspend () -> Unit) { - viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { sendPostSync() onDone() } @@ -274,12 +276,14 @@ open class ChannelNewMessageViewModel : cancel() accountViewModel.account.signAndSendPrivately(template, channelRelays) - accountViewModel.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - account.deleteDraft(draftTag.current) + account.deleteDraftIgnoreErrors(draftTag.current) } else { val attachments = mutableSetOf() nip95attachments.forEach { @@ -288,7 +292,7 @@ open class ChannelNewMessageViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt index 668878e1d5..908b15fc7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt @@ -58,14 +58,14 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.navigation.topbars.TitleIconModifier import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index 2f87c20798..100ab27402 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -62,7 +62,7 @@ fun ChannelCardCompose( accountViewModel: AccountViewModel, nav: INav, ) { - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav) { if (forceEventKind == null || baseNote.event?.kind == forceEventKind) { CheckHiddenFeedWatchBlockAndReport( note = baseNote, @@ -94,7 +94,7 @@ fun NormalChannelCard( accountViewModel: AccountViewModel, nav: INav, ) { - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote, accountViewModel, nav) { showPopup -> CheckNewAndRenderChannelCard( baseNote, routeForLastRead, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt index 7c2112abf1..b6157d74c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt @@ -42,7 +42,7 @@ fun filterCommunitiesAllCommunities( filter = Filter( kinds = CommunityPostApprovalEvent.KIND_LIST, - ids = communityList, + tags = mapOf("a" to communityList), limit = 300, since = since, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt index fe3872fd80..40d6bf4575 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt @@ -25,11 +25,12 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent fun filterClassifiedsByCommunity( relay: NormalizedRelayUrl, - community: String, + community: Address, authors: Set?, since: Long? = null, ): List { @@ -40,9 +41,9 @@ fun filterClassifiedsByCommunity( relay = relay, filter = Filter( - authors = authors, + authors = listOf(community.pubKeyHex), kinds = listOf(CommunityDefinitionEvent.KIND), - ids = listOf(community), + tags = mapOf("d" to listOf(community.dTag)), limit = 300, since = since, ), @@ -59,11 +60,13 @@ fun filterCommunitiesByCommunity( return communitySet.set .mapNotNull { - filterClassifiedsByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) + it.value.communityAddress?.let { address -> + filterClassifiedsByCommunity( + relay = it.key, + community = address, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt index 6798e81f6b..334aeb33fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils fun filterCommunitiesGlobal( @@ -42,7 +41,7 @@ fun filterCommunitiesGlobal( relay = it.key, filter = Filter( - kinds = listOf(CommunityDefinitionEvent.KIND), + kinds = CommunityPostApprovalEvent.KIND_LIST, limit = 100, since = since, ), @@ -51,7 +50,7 @@ fun filterCommunitiesGlobal( relay = it.key, filter = Filter( - kinds = listOf(CommunityPostApprovalEvent.KIND), + kinds = CommunityPostApprovalEvent.KIND_LIST, limit = 100, since = since ?: TimeUtils.oneWeekAgo(), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 9d0ec55162..bd30e46660 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -142,22 +142,19 @@ fun NewProductScreen( titleRes = R.string.new_product, isActive = postViewModel::canPost, onCancel = { - try { - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendDraftSync() - nav.popBack() - postViewModel.cancel() - } - } catch (e: SignerExceptions.ReadOnlyException) { - // do nothing. + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.runIOCatching { + postViewModel.sendDraftSync() + postViewModel.cancel() } + nav.popBack() }, onPost = { try { accountViewModel.viewModelScope.launch(Dispatchers.IO) { postViewModel.sendPostSync() nav.popBack() - postViewModel.cancel() } } catch (e: SignerExceptions.ReadOnlyException) { accountViewModel.toastManager.toast( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 8cb8ed9707..265f6d544d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -104,20 +104,22 @@ open class NewProductViewModel : IZapRaiser { val draftTag = DraftTagState() + var accountViewModel: AccountViewModel? = null + var account: Account? = null + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel?.runIOCatching { + sendDraftSync() + } } } } } - var accountViewModel: AccountViewModel? = null - var account: Account? = null - var productImages by mutableStateOf>(emptyList()) val iMetaDescription = IMetaAttachments() @@ -288,26 +290,26 @@ open class NewProductViewModel : } suspend fun sendPostSync() { + val accountViewModel = accountViewModel ?: return val template = createTemplate() ?: return - accountViewModel?.account?.signAndSendPrivatelyOrBroadcast( - template, - relayList = { relayList }, - ) - - accountViewModel?.deleteDraft(draftTag.current) - + val version = draftTag.current cancel() + + accountViewModel.account.signAndSendPrivatelyOrBroadcast(template, relayList = { relayList }) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { val accountViewModel = accountViewModel ?: return if (message.text.isBlank()) { - accountViewModel.account.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt index 8697cf643d..ffd978b741 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt @@ -50,9 +50,9 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation +import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -211,7 +211,7 @@ fun SellProduct(postViewModel: NewProductViewModel) { } TextSpinner( - placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second, + placeholder = conditionTypes.first { it.first == postViewModel.condition }.second, options = conditionOptions, onSelect = { postViewModel.updateCondition(conditionTypes[it].first) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index 88bfc37633..95179f429f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -38,32 +38,27 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer +import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS +import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal.DraftEventsFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -74,42 +69,16 @@ fun DraftListScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val draftFeedViewModel: DraftEventsFeedViewModel = - viewModel( - key = "NostrDraftEventsFeedViewModel", - factory = DraftEventsFeedViewModel.Factory(accountViewModel.account), - ) - - RenderDraftListScreen(draftFeedViewModel, accountViewModel, nav) + RenderDraftListScreen(accountViewModel.feedStates.drafts, accountViewModel, nav) } @Composable private fun RenderDraftListScreen( - feedViewModel: DraftEventsFeedViewModel, + feedState: FeedContentState, accountViewModel: AccountViewModel, nav: INav, ) { - val lifeCycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(feedViewModel) { - feedViewModel.invalidateData() - } - - DisposableEffect(lifeCycleOwner) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - println("DraftList Start") - feedViewModel.invalidateData() - } - if (event == Lifecycle.Event.ON_PAUSE) { - println("DraftList Stop") - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } - } + WatchLifecycleAndUpdateModel(feedState) DisappearingScaffold( isInvertedLayout = false, @@ -119,15 +88,15 @@ private fun RenderDraftListScreen( accountViewModel = accountViewModel, ) { Column(Modifier.padding(it).fillMaxHeight()) { - RefresheableBox(feedViewModel) { - SaveableFeedState(feedViewModel.feedState, DRAFTS) { listState -> - RenderFeedState( - viewModel = feedViewModel, + RefresheableBox(feedState) { + SaveableFeedState(feedState, DRAFTS) { listState -> + RenderFeedContentState( + feedContentState = feedState, accountViewModel = accountViewModel, listState = listState, nav = nav, routeForLastRead = null, - onLoaded = { DraftFeedLoaded(it, listState, null, accountViewModel, nav) }, + onLoaded = { DraftFeedLoaded(it, listState, accountViewModel, nav) }, ) } } @@ -140,7 +109,6 @@ private fun RenderDraftListScreen( private fun DraftFeedLoaded( loaded: FeedState.Loaded, listState: LazyListState, - routeForLastRead: String?, accountViewModel: AccountViewModel, nav: INav, ) { @@ -207,7 +175,7 @@ private fun DraftFeedLoaded( NoteCompose( item, modifier = MaterialTheme.colorScheme.maxWidthWithBackground, - routeForLastRead = routeForLastRead, + routeForLastRead = null, isBoostedNote = false, isHiddenFeed = items.showHidden, quotesLeft = 3, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedViewModel.kt deleted file mode 100644 index a0c753bfb7..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedViewModel.kt +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal - -import androidx.compose.runtime.Stable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel - -@Stable -class DraftEventsFeedViewModel( - val account: Account, -) : FeedViewModel(DraftEventsFeedFilter(account)) { - class Factory( - val account: Account, - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = DraftEventsFeedViewModel(account) as T - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 443f99376e..cb623710bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -132,7 +132,7 @@ fun GeoHashActionOptions( tag: String, accountViewModel: AccountViewModel, ) { - val isFollowingTag by observeUserIsFollowingGeohash(accountViewModel.userProfile(), tag, accountViewModel) + val isFollowingTag by observeUserIsFollowingGeohash(tag, accountViewModel) if (isFollowingTag) { UnfollowButton { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index d8fbb9dc2c..f6c07d8576 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -145,7 +145,7 @@ fun HashtagActionOptions( tag: String, accountViewModel: AccountViewModel, ) { - val isFollowingTag by observeUserIsFollowingHashtag(accountViewModel.userProfile(), tag, accountViewModel) + val isFollowingTag by observeUserIsFollowingHashtag(tag, accountViewModel) if (isFollowingTag) { UnfollowButton { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index c9821ec971..d8aa344efb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -98,7 +98,6 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @@ -174,7 +173,6 @@ private fun NewPostScreenInner( // function when the postViewModel is released accountViewModel.runIOCatching { postViewModel.sendPostSync() - delay(100) nav.popBack() } }, @@ -183,9 +181,9 @@ private fun NewPostScreenInner( // function when the postViewModel is released accountViewModel.runIOCatching { postViewModel.sendDraftSync() - nav.popBack() postViewModel.cancel() } + nav.popBack() }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 4d38c1aa50..5b975b6bfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -146,7 +146,9 @@ open class ShortNotePostViewModel : draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel.runIOCatching { + sendDraftSync() + } } } } @@ -481,12 +483,14 @@ open class ShortNotePostViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - accountViewModel.account.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { val attachments = mutableSetOf() nip95attachments.forEach { @@ -495,7 +499,7 @@ open class ShortNotePostViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index dd280e7852..bd99422ba6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -138,10 +138,9 @@ fun NewPublicMessageScreen( // function when the postViewModel is released accountViewModel.runIOCatching { postViewModel.sendDraftSync() - delay(100) - nav.popBack() postViewModel.cancel() } + nav.popBack() }, onPost = { // uses the accountViewModel scope to avoid cancelling this @@ -149,9 +148,7 @@ fun NewPublicMessageScreen( accountViewModel.runIOCatching { postViewModel.sendPostSync() nav.popBack() - postViewModel.cancel() } - nav.popBack() }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index f3b8ff4fed..3c42d85e1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -127,7 +127,9 @@ class NewPublicMessageViewModel : draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel.runIOCatching { + sendDraftSync() + } } } } @@ -319,12 +321,14 @@ class NewPublicMessageViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - accountViewModel.account.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { val broadcast = mutableSetOf() nip95attachments.forEach { @@ -333,7 +337,7 @@ class NewPublicMessageViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template, broadcast) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index aeb22a742e..d83168ddb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -47,7 +47,7 @@ fun GalleryCardCompose( nav: INav, ratio: Float = 1.0f, ) { - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, shortPreview = true) { + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav, shortPreview = true) { CheckHiddenFeedWatchBlockAndReport( note = baseNote, modifier = modifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt index 663b21dd72..c10933db85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt @@ -60,10 +60,10 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.LightRedColor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 64523f48d3..0248eecc07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -56,13 +56,13 @@ import com.vitorpamplona.amethyst.model.parseFeatureSetType import com.vitorpamplona.amethyst.model.parseGalleryType import com.vitorpamplona.amethyst.model.parseThemeType import com.vitorpamplona.amethyst.ui.components.PushNotificationSettingsRow +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.mockSharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 02b5a801c3..b8934c2a97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -68,6 +68,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.WarningType import com.vitorpamplona.amethyst.model.parseWarningType import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenWord +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -75,8 +77,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.elements.AddButton import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenWordsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.SpammerAccountsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index d03940fff4..1beaef7334 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -395,6 +395,7 @@ fun NoteMaster( WatchNoteEvent( baseNote = baseNote, accountViewModel = accountViewModel, + nav, modifier, ) { CheckHiddenFeedWatchBlockAndReport( @@ -405,7 +406,7 @@ fun NoteMaster( accountViewModel = accountViewModel, nav = nav, ) { canPreview -> - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote, accountViewModel, nav) { showPopup -> FullBleedNoteCompose( baseNote, modifier @@ -552,7 +553,7 @@ private fun FullBleedNoteCompose( } else if (noteEvent is PictureEvent) { PictureDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, PaddingValues(vertical = Size5dp), backgroundColor, accountViewModel = accountViewModel, nav) } else if (noteEvent is BaseVoiceEvent) { - VoiceHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav) + VoiceHeader(noteEvent, baseNote, accountViewModel, nav) } else if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 5e10e20752..af3b6678f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -167,6 +167,7 @@ val Size39Modifier = Modifier.size(39.dp) val Size40Modifier = Modifier.size(40.dp) val Size50Modifier = Modifier.size(50.dp) val Size55Modifier = Modifier.size(55.dp) +val Size75Modifier = Modifier.size(75.dp) val TinyBorders = Modifier.padding(2.dp) val NoSoTinyBorders = Modifier.padding(start = 5.dp, end = 5.dp, top = 2.dp, bottom = 2.dp) @@ -353,3 +354,7 @@ val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder) val SimpleHeaderImage = Modifier.fillMaxWidth().heightIn(max = 200.dp) val BadgePictureModifier = Modifier.size(35.dp).clip(shape = CutCornerShape(20)) + +val MaxWidthPaddingTop5dp = Modifier.fillMaxWidth().padding(top = 5.dp) + +val VoiceHeightModifier = Modifier.fillMaxWidth().height(100.dp) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt index fc370db073..782e60b4be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt @@ -46,8 +46,8 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 567ef893e1..f6e4f8aac0 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1010,4 +1010,6 @@ Vyhledávání hashtag: #%1$s Nepřekládat z Zde zobrazené jazyky nebudou přeloženy. Vyberte jazyk, který chcete odstranit a nechat je znovu přeložit. + Pozastavit + Hrát diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index fc85f6d271..223c59d5fb 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -933,4 +933,7 @@ Vyberte seznam pro filtrování kanálu Odhlásit se na zámek zařízení Sdílet obrázek… + Možnost %1$s z %2$s + Filtr kanálu, %1$s vybráno + Filtr kanálu, %1$s diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index d9aea16725..5d7816ae6a 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1015,4 +1015,6 @@ anz der Bedingungen ist erforderlich Suche Hashtag: #%1$s Nicht übersetzen von Die hier angezeigten Sprachen werden nicht übersetzt. Wählen Sie eine Sprache, um sie zu entfernen und lassen Sie sie erneut übersetzen. + Pausen + Abspielen diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index c7e84846a4..ff6ebf190a 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -973,4 +973,7 @@ anz der Bedingungen ist erforderlich Blockierte Relays Amethyst wird sich niemals mit diesen Relays verbinden Öffentliche Nachricht + Option %1$s von %2$s + Feed-Filter, %1$s ausgewählt + Feed-Filter, %1$s diff --git a/amethyst/src/main/res/values-fr-rFR/strings.xml b/amethyst/src/main/res/values-fr-rFR/strings.xml index 9487c1c913..f4a15147b9 100644 --- a/amethyst/src/main/res/values-fr-rFR/strings.xml +++ b/amethyst/src/main/res/values-fr-rFR/strings.xml @@ -51,6 +51,13 @@ Connectez-vous avec une clé privée pour ne plus Suivre Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir masquer un mot ou une phrase Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir afficher un mot ou une phrase + Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir modifier les paramètres + Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir téléverser + Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir signer des événements + Déchiffrement non autorisé + Le signataire n\'a pas autorisé le décryptage nécessaire pour effectuer cette opération. Activez les décryptions NIP-44 dans votre application signataire et réessayez + Signataire introuvable + L\'application de signature a-t-elle été désinstallée ? Vérifiez si le signataire est installé et a ce compte. Déconnectez-vous et reconnectez-vous de l\'application signataire a changé. Zaps Nombre de vues Boost @@ -71,6 +78,8 @@ Erreur d\'analyse du message d\'erreur " Abonnements" " Abonnés" + "%1$s abonnements" + "%1$s abonnés" Profil Filtres de sécurité Déconnexion @@ -96,6 +105,7 @@ Mon groupe spectaculaire Url de l\'image Description + Description introuvable "à propos…" Quoi de neuf ? Rédiger un message… @@ -137,6 +147,10 @@ Vidéo enregistrée dans la galerie vidéo du téléphone Impossible d\'enregistrer la vidéo Charger une image + Prendre une photo + Enregistrer un message + Enregistrer un message + Cliquez et maintenez pour enregistrer un message Chargement… L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats "répondre ici" @@ -155,6 +169,7 @@ Galerie "Suivis" "Signalements" + "%1$s rapports" Plus d\'options " Relais" Site Web @@ -213,6 +228,13 @@ Ne plus suivre Canal créé "Les informations du canal ont été remplacées par" + Discussion éphémère + Relais de discussion + Relais de discussions + Les relais de discussions sont des groupes de discussion contrôlés par leur relais d\'origine. + Ils sont visibles par tous les utilisateurs de Nostr et tout le monde peut y participer. + Ils sont idéaux pour les communautés ouvertes autour de sujets spécifiques. Certains de ces groupes sont éphémères + et, par conséquent, les messages de discussion disparaissent avec le temps Chat public Métadonnées de tchat public Les conversations publiques sont visibles pour tout le monde sur Nostr et n\'importe qui @@ -221,6 +243,8 @@ Relais Insérez entre 1 et 3 relais qui hébergent ce groupe. Les clients Nostr utilisent ce paramètre pour savoir où télécharger les messages et où les envoyer. + Relais payant + Force Tor lors de la connexion messages reçus Retirer Auto @@ -265,6 +289,7 @@ Erreur "Créé par %1$s" "Image de badge pour %1$s" + Image du badge de récompense Vous avez reçu un nouveau Badge Le Badge a été décerné à Copie du contenu de la note dans le presse-papiers @@ -290,6 +315,7 @@ Ne plus suivre Suivre Supprimer de la galerie + Supprimer ce média de votre galerie. Demande de Suppression Amethyst demandera que votre note soit supprimée des relais auxquels vous êtes actuellement connecté. Il n\'y a aucune garantie que votre note sera définitivement supprimée de ces relais, ou d\'autres relais où elle peut être stockée. Bloquer @@ -411,6 +437,7 @@ Non Liste des Abonnés Tous les abonnements + Suivre via Proxy Autour de moi Général Liste en Sourdine @@ -485,9 +512,18 @@ Toujours cacher les contenus sensibles Toujours afficher les contenus sensibles Toujours afficher les avertissements de contenu + Masquer + Afficher + Avertir Recommendations: Filtrer le spam des inconnus Avertir lorsque les messages ont été rapportés par vos abonnés + Filtrer les spams + Masque les messages d\'inconnus qui étaient exactement les mêmes depuis 5 fois ou plus + Avertir sur les rapports + Affiche un message d\'avertissement lorsque des messages ont été rapportés 5 fois ou plus par vos abonnements + Afficher le contenu sensible + Affiche un message d\'avertissement lorsque l\'auteur du message l\'a marqué comme sensible Nouveau symbole de réaction Aucun type de réaction sélectionné. Appuyer longuement pour changer Collecte de Zaps @@ -539,7 +575,12 @@ Se déconnecter supprime toutes vos informations locales. Assurez-vous d\'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ? Tags suivis Relais + Packs à suivre + Lectures + Algorithmes de flux Place de Marché + Diffusions en direct + Communautés Salons Messages approuvés Ce groupe n\'a pas de description ou de règles. Parlez au propriétaire pour en ajouter une @@ -547,6 +588,7 @@ Contenu sensible Ajouter un avertissement de contenu sensible avant de montrer ce contenu Préférences de l\'application + Préférences utilisateur Paramètres Toujours Wifi uniquement @@ -591,6 +633,8 @@ Ajoute un Geohash de votre emplacement au message. Le public saura que vous êtes à moins de 5km de l\'emplacement actuel Post exclusif à la localisation Seuls les abonnés à cette localisation le verront. Vos abonnés généraux ne le verront pas. + Message à hashtag-exclusif + Seuls les abonnés du hashtag le verront. Vos abonnés généraux ne le verront pas. Chargement de la localisation Pas d\'autorisations de localisation 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 @@ -599,6 +643,7 @@ Activer Public Nouveau Groupe Public ou Privé + Relais Privé À Sujet @@ -664,6 +709,7 @@ Portefeuille %1$s Erreur lors de l\'ouverture de l\'application de signature L\'application signataire est introuvable. Vérifiez que l\'application n\'a pas été désinstallée + Requête de signature rejetée Demande de signature rejetée Assurez-vous que l\'application signataire a autorisé cette transaction Aucun portefeuille trouvé pour payer une facture lightning (Erreur : %1$s). Veuillez installer un portefeuille Lightning pour utiliser les zaps @@ -791,6 +837,8 @@ Nouveau Message Nouveaux Shorts : images ou vidéos Nouvelle Note Communautaire + Nouveau produit + Nouveau message géo-exclusif Ouvrir toutes les réactions à ce message Fermer toutes les réactions à ce message Répondre @@ -845,12 +893,29 @@ Insérez entre 1 et 3 relais pour stocker des événements que personne d\'autre ne peut voir, comme vos Brouillons et/ou paramètres d\'application. Idéalement, ces relais sont soit locaux soit nécessitent une authentification avant de télécharger le contenu de chaque utilisateur. Relais généraux Amethyst utilise ces relais pour télécharger des messages pour vous. + Relais connectés + Liste actuelle des relais utilisés Relais recommandés Ajoutez les relais suivants à votre liste de relais généraux afin de recevoir les messages des utilisateurs listés. Relais de recherche Liste des relais à utiliser pour la recherche et l\'étiquetage des utilisateurs. L\'étiquetage et la recherche ne fonctionneront pas si aucune option n\'est disponible. Relais locaux Liste des relais qui sont en cours d\'exécution sur cet appareil. + Relais de confiance + Relais de confiance + Relais de confiance ne nécessitant pas une connexion Tor pour + Relais Proxy + Relais Proxy + Relais agrégateurs que l\'application doit utiliser pour télécharger vos flux, tels que filter.nostr.wine. Cela remplace le modèle de boîte d\'envoi et permettra à l\'application de se connecter uniquement aux relais de vos listes. + Relais de diffusion + Relais de diffusion + Relais qui se spécialisent dans la publication de vos notes à tous les autres relais, comme sendit.nosflare.com. Amethyst ajoutera ce relais à tous vos nouveaux événements + Relais indexeur + Relais indexeur + Relais qui se spécialisent dans l\'hébergement des métadonnées et des listes de relais de chacun, comme purplepag.es. Amethyst utilisera ces relais pour trouver des utilisateurs qui ne sont pas dans vos listes. + Relais bloqués + Relais bloqués + Amethyst ne se connectera jamais à ces relais Zap les Devs ! Votre don nous aide à faire la différence. Chaque sat compte ! Faire un don maintenant @@ -940,4 +1005,13 @@ Sélectionnez une liste pour filtrer le fil Se déconnecter au verrouillage de l\'appareil Message privé + Message public + Relais de chat + Le relais auquel tous les utilisateurs de ce chat se connectent + Partager l\'image… + Hashtag de recherche : #%1$s + Ne pas traduire depuis + Les langues affichées ici ne seront pas traduites. Sélectionnez une langue pour la supprimer et l\'avoir traduite à nouveau. + Pause + Lecture diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 88760ed372..19efe1c365 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -1012,4 +1012,6 @@ विषयसूचक खोज : #%1$s अनुवाद ना करें यहाँ प्रस्तुत भाषाओं का अनुवाद नहीं होगा। भाषा चयन करें हटाने के लिए जिससे उसका अनुवाद पुनः होने लगेगा। + विराम + चलाएँ diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index c87d142d55..fb63217b7d 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -1012,4 +1012,6 @@ Hashtag keresése: #%1$s Innentől NE fordítsa le Az itt látható nyelvek nem lesznek lefordítva. Az eltávolításához és az újbóli fordításhoz válasszon ki egy nyelvet. + Szüneteltetés + Lejátszás diff --git a/amethyst/src/main/res/values-nl-rNL/strings.xml b/amethyst/src/main/res/values-nl-rNL/strings.xml index d4e9229b7a..50d666ced4 100644 --- a/amethyst/src/main/res/values-nl-rNL/strings.xml +++ b/amethyst/src/main/res/values-nl-rNL/strings.xml @@ -1012,4 +1012,6 @@ Zoek hashtag: #%1$s Niet vertalen van Talen die hier getoond worden worden worden niet vertaald. Selecteer een taal om te verwijderen en het opnieuw te laten vertalen. + Pauze + Afspelen diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 94d56519b7..6c651da040 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -162,7 +162,7 @@ Nowe Kontakty Zablokowani użytkownicy Nowe Wpisy - Konwersacje + Komentarze Wpisy Odpowiedzi Twoje @@ -572,9 +572,9 @@ Wylogowanie usuwa wszystkie informacje lokalne. Upewnij się, że masz kopię zapasową kluczy prywatnych, aby uniknąć utraty konta. Czy chcesz kontynuować? Obserwowane tagi Transmitery - Obserwowani - Wyświetlenia - Algorytmy kanału + Godne uwagi + Popularne + Wybrane przez algorytm Market Transmisja na żywo Społeczności @@ -1009,4 +1009,6 @@ Szukaj tagu: #%1$s Nie tłumacz z Języki wyświetlane tutaj nie będą tłumaczone. Wybierz język, aby usunąć go z listy języków nietłumaczonych. + Pauza + Odtwórz diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 40dd5406e6..037614def0 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -247,8 +247,8 @@ Forçar uso do Tor ao conectar postagens recebidas Remover - Automaticamente - traduzido de + Autotraduzido + de para Mostrar em %1$s primeiro Chat público sobre %1$s @@ -1010,4 +1010,9 @@ Pesquisar hashtag: #%1$s Não Traduzir de Os idiomas mostrados aqui não serão traduzidos. Selecione um idioma para removê-lo e traduzi-lo novamente. + Pausar + Reproduzir + Opção %1$s de %2$s + Filtro de feed, %1$s selecionado + Filtro de feed, %1$s diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 671dd309aa..fa986f452f 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1009,4 +1009,9 @@ Sök hashtag: #%1$s Översätt inte från Språk som visas här kommer inte att översättas. Välj ett språk för att ta bort det och få det översatt igen. + Pausa + Spela + Alternativ %1$s av %2$s + Flödesfilter, %1$s valt + Flödesfilter, %1$s diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 2ff9f4e653..b598945356 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -51,13 +51,13 @@ 你正在使用公钥,公钥是只读的。使用私钥登录以便能够取消关注 你正在使用公钥,公钥是只读的。使用私钥登录以便能够隐藏单词或句子 你正在使用公钥,公钥是只读的。使用私钥登录以便能够显示单词或句子 - 你正在使用公钥,公钥是只读的。要更改设置请用私钥登录 - 你正在使用公钥,公钥是只读的。要上传请使用私钥登录 - 你正在使用公钥,公钥是只读的。要报名参加活动请使用私钥登录 + 您正在使用公钥以只读方式登录,需要使用私钥登录才能更改设置。 + 您正在使用公钥以只读方式登录,需要使用私钥登录才能上传。 + 您正在使用公钥以只读方式登录,需要使用私钥登录才能签署事件。 未授权的解密 - 签名人没有授权进行此操作所需的解密。在签名人应用中激活 NIP-44 解密并重试 - 未找到签名人 - 签名人应用被卸载了吗?检查是否安装了签名人以及是否签名人有该账户。注销并再次登录,签名人应用已更改。 + 签名器没有授权解密操作,请在签名器中授予 NIP-44 解密权限并重试。 + 未找到签名器 + 签名器被卸载?请检查是否已经安装了签名器以及其中是否存在该账户。变更签名器需要注销后重新登录。 打闪 浏览次数 提升 @@ -150,7 +150,7 @@ 拍照 录制消息 录制消息 - 单击并按住来录制消息 + 点按以录制消息 上传中… 用户尚未设置闪电地址以接收聪 "🔏在此回复… " @@ -231,10 +231,10 @@ 临时聊天 中继聊天 中继聊天 - 中继聊天是由主中继控制的聊天群。 - 它们对 Nostr 上的所有人可见,任何人均可加入它们。 - 它们对围绕特定话题的开放社区很好。这些群中的一些是临时存在的 - 因而,聊天消息会随着时间消失 + 中继聊天是由聊天所属中继控制的聊天群组。 + Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 + 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, + 因此聊天信息会随着时间的推移而消失。 公共聊天 公开聊天元数据 公开聊天对所有 Nostr 用户都是可见的,任何人都可以: @@ -244,7 +244,7 @@ 使用 1~3 个中继托管这个群组。 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。 付费中继 - 连接时强制使用 Tor + 强制使用 Tor 进行连接 收到文章 移除 自动 @@ -289,7 +289,7 @@ 错误 "由 %1$s 创建" "颁发给 %1$s 的徽章图片" - \"徽章奖品图片 + \"徽章图片 你收到了新的徽章奖励 徽章奖励授予 文本已复制到剪贴板 @@ -577,7 +577,7 @@ 中继器 关注包 次浏览 - Feed算法 + 动态源算法 市场 直播 社区 @@ -633,8 +633,8 @@ 将你所在位置的地理位置添加到帖子。公众会知道你在当前位置的5公里之内(3英里) 位置限定帖子 只有处于同一地理位置的关注者才能看到贴文。其他追随者无法看到。 - 话题标签专属帖子 - 只有话题标签的关注者才会看到它。您的一般关注者不会看到它。 + 话题限定贴文 + 只有关注了话题的人才能看到贴文,您的普通关注者也无法看到。 加载位置中 没有位置信息权限 在显示你的内容之前添加敏感的内容警告。针对任何 NSFW 内容或一些人可能觉得有冒犯性或令人不安的内容。 @@ -709,7 +709,7 @@ 钱包 %1$s 打开签名应用时出错 找不到签名应用。检查应用是否已被卸载 - 签名请求被拒绝了 + 签名请求已被拒绝 签名请求被拒绝了 请确保签名应用程序已授权此交易 找不到支付闪电发票的钱包(错误:%1$s)。请安装闪电钱包来使用打闪 @@ -838,7 +838,7 @@ 新短篇媒体:图像或视频 新社区笔记 新产品 - 新建地理位置专属帖子 + 新建地理位置限定帖文 展开对此帖子的所有回应 收起对此帖子的所有回应 回复 @@ -903,19 +903,19 @@ 在该设备中运行的中继列表。 可信中继 可信中继 - 你信任的无需 Tor 连接的中继 + 无需使用 Tor 进行连接的可信中继 代理中继 代理中继 - 聚合器中继下载 feed 必须使用的应用的流量,类似 filter.nostr.wine。这替代 outbox 模型,让应用只连接到列表中的中继。 + 聚合器中继是应用获取资讯源时使用的中继(例如:filter.nostr.wine),设置这个列表将覆盖信箱模型的行为,使得应用只会连接到这个列表中的中继以获取内容。 广播中继 广播中继 - 专门推送便笩到所有其他中继的中继,类似 sendit.nosflare.com。Amethyst 会将这个中继添加到所有你所做的新事件 + 推送你的贴文到其他中继的中继(例如:sendit.nosflare.com),应用会把这个中继作为提示嵌入到你发送的所有事件的 JSON 结构中。 索引器中继 索引器中继 - 专门托管每个人的元数据和中继列表的中继,类似 purplepag.es。Amethyst将使用这些中继来查找不在列表中的用户。 - 屏蔽的中继 - 屏蔽的中继 - Amethyst 永远不会连接到这些中继 + 专门保存每个人的账户元数据的中继(例如:purplepag.es),应用会使用这些中继来查找不在你列表中的账户元数据。 + 中继黑名单 + 中继黑名单 + 应用永远不会连接的中继 打闪开发人员! 你的捐赠帮助我们做出不同的贡献。每个聪都很重要! 立即捐款 @@ -1010,6 +1010,8 @@ 此聊天所有用户都连接到的中继 分享图片… 搜索话题标签:#%1$s - 不要翻译自 - 此处显示的语言不会被翻译。请选择一种语言来移除它并重新翻译它。 + 不要翻译 + 此处显示的语言不会被翻译,请选择一种目标语言重新翻译并去除这个提示。 + 暂停 + 播放 diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 3612aae152..c4cbe33a6c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Amethyst + Amethyst Amy Debug Amy Benchmark Point to the QR Code @@ -1230,4 +1230,10 @@ Don\'t Translate From Languages shown here will not be translated. Select a language to remove it and have it translated again. + Pause + Play + Open dropdown menu + Option %1$s of %2$s + Feed filter, %1$s selected + Feed filter, %1$s diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index be36c33f9d..3973f07587 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.notifications +import android.util.Log import com.google.firebase.messaging.FirebaseMessaging import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.service.retryIfException @@ -35,12 +36,16 @@ object PushNotificationUtils { accounts: List, okHttpClient: (String) -> OkHttpClient, ) = with(Dispatchers.IO) { - val token = FirebaseMessaging.getInstance().token.await() - if (hasInit?.equals(accounts) == true && lastToken == token) { - return@with - } + try { + val token = FirebaseMessaging.getInstance().token.await() + if (hasInit?.equals(accounts) == true && lastToken == token) { + return@with + } - registerToken(token, accounts, okHttpClient) + registerToken(token, accounts, okHttpClient) + } catch (e: Exception) { + Log.e("PushNotificationUtils", "Failed to get Firebase token", e) + } } suspend fun checkAndInit( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 8a74e2cdec..87bbf0306e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -95,6 +95,14 @@ class RelayUrlNormalizer { @OptIn(ExperimentalContracts::class) fun fix(url: String): String? { if (url.length < 3) return null + if (url.length > 100) { + // removes multiple urls in the same line + val schemeIdx = url.indexOf("://") + val nextScheme = url.indexOf("://", schemeIdx + 3) + if (nextScheme > 0) { + return null + } + } val trimmed = if (url[0].isWhitespace() || url[url.length - 1].isWhitespace()) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index c9dac836ce..b43d0e8ab7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -60,7 +60,7 @@ class ChannelCreateEvent( val newInfo = try { - if (isEncrypted()) { + if (content.isEmpty() || !content.startsWith("{") || isEncrypted()) { ChannelDataNorm() } else { ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt index 526214f20b..eabbdb7542 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -66,7 +66,7 @@ class ChannelMetadataEvent( val newInfo = try { - if (isEncrypted()) { + if (content.isEmpty() || !content.startsWith("{") || isEncrypted()) { ChannelDataNorm() } else { ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() diff --git a/zapstore.yaml b/zapstore.yaml index 47dabbb2ef..fb1e369983 100644 --- a/zapstore.yaml +++ b/zapstore.yaml @@ -6,11 +6,6 @@ amethyst: builder: npub142gywvjkq0dv6nupggyn2euhx4nduwc7yz5f24ah9rpmunr2s39se3xrj0 repository: https://github.com/vitorpamplona/amethyst artifacts: - - amethyst-fdroid-arm64-v8a-v%v.apk - - amethyst-fdroid-armeabi-v7a-v%v.apk - - amethyst-fdroid-universal-v%v.apk - - amethyst-fdroid-x86-v%v.apk - - amethyst-fdroid-x86_64-v%v.apk - amethyst-googleplay-arm64-v8a-v%v.apk - amethyst-googleplay-armeabi-v7a-v%v.apk - amethyst-googleplay-universal-v%v.apk @@ -19,3 +14,4 @@ amethyst: +